1.进程状态是什么?
2. 运行、阻塞、挂起状态
进程在 CPU 上运行的的本质:CPU 在系统内部维护一个调度队列,调度队列就是一个个的 task_struct
。
- CPU 运行一个进程的本质:并不是运行这个进程的代码和数据,而是运行进程对应的 task_struct 对象。task_struct 中的属性含有代码和数据的地址,通过地址可以找到对应 task_struct 的代码和数据。每一个进程都有对应的代码和数据。
- 一个 CPU 只有一个调度/运行 队列(runqueue),类型:struct task_struct*。调度队列和全局双链表是两码事,task_struct 即属于调度队列,又属于全局双链表。
运行状态:进程在调度队列中,进程的状态就是运行状态。
阻塞状态:等待某种设备或者资源就绪,若不就绪,进程就不会被调度,就是阻塞状态(例如:当运行到 scanf 函数时,等待用户向键盘输入的本质:等待键盘硬件就绪,就是等待键盘按键按下。如果没有键盘按键按下,叫做键盘硬件不就绪,那么 scanf 无法读取数据)
- 操作系统要对硬件资源做管理:先描述,再组织。
3.Linux进程状态
进程状态:通过宏定义在 task_struct 中的一个整数。下面的状态在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 */
};
t(tracing stop):
4.查看进程状态
#每隔1秒查看进程myprocess
while : ; do ps axj | head -1; ps axj | grep myprocess; sleep 1; done
S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠)
T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停⽌(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运⾏。
两种阻塞状态
- S(sleeping):睡眠状态。可中断休眠,浅睡眠。
- D(disk sleep):磁盘休眠状态。不可中断休眠,深度睡眠。
5.僵尸进程
X(dead):死亡状态。进程死亡,释放 PCB、代码和数据。无法在任务列表中看到该状态。
Z(zombie):僵尸状态。在子进程退出之后,在父进程获取子进程退出信息之前,子进程处于僵尸状态。子进程会一直等待父进程获取退出信息。
为什么有僵尸状态?为了获取退出信息!
- 创建子进程的目的:让子进程完成某种事情!子进程退出(死亡)之后,最终完成的结果,父进程必须知道。
- 子进程退出后,可以将子进程的代码和数据释放(子进程不会被调度了),但是子进程的 PCB 不能释放(退出信息保存在 PCB 中,父进程通过系统调用获得 PCB 中的退出信息,知道子进程是由什么原因退出的)
- 在子进程退出之后,在父进程获取子进程退出信息之前,子进程处于僵尸状态。
模拟僵尸状态:利用 fork 系统调用创建父子进程,子进程退出后,父进程什么都不干(父进程没有获取子进程的退出信息),此时子进程一直是僵尸状态。
//myprocess.c源代码
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
int count = 3;
while (count)
{
printf("我是子进程,我正在运行:%d\n", count);
sleep(1);
count--;
}
}
else
{
while (1)
{
printf("我是父进程,我正在运行!\n");
sleep(1);
}
}
return 0;
}
- 如果父进程不管子进程(不获取子进程的退出信息),那么子进程或一直处于僵尸状态!
- 而处于僵尸状态时:子进程的 PCB 会一直存在,则 PCB 会一直占用内存。父进程不获取子进程的退出信息,就导致
内存泄漏
。- 父进程除了要获取子进程的退出信息,还要释放子进程的 PCB,防止内存泄漏。
注意:
- 进程退出后,内存泄漏就不存在了,操作系统会自动回收内存。
- 平时我们使用的软件,一但运行就是死循环进程,除非手动关闭软件。该进程称为 “常驻内存” 进程(例如:操作系统)。常驻内存进程若存在内存泄漏,问题很严重。
- 操作系统无法释放 PCB,因为子进程 PCB 中的退出信息要交给父进程,所以需要一直维护 PCB,不能直接释放。程序员需要解决这个问题。
关于操作系统内核结构申请的知识点:
- 当频繁的启动/退出进程时,为了减少申请内存的时间,操作系统拥有一套 slab 机制。当父进程获取子进程的退出信息之后,可以不用释放 PCB,而是将 PCB 存入到一个 PCB 列表中。当再次启动进程时,不用申请内存,只需使用列表中的 PCB 与代码和数据组成进程即可。
6.僵尸进程危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于僵尸状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,僵尸状态一直不退出,PCB就要一直都要维护。
- 如果一个父进程创建了很多子进程,就是不回收,则会造成内存资源的浪费。因为数据结构对象本身就要占用内存。
7.孤儿进程
思考:父进程如果提前退出,那么子进程后退出,进入僵尸状态的子进程之后,那该如何处理呢?
孤儿进程:在父子进程关系中,如果父进程先退出,子进程要被1号进程(操作系统)领养,被领养的子进程叫做 “孤儿进程”,孤儿进程由1号进程所回收。
模拟孤儿进程:父进程先退出,子进程继续执行。
//myprocess.c源代码
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
while (1)
{
printf("我是子进程。pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
else
{
int count = 3;
while (count)
{
printf("我是父进程。pid:%d,ppid:%d\n", getpid(), getppid());
count--;
sleep(1);
}
}
return 0;
}
问题:当父进程先退出时,为什么1号进程要领养子进程?
答案:如果不领养,当子进程退出后,没有父进程获取子进程的退出信息,子进程就一直处于僵尸状态,子进程的 PCB 已知占用内存导致内存泄露。领养之后,子进程就有父进程了,当子进程退出后,父进程可以获取子进程的退出信息,对子进程进行统一回收内存。这也是存在忽而进程的原因!
注意:
- 父进程的父进程是bash,父进程退出后,退出信息由bash回收。
- 孤儿进程时后台进程。无法使用Ctrl + c杀掉后台进程,后台进程或一直往前台打印消息。只能使用
kill -9 进程id
杀掉后台进程。