马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
进程是操作系统里最基础的概念,也是"程序跑起来之后"的那个东西。这篇把进程是什么、Linux 用什么数据结构描述进程、task_struct 里到底装了什么、PID 和 PPID 怎么看进程父子关系、以及 fork 之后父子数据为什么互不干扰,一次讲透,并且每段都配上能直接敲的命令和可编译的代码。
一、什么是进程
进程是正在运行的程序的实例,它不只是代码,还包括代码执行时所需的资源和状态信息。简单记:
进程 = 程序的代码和数据 + 内核数据结构(内核数据结构用来管理进程的资源与状态)
二、描述进程:PCB 与 task_struct
1. PCB 是什么
PCB(Process Control Block)是操作系统中用于管理每个进程的重要数据结构,包含管理该进程所需的全部信息,用来跟踪、控制和调度进程。每个进程对应唯一的 PCB,系统靠 PCB 识别和管理进程状态与资源。
程序运行时要先被加载进内存,然后由 CPU 读取执行。如果只加载代码和数据,那进程的状态、标识符、优先级这些信息怎么管?于是把这些代表性的信息合起来,建出一个 task_struct 来管理进程。
2. task_struct 里有什么
struct task_struct {
struct list_head tasks; /* 同一进程组中所有任务的列表头 */
pid_t pid; /* 进程 ID */
int state; /* 进程状态 */
unsigned int flags; /* 进程标志 */
struct task_struct *parent; /* 父进程 PCB 指针 */
struct list_head children; /* 子进程链表 */
struct list_head sibling; /* 兄弟进程链表 */
struct thread_info *thread_info;
struct fs_struct *fs; /* 文件系统结构体 */
struct files_struct *files; /* 打开文件结构体 */
struct signal_struct *signal; /* 进程信号管理结构体 */
struct sighand_struct *sighand; /* 信号处理函数集合 */
u64 start_time; /* 进程开始时间 */
struct taskstats *stats; /* 与进程相关的统计信息 */
};
struct task_struct 里有一个指针指向下一个进程,还有一个指针指向自己对应的进程——其实就是一个双向链表结构,靠 children 和 sibling 串起整个进程树。
3. task_struct 内容九分类
- 标识符:描述本进程的唯一标识,用来区别其他进程;
- 状态:任务状态、退出代码、退出信号等;
- 优先级:相对于其他进程的优先级;
- 程序计数器:程序中即将被执行的下一条指令的位置;
- 内存指针:程序代码和相关数据的指针,以及与其他进程共享的内存块的指针;
- 上下文数据:进程执行时处理器寄存器中的数据;
- I/O 状态信息:待处理的 I/O 请求、分配给进程的 I/O 设备和被进程使用的文件列表;
- 记账信息:处理器时间总和、使用时钟数总和、时间限制、记账号等;
- 其他信息。
三、标识符:看 PID 和 PPID
1. 找出指定进程
ps ajx | grep myexe
ps ajx 看全部进程,加管道 grep 就看指定进程。全量输出第一排有表头,自己的没有,那就把表头加回来:
ps ajx | head -1 && ps ajx | grep myexe
表头里的 pid 就是标识符。pid 具有唯一性,能唯一定位一个进程。
2. 父子关系
用 pid 查指定进程时会多出一个 bash 进程——myexe 的 ppid 和 bash 的 pid 相同,说明 bash 是父、myexe 是子。bash 是命令解释器,启动时它 fork 出一个子进程去执行我们的命令。这就是 shell 里每条命令都是子进程的根本原因。
ppid 就是父进程 id(parent pid)。
3. 怎么结束进程
- 第一种:Ctrl + C;
- 第二种:
kill + 进程标识符。
四、手动创建子进程:fork
fork 函数可以手动创建一个子进程。返回值规则:给父进程返回子进程的 pid,给子进程返回 0,创建失败返回 -1。
int main()
{
pid_t id = fork();
while (1) {
if (id > 0) {
printf("我是父进程,pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
} else if (id == 0) {
printf("我是子进程,pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
父子都打印了,可代码是 if 和 else if,为什么会同时成立?因为创建的子进程与父进程共享同一份代码,但数据是私有的。
1. 验证数据私有
int gval;
int main()
{
pid_t id = fork();
while (1) {
if (id > 0) {
printf("我是父进程,pid:%d, ppid:%d, id:%d, gval:%d\n", getpid(), getppid(), id, gval);
sleep(1);
} else if (id == 0) {
printf("我是子进程,pid:%d, ppid:%d, id:%d, gval:%d\n", getpid(), getppid(), id, gval);
gval++;
sleep(1);
}
}
return 0;
}
全局变量 gval,子进程负责 ++ 和读取,父进程只读取。运行一下会发现只有子进程的 gval 在涨,父进程不动——两个进程数据私有,进程之间具有高度独立性。
2. 多进程创建
按上面同样的套路循环 fork,就能一次拉起一批子进程。注意别写成无上限的递归,否则进程数会瞬间爆掉。
五、一句话记住
进程 = 内核数据结构(task_struct)+ 自己的代码和数据。PCB 是描述它的容器,PID 是它的身份,fork 是造它的手段,父子各自一份数据。这四句话连起来,后面学进程等待、进程替换、线程,就有了落脚点。
qidao123.com企服评测-应用市场 编辑提示
本文按进程的组成公式(代码数据 + 内核数据结构)、PCB 与 task_struct 的定义与字段作用、struct task_struct 关键成员清单、task_struct 内容的九类划分(标识符/状态/优先级/程序计数器/内存指针/上下文数据/I-O 状态/记账信息/其他)、ps ajx 与 head -1 与 grep 组合查 PID 与 PPID 的父子关系、Ctrl+C 与 kill 两种结束方式、fork 返回值规则与父子共享代码但数据私有的验证方法(含全局变量 gval 实验代码),重排成一条"概念—描述—查看—创建"的学习路径。文中代码与结构体片段为教学示意,函数名与字段含义取自 Linux 内核公开源码与 POSIX 标准;进程创建与终止操作请在自有编译与测试环境中进行,避免失控 fork 耗尽系统资源。 |