1 进程和PCB控制块
1.1 进程简单描述
进程可以理解为程序的一次执行,也可以理解为程序运行的一个实例。进程是分配资源的基本单位。一个进程由三部分组成:进程控制块PCB(Process Control Block),有关程序段,该程序段对其操作的数据结构集。
每个用户均可同时运行多个程序。为了区分每一个运行的程序,Linux给每个进程都做了标识,称为进程号PID(process ID),每个进程的进程号是唯一的。
Linux 给每个进程都打上了运行者的标志,用户可以控制自己的进程:给自己的进程分配不同的优先级,也可以随时终止自己的进程。其实就是进程继承了执行者的UID和GID。
Linux 不可能在一个 CPU 上同时处理多个任务(作业)请求,而是采用 “分时” 技术来处理这些任务请求。
1.2 ps和kill命令
常使用ps
命令来查看当前运行的进程
1 | 使用ps ajx 可以查看其父进程 |
STAT
字段表示进程状态,在下一节介绍。详细命令参数可通过man
手册查看。
可以使用kill
命令通过进程PID号来终止进程
1 | kill 9527 -9 |
其中-9
是终止的信号参数。
1.2 进程的状态
一个进程至少具有5种基本状态:初始态、执行状态、等待(阻塞)状态、就绪状态、终止状态。
- 初始状态:进程刚被创建,由于其他进程正占有CPU所以得不到执行,只能处于初始状态。
- 就绪状态:只有处于就绪状态的经过调度才能到执行状态。
- 运行状态:任意时刻处于执行状态的进程只能有一个。
- 等待状态:进程等待某件事件完成
- 终止状态:进程结束
有的系统,为了暂时缓和内存的紧张状态,或为了调节系统负荷,又引入了挂起的功能:暂时挂起一部分进程,把他们从内存临时换出到外存。
阻塞挂起:进程在外存并等待某事件的出现
就绪挂起:进程在外存,但只要进入内存,就可以运行
事实上不同系统不同翻译中对进程状态的描述略有差别。其中STAT
字段即表示进程状态,通过man ps
可以查看相关含义
1 | D uninterruptible sleep (usually IO) |
对应解释为
- R—TASK_RUNNING(可执行状态)
- S—TASK_INTERRUPTIBLE(可中断的睡眠状态)
- D—TASK_UNINTERRUPTIBLE(不可中断的睡眠状态)
- T — TASK_STOPPED或TASK_TRACED(暂停状态或跟踪状态)
- Z — TASK_DEAD - EXIT_ZOMBIE(退出状态,进程成为僵尸进程)
- X — TASK_DEAD - EXIT_DEAD(退出状态,进程即将被销毁)
1.3 PCB进程控制块
在Linux中进程信息存放在叫做进程控制块的数据结构中,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息。Linux内核的进程控制块是task_struct结构体。这在不同系统中的实现略有差别。
task_struct是Linux内核的一种数据结构,它会被装载到RAM并且包含着进程的信息。每个进程都把它的信息放在task_struct这个数据结构体中,其中包含了众多信息。
主要信息有
进程id
进程状态status
文件描述符表
进程工作目录位置
umask掩码
信号相关信息资源
用户id和组id
详细内容见操作系统相关书籍,留待后续补充。
2 fork()和父子进程
2.1 fork()
可以在程序中通过fork()
来创建进程,此时创建的进程即是该程序进程的子进程。
1 |
|
这个函数非常奇妙,在正常执行的情况下,将会有两个返回值。所谓的两个返回值其实就是在父子进程中各返回一次,在父进程中返回的是子进程的PID,而在子进程中返回的是0,因此可以通过检查返回值,来确定后续代码在父进程还是子进程中执行。因为进程号都是大于0的,所以执行出错时返回-1。
2.2 getpid()\getppid()
这两个函数分别可以获取当前进程的PID和当前进程父进程的PID。
1 |
|
该函数不会执行出错,无需判断返回值。
2.3 父子进程关系
在刚刚完成fork()
后,父子进程间的data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式都完全一样。
而进程PID、fork()
返回值、各自父进程、进程创建时间、闹钟、未决信号集则不一样。
而其相同的全局变量也并未完全是简单地拷贝,而是遵循“读时共享、写时复制”的原则。如文件描述符和mmap映射区。
2.4 循环创建n个子进程
这里要求循环创建n个子进程时,并且保证所有子进程均由最初父进程产生,也就是说所有子进程应该是兄弟关系。并可以控制特定子进程完成特定操作。
1 |
|
编译执行,看看效果
1 | ./fork_loop |
3 wait()和waitpid()
3.1 函数介绍
先介绍一下孤儿进程和僵尸进程。
- 孤儿进程:父进程先于子进终止,子进程沦为“孤儿进程”,会被 init 进程领养。
- 僵尸进程:子进程终止,父进程尚未对子进程进行回收,在此期间,子进程为“僵尸进程”。kill 对其无效。子进程的PCB资源会残留在内核中,浪费内存资源。
所以在子进程终止之后,父进程应当承担回收子进程的责任,否则就可能产生故而进程和僵尸进程。
可以在父进程中使用wait()
或waitpid()
来对子进程进行回收。
1 |
|
wait()
函数将阻塞地回收任意一个子进程;执行成功时将返回回收进程的PID,失败返回-1;通过传出参数wstatus
可以获得子进程终止状态。
- 获取子进程正常终止值:
WIFEXITED(status)
为真的话,可调用WEXITSTATUS(status)
获得子进程的退出值。 - 获取导致子进程异常终止信号:
WIFSIGNALED(status)
为真的话,可调用WTERMSIG(status)
得到导致子进程异常终止的信号编号。
waitpid()
则可以指定特定子进程进行回收,同时也可以设置非阻塞。返回值大于0时表示成功回收子进程,返回值就是对应PID;返回值为0时,表示设置了非阻塞且子进程未结束未能回收;返回值为-1时表程序执行出错。
注意无论这两个函数一次都只能回收一个子进程,需要回收多个子进程需要使用循环。
3.2 使用实例
在2.4节的程序中,子进程先于父进程终止,且父进程未对其回收,所以会产生僵尸进程。这里对其进行改进
1 |
|
编译后执行,得到结果
1 | ./wait |
显然,所有子进程都已经被回收。
4 结束语
本篇文章先简单介绍了进程和PCB控制块的概念,因为这其实应该是操作系统的相关概念,所以在此不多做纠缠,只是有个简单印象,同时记录了相关常用命令ps
和kill
;然后介绍了用来创建子进程的fork()
函数,已经实现了一个循环创建特定个数子进程的小demo;最后介绍了用以回收子进程的wait()
和waitpid()
函数,并对之前的小demo做了改进,使其不再产生僵尸进程。
fork()
函数非常重要,在后续文章其他函数的学习中,还将多次使用其实现多进程程序的编写。