前言
本篇为嵌入式 Linux 系统编程第五天完整复盘笔记,开篇先整理当日早测全套 Linux 文件 IO、用户信息、目录解析、时间转换等口述考点,再讲解glob文件匹配、getopt命令行参数解析两大工具函数;核心重点讲解 Linux 进程完整体系:进程定义、资源分配、fork 创建子进程、进程继承属性、8 种进程终止方式、atexit/on_exit终止回调、wait回收子进程、exec 族进程替换函数;最后配套质数筛选多进程优化、简易 shell 模拟两大课堂作业。
一、当日早测全套口述考点(Linux 文件 IO & 系统基础)
早测全部为简答题,覆盖前几日文件 IO、标准 IO、目录、时间、用户信息核心知识点,完整标准答案整理如下:
1. 文件的元信息都有什么?
文件元信息是不包含文件真实内容、用于描述文件属性的配套信息,通过stat/lstat/fstat函数读取,核心包含:
- 文件类型:普通文件、目录、管道、软链接、设备文件等;
- 文件权限:rwx 读写执行权限、SUID/SGID 特殊权限位;
- 文件归属:UID 文件所有者、GID 所属用户组;
- 文件大小:字节为单位的文件体积;
- 时间戳:最后访问时间 atime、最后修改内容 mtime、最后修改属性 ctime;
- 文件硬链接计数、文件存储设备号、inode 编号、文件读写块大小等。
2. Linux 文件类型有几种?分别是?
Linux 共 7 种基础文件类型,通过 stat 结构体st_mode判断:
-普通文件:文本、二进制、可执行程序;d目录文件:存放文件索引与 inode 映射;l软链接文件:符号链接,指向其他文件路径;b块设备文件:硬盘、SD 卡等块存储设备;c字符设备文件:串口、键盘、LED 等字节流外设;p管道文件:匿名管道,进程间单向通信;s套接字文件:本地 socket,进程间双向通信。
3. 文件 IO 中文件描述符是什么?
文件 IO 属于系统调用(open/read/write/close),文件描述符 fd 是内核分配给进程的非负整型索引:
- 进程打开文件时,内核在进程文件描述符表分配一个数字;
- 0 标准输入、1 标准输出、2 标准错误,默认自动打开;
- 所有读写操作均通过 fd 告知内核操作哪个文件;
- 进程独立 fd 表,不同进程相同 fd 代表不同文件。
4. 标准 IO 中文件流是什么?
标准 IO(fopen/fread/fprintf)封装在 C 库层,核心载体为FILE*文件流指针:
- 对底层文件描述符做上层封装,内置缓冲区;
- 统一屏蔽不同系统底层 IO 差异,跨平台兼容性更好;
- 一个 FILE 流绑定一个底层 fd,操作时库函数自动管理缓存。
5. 标准 io 缓存有哪些类型?有什么作用?如何刷新缓存
三种缓存类型
- 全缓冲:缓冲区填满才触发真实系统调用写磁盘,普通文件默认;
- 行缓冲:读到换行符
\n自动刷新,标准输出 stdout 默认; - 无缓冲:无缓存,每次调用直接下发系统调用,标准错误 stderr 默认。
缓存核心作用
减少系统调用次数,降低 CPU 内核态 / 用户态切换开销,大幅提升读写性能。
缓存刷新方式
- 主动刷新:
fflush(FILE* stream)强制刷新指定流;fflush(NULL)刷新所有打开流; - 被动刷新:缓冲区写满、行缓冲读到换行、程序正常 exit 退出、文件 fclose 关闭;
- 强制不刷新:
_exit()/_Exit()直接调用内核,跳过库缓存刷新。
6. 如何解析目录
两种主流方案:
- 系统调用目录 API:
opendir()打开目录→readdir()循环读取目录项→closedir()关闭,读取每一个文件 inode、文件名、文件类型; - glob 函数:通配符批量匹配目录文件,自动遍历匹配
*?等通配路径。
7. 如果想读文件的一行,你有哪些方式
- 标准 IO:
fgets(buf, size, fp)按行读取,读到换行停止; - 标准 IO:
getline(&buf, &len, fp)动态分配缓冲区,自动适配长行; - 文件 IO 底层:
read()循环读取字节,手动判断\n分割行,自行封装行读取逻辑。
8. 如何将时间戳转换为格式化的时间字符串?
完整调用链路:
time_t t = time(NULL)获取秒级时间戳;struct tm* tm = localtime(&t)时间戳转本地时间结构体;strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm)自定义格式化输出时间字符串。
9. Linux 中用户的信息存储在哪个文件,密码存储在哪个文件?分别用哪些函数解析
- 用户基础信息:
/etc/passwd,解析函数getpwuid()、getpwnam(); - 用户加密密码:
/etc/shadow(仅 root 可读),解析函数getspuid()、getspnam()。
二、今日新课核心知识点
2.1 glob 文件通配符匹配函数
核心作用
模拟 shell 命令行通配符解析,自动匹配带*、?的路径,批量获取目录下匹配文件列表。 示例:命令行/etc/*代表匹配 /etc 目录下所有文件,代码中使用glob()可实现相同效果,无需手动遍历目录拼接字符串。 函数原型:int glob(const char *pattern, int flags, int (*errfunc)(const char *epath, int eerrno), glob_t *pglob);匹配结果存放在glob_t结构体,遍历gl_pathv数组获取所有匹配文件名。
2.2 getopt (3) 命令行参数解析
用于解析程序启动时传入的短选项(如./a.out -l -p 8080),自动拆分选项、选项参数,替代手动循环切割 argv 字符串,简化命令行工具开发。
2.3 Linux 进程完整体系
2.3.1 对应教材章节
进程基础:7、8、9、13 章;进程信号 / 替换:10、15 章。
2.3.2 进程核心概念
- 官方定义:进程是操作系统分配系统资源的最小单位,是正在内存中执行的程序实例;程序是磁盘静态二进制文件,进程是运行时动态实体。
- 进程独占资源划分(以单个进程为隔离单位)
- 进程表项:操作系统为每一个进程单独分配一条进程控制块 PCB,存储进程所有核心属性;
- 独立虚拟内存空间:每个进程拥有一套独立虚拟地址映射表,fork 创建子进程时采用写时复制 COW 机制,默认不共享物理内存;
- 独立终端会话、独立 UID/GID 权限标识、独立文件描述符表;
- Shell 与进程关系:shell 本质是运行在终端的前台进程,输入命令后 shell 创建子进程执行对应程序。
2.3.3 进程完整生命周期:创建 → 调度运行 → 终止
- 创建:fork 生成子进程;
- 调度:操作系统内核调度器分时分配 CPU 时间片,切换进程运行;
- 终止:进程执行完毕或异常崩溃,释放资源,父进程调用 wait 回收残留 PCB。
- 查看系统全部运行进程命令:
ps axj,可查看 PID、PPID 父进程 ID、终端、CPU 占用、命令等信息。
2.4 进程创建 fork (2)
1. 函数原型
pid_t fork(void);2. pid_t 进程标识类型
- 本质为非负整型数据,最小值 0;
- 调用
getpid()获取当前进程自身 PID;getppid()获取父进程 PID。
3. fork 返回值三重逻辑
- 返回负数:fork 创建失败,资源不足;
- 返回 0:当前代码运行在子进程;
- 返回正数(子进程 PID):当前代码运行在父进程。
4. 子进程从父进程继承的全部属性
fork 瞬间子进程复制父进程几乎所有上下文,无需重新初始化:
- 所有已打开的文件描述符;
- 当前绑定的终端会话;
- 用户 UID、用户组 GID;
- 文件权限屏蔽字 umask;
- 信号处理函数、当前工作目录、环境变量、缓存区数据; 不继承:PID、PPID、CPU 运行时间、异步 IO 上下文。
2.5 进程终止(共 8 种终止方式,分为正常 / 异常两大类)
第一类:5 种正常终止(主动退出,可传递退出状态码)
- main 函数执行 return 返回;
- 程序任意位置调用标准库
exit(3); - 调用系统调用
_exit(2)/_Exit(2); - 进程最后一个执行线程从线程入口函数 return 返回;
- 进程最后一个线程调用
pthread_exit(3)线程退出函数。
第二类:3 种异常终止(程序被动崩溃,无正常退出码)
- 收到内核致命信号强制终止:段错误、数组越界、重复释放内存、总线错误等;
- 代码主动调用
abort(3),触发 SIGABRT 信号自杀; - 进程最后一个线程响应线程取消请求,被动终止。
2.5.1 C 程序启动与终止底层流程
参考教材 167 页流程图:
- 内核加载程序后,先执行系统启动例程
_start; _start初始化标准 IO、环境变量,调用用户 main 函数;- main 执行完毕后,自动调用
exit(status); - exit 会逆序执行所有注册的终止回调函数,刷新标准 IO 缓存,最终调用
_exit进入内核销毁进程。
2.5.2 终止处理函数注册 atexit /on_exit
- 作用:用户提前注册回调函数,进程调用
exit()正常退出前自动执行; - 执行规则:逆序执行,后注册的函数优先调用;
- 区别:
atexit(void (*func)(void)):无参数回调;on_exit(void (*func)(int status, void *arg), void *arg):可传递退出状态与自定义参数;
- 关键区分:
_exit(0)直接调用内核系统调用,不会刷新标准 IO 缓存、不会执行 atexit 注册的终止函数,直接销毁进程。
2.5.3 课堂练习:多进程质数筛选优化
需求:判断一个数字是否为质数单进程耗时 1s,范围 100~300 共 201 个数字,要求整体耗时控制在 1s 内。
- 基础思路:一个数字创建一个子进程,201 个进程并行计算,全部同时运行,总耗时 1s;
- 资源优化方案:频繁创建大量进程消耗内存、PID 资源,限制固定 4 个子进程;父进程维护任务列表,循环给空闲子进程分配数字任务,进程复用,降低系统开销。
2.6 等待子进程终止 wait (2)
核心功能:回收已结束子进程 PCB 资源(俗称收尸)
子进程终止后不会立刻释放资源,会变为僵尸进程;父进程调用wait(int *wstatus)阻塞等待任意一个子进程退出,读取退出状态,释放子进程内核资源,避免僵尸进程堆积。
2.7 进程替换 exec 函数族
2.7.1 使用场景
- 网络服务程序:父进程 fork 子进程,子进程 exec 替换为业务处理程序;
- Shell 命令行:shell fork 子进程后,调用 exec 加载外部命令程序(ls/cat/mkdir 等)。
2.7.2 函数共性
exec 族为可变参数函数,参数列表末尾以NULL作为结束标记;执行成功后直接替换当前进程代码段,原有代码不再执行;仅执行失败时返回 - 1。
2.7.3 五大 exec 函数详细区分
| 函数名 | 参数特征 | 路径查找规则 | 使用示例 |
|---|---|---|---|
| execl | l=list 可变参数列表 | 必须传入完整程序绝对路径 | execl("/bin/ls", "ls", "-l", "/home", NULL); |
| execv | v=vector 参数存 char * 数组 | 完整绝对路径 | char *argv[]={"ls","-l","/home",NULL}; execv("/bin/ls",argv); |
| execlp | l 可变参数 + p 自动搜 PATH | 仅传程序名,自动遍历 PATH 环境变量查找 | execlp("ls","ls","-l",NULL); |
| execvp | v 数组参数 + p 自动搜 PATH | 仅传程序名,自动匹配 PATH | char *argv[]={"ls","-l",NULL}; execvp("ls",argv); |
补充字符串分割工具
解析命令行字符串时拆分参数,推荐strsep()(线程安全)、strtok(),切割空格分隔的命令参数。
2.8 课堂作业 1:模拟简易 Shell 命令行
完整实现逻辑框架
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> int main(void) { char buf[128]; char *argv[32]; char *p; while(1) { // 1.打印命令提示符 printf("my_shell > "); fflush(stdout); // 2.读取整行输入命令 getline(&buf, &(size_t){128}, stdin); // 去除末尾换行符 buf[strcspn(buf, "\n")] = '\0'; // 3.strsep切割字符串,拆分命令+参数存入argv数组 int argc = 0; p = strsep(&buf, " "); while(p != NULL) { argv[argc++] = p; p = strsep(&buf, " "); } argv[argc] = NULL; // 空命令跳过 if(argc == 0) continue; // 4.fork创建子进程执行命令 pid_t pid = fork(); if(pid == 0) { // 子进程:execvp替换进程执行外部命令 execvp(argv[0], argv); // exec失败才会执行到这里 perror("execvp failed"); _exit(-1); } else if(pid > 0) { // 父进程阻塞等待子进程执行完毕 wait(NULL); } } return 0; }执行逻辑说明:循环读取用户输入→分割参数→创建子进程→子进程调用 execvp 执行系统命令→父进程 wait 回收子进程,复刻真实 shell 基础执行逻辑。
2.9 课堂作业 2:进程注册多个终止回调函数
需求:为进程注册 3 个 atexit 终止处理函数,打印区分先后顺序,验证后注册先执行规则
示例代码
#include <stdio.h> #include <stdlib.h> void func1(void) { printf("回调函数1执行\n"); } void func2(void) { printf("回调函数2执行\n"); } void func3(void) { printf("回调函数3执行\n"); } int main(void) { // 注册顺序:1→2→3 atexit(func1); atexit(func2); atexit(func3); printf("主程序运行结束,准备exit\n"); exit(0); }预期输出顺序
主程序运行结束,准备exit 回调函数3执行 回调函数2执行 回调函数1执行完美验证逆序执行规则。
三、本章核心知识点总结
- 早测 Linux 文件 IO 体系:文件元信息、7 类文件、fd/FILE 流、三类缓存、目录解析、行读取、时间戳转换、用户信息文件与解析函数;
- 工具函数:glob 实现通配文件匹配,getopt 解析命令行短参数;
- 进程基础:进程是资源分配最小单位,fork 复制进程、写时复制虚拟内存、继承父进程绝大多数属性;
- 进程终止:8 种终止方式,exit 执行注册回调并刷新缓存,_exit 直接内核退出无回调;atexit 逆序执行终止函数;
- 多进程优化:大量计算任务可并行 fork 提速,进程过多时采用固定进程池分配任务节约资源;wait 回收子进程防止僵尸进程;
- exec 进程替换:区分 execl/execv/execlp/execvp 路径与参数传递差异,搭配 fork 实现 shell、服务器多任务架构;
- 实战作业:简易 shell 模拟、多回调终止函数验证、多进程质数并行计算。
补充拓展易错点
- fork 后父子进程共享文件偏移量,同时读写同一文件会互相覆盖;
- exec 执行成功后不会返回,仅失败才会向下执行错误处理代码;
- 子进程终止后父进程不调用 wait 会产生僵尸进程,长期运行占用系统 PID 资源;
stdout行缓冲,fork 前不 fflush 会导致父子进程重复打印缓存内容;- execp 自动读取 PATH 环境变量,无需填写程序绝对路径,开发命令行工具更便捷。