【Linux】进程的概念和状态
个人主页:@我要成为c嘎嘎大王
希望这篇文章可以让你有所收获!
一、进程
1.1 进程的概念
在给进程下定义之前,我们先了解一下进程:
我们在编写完代码并运行起来时,在我们的磁盘中会形成一个可执行文件,当我们双击这个可执行文件时(程序时),这个程序会加载到内存中,而这个时候我们不能把它叫做程序了,应该叫做进程。
所以说,只要把程序(运行起来)加载到内存中,就称之为进程。
进程的概念:程序的一个执行实例,正在执行的程序等
1.2 描述进程-PCB
PCB:进程控制块(结构体)
当一个程序加载到内存中,操作系统要为刚刚加载到内存的程序创建一个结构体(PCB),进程信息被放在这个结构体中(PCB),可以理解为PCB是进程的属性的集合。
- 在Linux操作系统下的PCB是:task_struct
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
1.3 task_struct
- 标示符:描述本进程的唯一标示符,用来区别其他进程
- 状态:任务状态
- 优先级:相对于其他进程的优先级
- 程序计数器:程序中即将被执行的下一条指令的地址
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块指针
- 上下文数据:进程执行时处理器的寄存器中的数据
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
- 其他信息:…
组织进程可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
为什么会存在PCB?
操作系统要进行软硬件的资源管理,要管理资源就要对资源先进行描述后组织,要描述就必须有PCB结构体。
如何理解创建进程?
把一个程序加载到内存中变成进程,操作系统会新增一个PCB,连接到双向链表中。
如何理解删除进程?
在双向链表中删除对应的PCB。
二、查看进程
1、 进程的信息可以通过 ls /proc 系统文件夹查看
2、大多数进程信息同样可以使用ps这些用户级工具来获取
ps aux | head -1 && ps aux | grep XXX
#include<stdio.h>
#include<unistd.h>
int main()
{while(1){sleep(1);}return 0;
}
三、通过系统调用获取进程标示符
3.1 getpid、getppid
- 进程id(PID)
- 父进程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main(){printf("pid: %d\n", getpid());printf("ppid: %d\n", getppid());return 0;}
3.2 fork初识
对于fork系统接口以下也将从这四个方面进行介绍。
- 头文件:<unistd.h>
- 作用:就是创建一个新的进程,新的进程被称为子进程,原来的进程被称为父进程。
- 返回值类型pid_t,就是有符号整数,与int相同;如果子进程成功创建,在父进程中fork返回子进程的PID,在子进程中fork返回0;如果子进程没有创建成功就返回-1;
- 没有参数。
3.2.1 fork调用结果演示
注意上面代码中的i的变换,在 ret > 0中 i 不做变化,在 ret == 0 中 i++。
- 根据上面的演示可以直观的看到程序同时进入和if和else if;
- 但是在根据后面打印的 i 会发现,在 ret > 0 的情况下打印的 i 每次都为0,ret == 0 的情况下打印的i每次加1,这两个 i 好像互相之间不影响,好像用的就是两个 i 一样。
对于这两种奇怪的现象在下面详细剖析fork你将会得到答案。
3.2.2 fork剖析
以下将围绕三个问题展开解释fork的工作原理。
fork是干什么的?
fork就是用来创建一个新的进程,完成与当前进程不同的功能;如果进程创建成功,新的进程被称为子进程,而原来的进程被称为父进程。子进程有自己的PID(如上图所示),也有自己的PPID,其PPID就是原来进程的PID,因为是父进程创建了这个进程。
子进程是突然创建的,那它怎么运行,它有没有代码和数据???
答:是的,子进程是由父进程创建的,它没有自己的代码和数据,所以他要与父进程共有代码;
代码是公用的,那数据是共用的吗???
答:代码是共用的可以理解,代码在运行期间不会被修改吗,但是数据就不一样了,数据是有可能被修改的,比如上图中,根据上面的 i 也能发现子进程中打印的 i 数据和父进程中的好像是独立的,这是不是意味着父进程将自己的数据拷贝一份给进程了???看看man手册中是这么说的。
fork使用的是写实拷贝:当子进程不对数据进行修改的时候就与父进程共用一个数据,当子进程需要对数据进行修改的时候再专门拷贝一份来供子进程进行修改。
通过写实拷贝可以让空间利用效率更高,对于不进行修改的数据不拷贝使得空间不会被大批量浪费。
fork为什么要对子进程和父进程分别返回?
答:返回值不同是为了区分父进程和子进程,让不同的进程流去执行不同的代码;父进程中返回值>0,其实际上返回的是子进程的PID,这样就能让父进程找到子进程的属性信息。
以下是fork系统接口的伪代码实现。
在fork系统调用接口返回之前就已经创建好了子进程,所以return ret是两个进程共享的,都会执行,所以两个进程都有返回值,返回值不同。
一个变量是如何存储不同内容的?
如果使用一个变量进行接受fork的返回值;
pid_t ret = fork();
这就会使得id中存储不同的数据,这是怎么做到的???
根据前面所说的子进程对父进程的数据会进行写时拷贝。父进程的返回值会直接写入 ret 变量中,而子进程也有返回值,所以子进程也要写入 ret 变量中,此时操作系统就会对 ret 进行拷贝,再生成一份让子进程进行修改。实际上还是两个不同的变量在存储,而不是同一个ret变量存储了两个不同的数据。
四、进程的状态
操作系统存在着五种状态模型:
- 新建态:刚刚创建的进程,操作系统还没有把它加入可执行进程组中。
- 就绪态:进程已经做好准备,只有有机会就会开始执行。
- 运行态:该进程正在执行。
- 阻塞态:进程在某些事件发生前不能执行,如I/O操作完成。
- 退出态:操作系统从可执行进程组中释放出进程,或者自身停止,或者是因为某些原因被取消。
但,我们今天是来看Linux操作系统中具体的进程状态。
下面的状态在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 */};
4.1 R 可执行状态(runing)
注意:此可执行状态R并非上面的运行态。
进程中的R状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里,即可被调度。操作系统会把进程中R状态的进程全放在调度队列中,方便调度。
创建了一个可执行态进程:
#include<stdio.h>
#include<unistd.h>
int main()
{while(1);return 0;
}
4.2 S 睡眠状态(sleeping)
意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
用代码创建一个睡眠状态的进程:
#include<stdio.h>
#include<unistd.h>
int main()
{while(1)sleep(10);return 0;
}
S状态是浅度睡眠,随时可以被唤醒,也可以被杀掉。
举个例子:
——小明和小华一起去吃饭,而小华对小明说:“小明,你先去楼下等我,我弄会东西,马上就好”。小明下完楼后就开始等小华——着等待的过程就是睡眠状态。
4.3 D 磁盘休眠状态(Disk sleeping)
可以表示为深度睡眠,该进程不会被杀掉,即使你是操作系统。除非我自动唤醒,才可以恢复。
举个例子:
——在系统有一个进程叫“小张”,磁盘中有一个负责写入数据的东西叫“小陈”。
“小张”要把数据存放到磁盘中,拜托“小陈”来存,由于磁盘中的东西较多,“小陈”尝试进行存储,让“小张”等他,如果存储失败,也会告诉“小张”。而在这个时间段,系统中的正在执行的进程越来越多,最后操作系统看见“小张”“占着茅坑不拉屎”,就把“小张”给踢出去了,之后”小陈“存放数据失败了,找”小张“问这数据是删掉还是再存放一次,然而”小张“已经被操作系统干掉了,”小陈“得不到回响,不知道怎么办,这份数据就丢失了。为了防止这个情况的发生,操作系统就搞了个D状态。
这种状态(D)的进程杀不死。
4.4 T 停止状态(stopped)
向进程发送SIGSTOP信号,该进程会响应该信号进入暂停状态,
向该进程发送SIGCONT信号,该进程会从暂停状态恢复到可执行状态。
4.5 X 死亡状态(dead) & Z 僵尸状态(zombie)
X 死亡状态:这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
Z 僵尸状态:一个处于僵尸状态的进程,会等待它的父进程或操作系统对它的信息进行读取,之后才会被释放。
举个例子:
——一个人突然死亡,普通人不会对现场进行清理,而是报警等警察和法医对该人进行信息的采集,之后才清理现场。其中,某人充当的角色是进程、警察和法医充当的角色的父进程或者操作系统。
五、僵尸进程
当一个进程变为僵尸状态的时候,该进程就变成了僵尸进程。
- 僵尸状态(zombie)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程
- 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
5.1 通过代码来模拟僵尸状态的进程:
#include<stdio.h>
#include<unistd.h>
int main(){pid_t ret = fork();if(ret == 0){// 子进程int time = 5;while(time--){printf("我是一个子进程,我的PID->%d, 我的PPID->%d\n", getpid(), getppid());sleep(1);}}else if(ret > 0){//父进程while(1){printf("我是一个父进程,我的PID->%d, 我的PPID->%d\n", getpid(), getppid()); sleep(1);}}else{printf("创建失败\n");return 1;}return 0;
}
用while :; do ps aux | head -1 && ps aux | grep a.out ; echo "#######################"; sleep 1 ; done来监控进程的状态。
5.2 僵尸进程的危害
父进程不读取,子进程会一直处于Z状态,而且维护僵尸进程本身也是数据维护,也属于进程的基本信息,所以保存在PCB中,所以说僵尸进程一直不退出,PCB一直维护,就会导致内存泄漏问题。
如何避免僵尸进程??
可以用wait方法和waitpid方法避免,后面文章中讲。
六、孤儿进程
在Linux中,进程的关系主要是父子关系。
一对父子进程中的父进程退出了,子进程还在运行,就会形成孤儿进程。
如果没有进程来回收该子进程的信息,那么会变成僵尸状态,会存在内存泄漏的问题。为了解决这个问题,该子进程会立即被 1 号init进程领养。
6.1 通过代码来模拟孤儿进程
通过查看该子进程的信息,可以得知该进程被1号init进程领养。
完
希望这篇小小文章可以为你解答疑惑!
若上述文章有什么错误,欢迎各位大佬及时指出,我们共同进步!