Linux应用层开发--进程处理
ps -ef 可以看父进程和子进程
1、 进程处理相关系统调用
system 函数底层调用
system()
函数会间接调用以下几个系统调用来执行命令:
-
fork()
:创建子进程 -
execve()
:在子进程中执行命令 -
waitpid()
:等待子进程结束
main 函数声明(C 标准)
1️ 无参数形式
int main(void);
int main();
-
两者等价,表示 main 不接受命令行参数。
-
推荐使用
int main(void)
(尤其从 C99 开始),避免歧义。
有参数形式
int main(int argc, char *argv[]);
-
argc
:参数数量(包含程序名本身) -
argv
:字符串数组,保存所有命令行参数-
argv[0]
:程序名称 -
argv[1] ~ argv[argc-1]
:用户输入的命令行参数
-
🔧 2、 fork 函数详解
相关头文件
#include <sys/types.h>
#include <unistd.h>
类型说明:pid_t
-
pid_t
是int
的别名 -
定义过程:
typedef int __pid_t; typedef __pid_t pid_t;
fork()
pid_t fork(void);
-
功能:复制当前进程,创建一个子进程
-
返回值说明:
-
父进程中:返回子进程的 PID
-
子进程中:返回 0
-
创建失败:返回 -1
-
getpid()
pid_t getpid(void);
-
功能:返回当前进程的 PID
-
不会失败,一定有返回值
getppid()
pid_t getppid(void);
-
功能:返回当前进程的父进程 PID
-
不会失败
小结
-
fork()
是进程创建的基础,父子进程共享代码段但拥有独立的数据段。 -
getpid()
和getppid()
是用于确认进程身份的工具函数。 -
每次
fork()
之后,父子进程会并发执行接下来的代码,可以根据fork()
的返回值判断当前是哪个进程。
二、 文件描述符的引用计数和 close()
基础理解
在 Linux 中,文件描述符(FD) 本质上是一个引用,指向内核中的 struct file
数据结构。当一个进程执行 fork()
,子进程会复制父进程的所有文件描述符,也就是复制引用,但不是复制文件本身。
每个 struct file
结构体中有一个字段 f_count
,表示有多少文件描述符在使用它(也就是引用计数)。
sleep 函数介绍
#include <unistd.h> unsigned int sleep(unsigned int seconds);
-
使进程休眠指定秒数
-
若过程中收到信号,可能提前醒来
-
返回值:
-
正常结束:返回
0
-
被打断:返回剩余秒数
-
原理解析
为什么子进程 close(fd)
后父进程还能 write()
?
-
fork()
后父子进程共享相同的 struct file 结构体 -
fd
是文件描述符,它在进程内部是一个整型值 -
每个
fd
对应一个struct file
(底层实际对象)
struct file
中的 f_count
是引用计数:
-
初始打开:引用计数 = 1
-
fork()
后:引用计数 = 2(因为父子进程都有一个fd
) -
子进程
close(fd)
:引用计数减为 1,struct file
还在 -
父进程继续使用
write()
没问题
只有当最后一个引用释放,引用计数为 0,struct file
才会被销毁。
小结
操作 | 行为 |
---|---|
open() | 返回 fd ,并创建 struct file ,引用计数 = 1 |
fork() | 子进程复制 fd ,引用计数 = 2 |
close(fd) | 减少引用计数,不一定销毁 struct file |
write(fd) | 只要引用计数 > 0,文件可继续使用 |
🔐 避免重复关闭同一个 fd:每个进程只需关闭自己持有的文件描述符,不要重复关闭。
三、 execve 与进程替换
1)execve 单独使用测试
背景
-
exec
系列函数用于在当前进程中执行另一个程序,替换原进程的正文段和数据段。 -
不会创建新进程,调用成功后,原进程空间会被新程序完全替换,PID 不变。
-
常用函数:
execve()
是底层实现函数,最通用。
原理说明
int execve(const char *pathname, char *const argv[], char *const envp[]);
-
pathname
:程序的完整路径 -
argv[]
:参数数组,第一个元素是程序名,最后一个必须是NULL
-
envp[]
:环境变量数组,可为NULL
实验流程
-
编写
erlou.c
,作为目标执行程序(输出传参内容) -
编写
execve_test.c
,调用execve
跳转执行erlou
关键逻辑
char *args[] = {"/完整路径/erlou", "banzhang", NULL};
char *envs[] = {"PATH=...省略...", NULL};
execve(args[0], args, envs);
-
若参数不足,目标程序内部会打印“参数不够,上不了二楼”
-
一旦
execve()
成功,后续代码不再执行(控制权已转移)
2)fork + execve 联合使用
场景模拟
-
老学员在一楼学习(父进程)
-
通过
fork()
创建新学员进程 -
子进程使用
execve()
跳转至二楼(执行另一个程序) -
父进程保持原样继续学习
关键逻辑
pid_t pid = fork();
if (pid == 0) {char *args[] = {"/完整路径/erlou", "ergou", NULL};execve(args[0], args, envs);
}
-
fork()
创建子进程 -
子进程替换自己,进入新程序
-
父进程输出提示,不受影响
四、 waitpid:父进程回收子进程
为什么需要 wait/waitpid?
-
若父进程不回收子进程,子进程会变成僵尸进程
-
僵尸进程虽已结束,但其资源(如 PCB)未被释放
-
会被系统进程(init 或 systemd)回收,但应主动处理
waitpid 用法
pid_t waitpid(pid_t pid, int *status, int options);
-
pid
:要等待的子进程-1
:等待任意子进程(最常用)
-
status
:用于存储子进程的退出状态 -
options
:-
0
:阻塞等待 -
WNOHANG
:非阻塞 -
WUNTRACED
:也返回停止的子进程 -
WCONTINUED
:也返回继续执行的子进程
-
实验流程
-
创建子进程后,子进程执行 ping
-
父进程使用
waitpid()
等待子进程完成后回收
关键逻辑
pid_t pid = fork();
if (pid == 0) {char *args[] = {"/usr/bin/ping", "-c", "50", "www.atguigu.com", NULL};execve(args[0], args, NULL);
} else {waitpid(pid, &status, 0); // 父进程挂起等待子进程退出
}
总结
技术点 | 关键特性 |
---|---|
execve | 当前进程被新程序完全替换,原代码不再执行 |
fork + execve | 创建子进程,再让子进程切换到新程序,实现分离执行流程 |
waitpid | 用于等待和回收子进程,防止出现僵尸进程 |
以下是根据你提供的内容整理的 Obsidian 学习笔记格式,去掉了多余代码,只保留关键部分,突出概念理解、运行要点及注意事项:
Linux进程相关概念与实操笔记(二)
六、 进程树(Process Tree)
概念理解
-
Linux进程通过父子关系组织,形成“进程树”。
-
每个进程只有一个父进程,但可以有多个子进程。
-
所有进程最终追溯到:
-
用户空间:
PID=1
的进程,通常为systemd
-
内核空间:
PID=2
的第一个内核线程
-
-
这两个最早的进程,父进程ID为
0
,即由内核创建。
示例说明
-
使用
fork()
创建子进程。 -
子进程中调用
execve()
执行另一个程序。 -
父进程阻塞等待,防止程序提前退出,便于测试。
char bye = fgetc(stdin); // 阻塞等待输入,模拟挂起
进程树观察方法
-
ps -ef
:查看所有进程 -
pstree
:树状展示进程结构 -
pstree -p
:附带 PID 的树状结构 -
父进程终止后,子进程会被 systemd 自动领养(变为孤儿进程)
七、 孤儿进程(Orphan Process)
定义
-
父进程先于子进程结束,子进程尚未结束时,该子进程称为“孤儿进程”。
-
孤儿进程将被
init
(即 systemd)进程自动收养。
实验说明
-
子进程执行
sleep(100)
保持活跃 -
父进程不等待子进程结束,直接返回
-
使用
ps -ef | grep [pid]
查看子进程状态 -
使用
pstree
查看其被 systemd 收养
sleep(100); // 模拟子进程长时间运行
编程注意
-
避免父进程提前退出导致孤儿进程
-
可使用
wait()
或waitpid()
正确回收子进程
总结
概念 | 说明 |
---|---|
进程树 | 所有用户进程最终来源于 PID=1 的 systemd |
内核线程 | 显示为 [] ,起始 PID=2,非用户空间线程 |
孤儿进程 | 父进程已退出、子进程仍存活;最终会被 systemd 领养 |
观察工具 | ps -ef , pstree , pstree -p |
编程实践建议 | 合理使用 wait/waitpid ,避免产生孤儿进程 |