目录
一、进程是什么
1.1 什么是进程
1.2 描述进程:PCB进程控制块
1.3 task_struct中存储什么
二、 组织进程
2.1进程管理
2.2 进程查看
三、fork创建进程
3.1 fork的作用
3.2 fork的原理
四、Linux内核中的进程状态
4.1 运行态
4.2 阻塞态
4.3 挂起态
4.4 Linux内核中的进程状态
4.5 前后台
本文系统阐述了进程的概念与管理机制:进程是运行的程序,由内核通过task_struct(PCB)描述和管理,包含PID、状态、上下文、资源指针等信息。操作系统用双向链表组织进程,通过/proc文件系统或ps命令查看进程。fork()创建子进程,采用写时拷贝(COW)优化内存使用,父子进程共享代码但独立数据。进程状态包括运行(R)、可中断睡眠(S)、不可中断睡眠(D)、停止(T)、僵尸(Z)等,其中僵尸进程需父进程回收。内核使用list_head链表结构高效管理进程,结合container_of宏实现链表节点
一、进程是什么
1.1 什么是进程
课本概念:运行起来的程序叫做进程
内核观点:担当分配系统资源(CPU时间,内存)的实体
当前:进程=内核数据结构(struct task_struct)+自己的程序代码和数据
操作系统内部可以同时存在多个进程,那么OS对进程必须要做管理
1.2 描述进程:PCB进程控制块
既然OS要管理进程,首先要认识进程,在OS管理进程时,也需要一个信息记录表,这就是PCB(Process Control Block),在Linux系统中,PCB的具体实现就是task_struct 结构体,每个进程都有且仅有一个task_struct,它包含了进程的所有属性,OS通过操作task_struct来管理进程,而非直接操作进程本身。
task_struct就是 Linux 内核里的 PCB(进程控制块),内核用来描述、管理一个任务(进程 / 线程)的结构体,存在内核空间
1.3 task_struct中存储什么
关于 task_struct ,我们需要掌握以下信息:
- pid:进程 ID,身份标识
- state:进程状态(运行、可中断睡眠、不可中断睡眠、僵尸等)
- 上下文数据:进程执行时处理器的寄存器中的数据,切换时保存现场
- 调度信息:优先级、时间片
- 资源指针:内存(mm_struct)、文件描述符表(files_struct)
- 亲属关系:父进程、子进程、兄弟进程指针
- 程序计数器:程序中即将被执行的下一条指令的地址
- IO状态信息:包括显示的IO请求,分配给进程的IO设备和被进程使用的文件列表
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记帐号等
二、 组织进程
2.1进程管理
我们在上文中有提到,OS是通过该结构体,用一些数据结构来进行进程的管理,那么在linux中,所有 task_struct 用双向链表串起来,内核遍历链表管理全部进程
2.2 进程查看
上文中提到进程的标识是pid,如果要查pid的话需要引入头文件
#include<sys/types.h>PPID = Parent Process ID,父进程 IDPID 是自己的进程号;PPID 就是是谁把这个进程启动起来的,那个父进程的 PID。
完整的示例代码如下:
#include <stdio.h> #include<unistd.h> #include<sys/types.h> int main() { while(1) { printf("我是一个进程: pid: %d, ppid: %d\n", getpid(), getppid()); //ppid的第一个p的意思是parent sleep(1); } return 0; }即可将进程的pid打印出:
我是一个进程: pid: 588, ppid: 30174但是如果使用CTRL C中止进程后,在此启动,进程会变化
那么进程怎么查看呢?
/proc 是内核虚拟文件系统,用来查看内核和各个进程的运行信息
在proc文件夹下有许多数字目录,这些数字目录就是进程的pid,每个数字文件夹对应一个正在运行进程的信息,里面存这个进程的task_struct相关信息。
cwd:进程当前工作目录
另一种查看进程的方法是:
ps命令:查看一瞬间所有当前终端的进程信息,包括系统本身
[root@VM-0-4-centos ~]# ps PID TTY TIME CMD 23975 pts/0 00:00:00 ps 30174 pts/0 00:00:00 bash [root@VM-0-4-centos ~]#ps axj :查看进程组 PGID、会话 SID、父子进程,专门用来学进程组、会话、作业控制
a:所有带终端的进程x:包含无终端的后台进程(守护进程)j:job 格式,作业控制,多出 PGID、SID 字段
ps axj | head -1:只打印 ps axj 的表头一行,先看懂字段含义,再过滤进程
[root@VM-0-4-centos ~]# ps axj PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 0 1 1 1 ? -1 Ss 0 1:51 /usr/lib/systemd/systemd --switched-root - 0 2 0 0 ? -1 S 0 0:00 [kthreadd] 2 4 0 0 ? -1 S< 0 0:00 [kworker/0:0H] 2 6 0 0 ? -1 S 0 0:08 [ksoftirqd/0] 2 7 0 0 ? -1 S 0 0:07 [migration/0] 2 8 0 0 ? -1 S 0 0:00 [rcu_bh] 2 9 0 0 ? -1 S 0 4:05 [rcu_sched] 2 10 0 0 ? -1 S< 0 0:00 [lru-add-drain] 2 11 0 0 ? -1 S 0 0:05 [watchdog/0] 2 12 0 0 ? -1 S 0 0:05 [watchdog/1] 2 13 0 0 ? -1 S 0 0:07 [migration/1]三、fork创建进程
在上文中所提到的 getppid 是查看父进程,那么当命令行直接启动时,创建的对应的进程,父进程都是bash ! Bash 是 Linux 默认的 Shell(命令解释器),命令行解释器本身也是一个进程
[root@VM-0-4-centos ~]# ps ajx | grep bash 30145 30174 30174 30174 pts/0 32200 Ss 0 0:00 -bash #-bash表示远程登陆的bash 30174 32201 32200 30174 pts/0 32200 S+ 0 0:00 grep --color=auto bash3.1 fork的作用
fork()的作用是复制父进程,生成一个子进程。子进程会继承父进程的代码,数据,文件描述符等资源,但拥有独立的PID和 task _struct,子进程的创建以父进程为模板,父子代码段一样,从fork之后的位置继续往下执行
#include <stdio.h> #include<unistd.h> #include<sys/types.h> int main() { printf("fork之前我是一个进程:pid: %d, ppid: %d\n", getpid(), getppid()); fork(); printf("fork之后我是一个进程:pid: %d, ppid: %d\n", getpid(), getppid()); return 0; }结果:
[root@VM-0-4-centos lesson]# ./mycmd fork之前我是一个进程: pid: 3182, ppid: 623 //fork()调用一次返回两次 fork之后我是一个进程: pid: 3182, ppid: 623 fork之后我是一个进程: pid: 3183, ppid: 3182返回两次:
- 父进程:返回子进程 pid(>0)
- 子进程:返回0
- 失败:父进程返回 - 1,没有子进程
3.2 fork的原理
当父进程调用 fork() 时,内核会进行如下步骤:
- 核心:复制进程内核以调用 fork 的父进程为模板,新建一个子进程
- 早期:完整复制父进程地址空间
- Linux 现在:写时拷贝 COW(不是立刻复制,父子先共享同一份页表;任意一方修改内存时,才单独拷贝那一页)
- task_struct:新建子进程 PCB(task_struct),分配新 pid
- 返回两次:
- 父进程:返回子进程 pid(>0)
- 子进程:返回0
- 失败:父进程返回 - 1,没有子进程
- 父子代码段一样,从 fork 之后的位置继续往下执行,不是从头重新跑 main
那么父子关于数据是怎么处理呢——进程是具有独立性的
写时拷贝的意义:避免创建进程时复制大量未修改的数据,节省内存,提高创建效率。
父子进程的执行顺序:不确定,由Linux调度器决定,父子进程会竞争CPU时间片
在这里我们看上述代码,C/C++ 中看到的都是虚拟地址(两个&flag为虚拟地址)。fork 之后父子进程虚拟地址一模一样;物理内存初始共享。当任意一方修改数据,操作系统才给修改的进程分配新物理页,实现分离,互不干扰。
fork 刚创建完、还没修改 flag 的时候: 父子的 flag 虚拟地址相同,物理地址是同一块,flag 的值也一样。 当父 / 子其中一个修改了 flag: 触发写时拷贝,OS 复制这一页,二者的 flag 不再是同一块物理地址,这时值才不一样。即:虚拟地址相同,物理地址已经分开了
四、Linux内核中的进程状态
状态:本质是task_struct内部的一个整型变量
进程状态:
运行:只要进程的pcb进入调度队列中,并且在CPU上执行,就处于运行状态
阻塞:进程在等资源,但是资源没有就绪,不在调度队列
挂起:PCB在调度队列,已经准备好了,等着 CPU
4.1 运行态
对上图的解释:
task_struct(PCB)里面嵌了一个小结构体list_head link
struct list_head { struct list_head *next; struct list_head *prev; }; struct task_struct { // 一大堆成员:pid、ppid、状态、优先级…… struct list_head link; // 就是图里红框这个! };next、prev 只指向别的list_head,不是直接指向 task_struct!链表串起来的,只是一个个link节点。遍历链表,拿到了link的地址,但是我想要整个task_struct,拿到 pid 等进程信息!已知结构体内部成员(link)的地址,反求整个宿主结构体(task_struct)的起始地址
两个关键宏
- offsetof:算出
link在 task_struct 里面,距离结构体开头偏移多少字节
#define offsetof(type, member) ((size_t)&(((type*)0)->member))- container_of(内核灵魂宏):
link地址 - link的偏移量 = task_struct首地址
#define container_of(ptr, type, member) ({ const typeof( ((type *)0)->member ) *__mptr = (ptr); (type *)( (char *)__mptr - offsetof(type,member) );})- ptr:拿到的 link 指针
- type:宿主类型
struct task_struct - member:结构体里面的成员名
link
普通链表:节点里面存数据;Linux 内核链表:把链表头(list_head)塞到数据结构里面!好处:同一个 task_struct,可以同时放进多条不同链表(就绪队列、等待队列),只需要嵌多个 list_head。
内核为什么要这么实现链表?
4.2 阻塞态
阻塞态:进程等待资源,资源未就绪,进程不被调度
4.3 挂起态
交换(swap、进程挂起)
内存不够的时候,OS 把进程代码、数据从内存换到磁盘 swap 分区(换出);等内存空闲了,再读回内存(换入)
- 本质:用时间换空间(磁盘远慢于内存,频繁 swap 会很卡)
- 区分两种挂起:
- 阻塞挂起:进程本身在等资源(阻塞),同时内存不够,被换到 swap
- 运行 / 就绪挂起:进程本来就绪 / 可运行,只是内存紧张,被换到 swap
⚠️注意:PCB(task_struct)一直留在内存里,不会被换出去!只有进程的代码数据被换去 swap
swap 分区常规大小:一般设为内存的 1/2 或者等于内存大小;swap in/out 变多 = 系统卡顿
4.4 Linux内核中的进程状态
Linux内核中定义了进程状态的宏,共7种状态,下⾯的状态在kernel源代码⾥定义
/*--------------------------------------------------- *The task state array is a strange "bitmap" of *reasons to sleep. Thus "running" is zero, and *you can test for combinations of others with *simple bit tests. -----------------------------------------------------*/ static const char *const task_state_array[] = { "R (running)", /* 0:运行态 */ "S (sleeping)", /* 1:可中断睡眠态 */ "D (disk sleep)", /* 2:不可中断睡眠态 */ "T (stopped)", /* 4:停止态 */ "t (tracing stop)", /* 8:追踪停止态 */ "X (dead)", /* 16:死亡态 */ "Z (zombie)", /* 32:僵尸态 */ };| 标记 | 名称 | 说明 |
|---|---|---|
| R | running | 意味着进程要么在运行种要么在队列里 |
| S | sleeping | 可中断休眠,意味着进程在等待事件完成(阻塞,可以被信号唤醒,比如 scanf 输入) |
| D | disk sleep | 不可中断休眠(等待 IO 硬件,不能被信号打断,一般等磁盘) |
| T | stopped | 暂停(收到 SIGSTOP 信号,gdb 调试断点就是这个) |
| t | tracing stop | 被追踪暂停(gdb 跟踪) |
| X | dead | 进程彻底释放,马上消失,几乎看不到 |
| Z | zombie | 僵尸进程:进程退出了,PCB 还在,等父进程回收退出信息 |
4.5 前后台
无论前台还是后台都能够向前台打印消息,在任何时刻,在Linux命令行操作时,有且仅有一个进程是前台进程,能够从键盘读取数据,拥有键盘文件的进程,就是前台进程;后台的作用是提高效率
前台进程(foreground)
- 占用终端,能读取键盘输入、输出到屏幕
- 可以直接用
Ctrl+C终止、Ctrl+Z暂停 - 前台跑起来之后,shell 卡住,不能敲新命令
后台进程(background)
- 不占用终端输入,shell 提示符立刻回来,可以继续敲别的命令
- 默认无法读取键盘,输出默认还会打印到屏幕
Ctrl+C对后台进程无效
这种Ss后面带加号的就是处于后台
以上就是本期全部内容