个人主页 : zxctscl
如有转载请先通知
文章目录
- 1. 前言
- 2. Linux的进程状态
- 2.1 S状态
- 2.2 R状态
- 2.3 T/t状态
- 2.4 D状态
- 3. 僵尸进程和孤儿进程
- 3.1 僵尸进程
- 3.2 孤儿进程
- 4. 进程的阻塞、挂起和运行
- 4.1 运行
- 4.2 阻塞状态
- 4.3 挂起
- 4.4 进程切换
1. 前言
上一篇博客中提到 【Linux】进程初步理解,这次继续来分享与进程有关的知识。
2. Linux的进程状态
Linux的进程状态就是struct task_struct内部的一个属性。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
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):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
2.1 S状态
检测一下这些状态:
先写一个测试代码:
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4
5 int main()
6 {
7
8 while(1)
9 {
10 printf("I am a process,pid:%d\n",getpid());
11 }
12 return 0;
13 }
运行代码:发现进程都是出于s状态:
printf在显示器上打的时候,根据冯诺依曼特体系结构,显示器是一个外设,所以CPU在跑当前程序时,要把数据写到内存中,然后刷新到外设上,但是不能保证,每一次打印的时候显示器都是就绪的。相比较CPU来讲,大部分时间这个进程都在等待设备资源是否就绪,如果资源不就绪,当前进程就一直出于S状态。
把代码在printf之前先休眠10秒:
此时发现进程一直出于S状态,可以直接ctrl+c把处于S状态的进程终止掉:
把S的这种状态叫做可中断睡眠,就是处于睡眠状态,依旧可以被外部信息随时打断。
2.2 R状态
那么把代码里面的printf给注释了:
此时进程都是R状态:
2.3 T/t状态
在kill命令中的19号命令,让进程暂停:
直接使用:
kill -19 pid
此时进程就处于T状态
要想让暂停的进程继续运行起来就用18号信号
此时进程又重新运行起来:
但是此时是在后台运行的,要想终止进程,只能使用kill -9。
暂停在之间调试的时候就已经用到了。
打开Makefile加-g选项
1 testStatus:testStatus.c
2 gcc -o $@ $^ -g
3 .PHONY:clean
4 clean:
5 rm -f testStatus
在10行打一个断点,然后查看一下进程的运行状态:
遇到断点,进程就暂停下来。
2.4 D状态
D状态是Linux系统比较特有的状态。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
操作系统杀进程的时候时毫无类别的去杀,在写入关键数据的进程是不能被杀掉的,所以操作系统规定,凡是进程在就行数据IO,在等待外设,像磁盘资源时,把状态设为D状态。
D状态不可被杀,深度睡眠,不可中断睡眠,就是一种sleep状态。
消除D状态:1. 让进程自己醒来;2. 重启–断点
3. 僵尸进程和孤儿进程
3.1 僵尸进程
Linux中一个进程的退出,它会将自己的退出信息保留在自己的PCB中。如果不读取PCB中的进程退出消息,那么进程就一直不释放,一般会释放掉代码和数据,但PCB的内核数据结构是一直存在的,直到将来对进程进行等待;如果不等待,那么进程就一直出于僵尸状态。如果读取了这个进程的退出信息或者等待了,那么这个进程才会变成X,进而将进程的信息全部释放。
为了测试重新写一个父子进程代码:
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4
5 int main()
6 {
7
8 pid_t id=fork();
9 if(id==0)
10 {
11 //child
12 int cnt=5;
13 while(cnt)
14 {
15 printf("I am a child,cnt:%d,pid:%d\n",cnt,getpid());
16 sleep(1);
17 cnt--;
18 }
19
20 }
21 else{
22 //parent
23 while(1)
24 { printf("I am parent,running always,pid:%d\n",getpid());
25 sleep(1);
26 }
27 }
28 return 0;
29 }
来看一下这个状态:
Z状态:已经运行完毕,但是需要维护自己的退出信息,在自己的进程task_struct会记录自己的退出信息,未来让父进程读取。
如果没有父进程读取,僵尸进程就会一直存在。
如果对僵尸进程一直不回收,就会引起内存泄漏问题,操作系统会调用waitpid来进行进程状态的改变,变为X,再由操作系统进行释放。
一个进程已经出于僵尸了,就不能kill,无法杀掉已经死掉的进程。
3.2 孤儿进程
如果一个进程在运行的时候,它的父进程先退出了,那么这个进程就是孤儿进程。
来代码看看:
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4
5 int main()
6 {
7
8 pid_t id=fork();
9 if(id==0)
10 {
11 //child
12 int cnt=5;
13 while(cnt)
14 {
15 printf("I am a child,cnt:%d,pid:%d\n",cnt,getpid());
16 sleep(1);
17
18 }
19
20 }
21 else{
22 //parent
23 int cnt=5;
24 while(cnt)
25 { printf("I am parent,running always,pid:%d\n",getpid());
26 sleep(1);
27 cnt--;
28 }
29 }
30 // while(1)
31 // {
32 // sleep(1);
33 // printf("I am a process,pid:%d\n",getpid());
34 // }
35 return 0;
36 }
来看看现象:
父进程如果先退出,子进程就会变成孤儿进程, 而孤儿进程一般会被1号进程(OS本身)进行领养。
为了保证孤儿进程正常被回收,孤儿进程会被操作系统领养。
可以直接kill掉孤儿进程:
在之前在Linux上写的代码,怎么出来没有关系过僵尸呢?或者内存泄漏?
因为直接在命令行中启动的进程,它的父进程是bash,bash会自动回收新进程的Z。
4. 进程的阻塞、挂起和运行
在网上找的一张进程状态图:
终止状态就等价于Z状态和X状态。
4.1 运行
进程运行一般在CPU上运行。
进程=task_struct+进程的代码和数据
每一个进程都有task_struct,为了对当前所有的进程进行管理,用链表将它们链接起来。而每一个CPU都会有一个运行队列struct runqueue,要运行进程,就得将进程放入运行队列struct runqueue中。从此CPU要运行已经进程,就在运行队列的头部取出一个进程,然后把相关的代码和数据拿到CPU寄存器中,进而就可以调度这个进程了。
一般而言一个进程被放到CPU上这个进程状态就是R,但是大部分教材中说的是进程在运行队列中,该进程的状态就是R状态,这里意思就是进程已经准备好了,可以随时被调度。
一个进程一旦持有CPU,会一直只运行这个进程吗?
不会,进程基于时间片进行轮转调度的。(而Linux中并不是以这种方法调度的,在之后的博客中会提到,请多多关注。)
让多个进程以切换的方式进程调度,在一个时间段内同时得以推进代码,就叫做并发。
把任何时刻,都有多个进程在真的同时运行,叫做并行。
4.2 阻塞状态
在C语言中用过一个scanf,如果不往里面输入数据,会一直处于什么状态?
来看看代码:
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4
5 int main()
6 {
7
8 int a=0;
9
10 scanf("%d",&a);
11
12 printf("a=%d\n",a);
阻塞状态就相当于S状态或者是D状态。
等待:等待键盘资源是否就绪,键盘上有没有被用户按下按键,按键数据交给进程:
操作系统是软硬件资源的管理者。
进程本身就是软件。
堆硬件的管理也是先描述在组织。
操作系统里面有一个结构体对设备就行管理,每一个结构体里面包含设备的状态,种类,还有指向其他设备的指针。
在等待说明进程并没有被调度,说明进程并不在运行队列中。
如果还有进程要等待键盘资源,就把对应的进程从运行队列放在等待队列里面。拿到对应的资源后,就回到运行队列里面,这个过程一般叫唤醒。
不是只有CPU才有运行队列,各种设备都有自己的等待队列。
阻塞和运行的状态变化,往往伴随进程PCB被连入到不同的队列中。
4.3 挂起
操作系统在运行进程的时候内存时比较吃紧的,一旦进程出于阻塞状态,那么就意味着当前进程不会被调度,这个进程的代码和数据就不会被访问,此时就会把这个进程的代码和数据唤出到磁盘上。那么曾经代码和数据占用的空间就空出来了,一旦获取到相应的资源,又会被唤起。
这个进程的PCB还在内存中,只是它的代码和数据在磁盘的swap分区,此时把这种状态叫做阻塞挂起。
这样操作系统就会更合理使用内存资源。
在用户层是感知不到的。
频繁的换入换出,会导致效率问题。这个是牺牲效率换取空间。
一般swap分区的大小不会太大,为了减少操作系统频繁的使用swap带来效率问题。
4.4 进程切换
CPU内部会有很多寄存器。
函数调用返回的临时变量,就用到了寄存器。
当一个进程被CPU调度的时候,CPU的寄存器中会保存当前进程的临时数据。如果这个进程的时间片到了,那么就会把这个进程从CPU上剥离下来,把下一个进程放上去。如果想要把这个进程恢复,就得保存上下文,然后CPU运行到这个进程又继续执行。
CPU内部的所有寄存器中的临时数据,叫做进程的上下文。
进程在切换,最重要的一件事就是:上下文数据的保护和恢复。
CPU内的寄存器:寄存器本身是硬件,具有数据的存储能力,CPU的寄存器硬件只有一套。
CPU内部的数据,可以有多套,有几个进程,就有几套和该进程对应的上下文数据。
所以寄存器!=寄存器内容
有问题请指出,大家一起进步!!!