课本概念: 程序的一个执行实例,正在执行的程序等。
内核观点: 担当分配系统资源(CPU时间,内存)的实体。
简单来说就是:当你将你在VS或者Linux上面写的代码进行编译链接后,我们的磁盘里面会生成一个可执行程序,此时这个可执行程序还只是磁盘里面的一个文件,但是当你运行这个可执行程序之后,该可执行程序就会被加载进内存中,然后经过cpu处理它就会变成一个进程。我们这里需要注意的是:当你的可执行程序还未被加载进内存时,此时的它还只能算是一个文件或者说是程序,而不是进程,当它被加载到内存经过cpu处理后,我们才能称它为进程。
在我们Linux操作系统中其实是有很多进程的,我们可以使用ps aux命令便可以查看系统中的进程
我们上面说过程序文件加载进了内存,最后这个程序会变成进程。通过这个图片我们可以看到,系统是允许多个进程同时运行的,我们前面的文章说过OS是一款搞管理的软件,那么这么多的进程总得需要管理吧,那么这里这么多的进程由谁管理呢?这里需要我们的OS来对这些进程来进行管理。
那么问题来了:操作系统如何管理进程呢?
先描述,再组织!!!
进程的信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
操作系统将每一个进程进行描述,形成一个个PCB,并将这些PCB以链表或者其他数据结构的方式组织起来。如此一来,操作系统对进程的管理就变成了对链表的增、删、查、改操作。
因此我们就可以得出一个结论:
进程:可执行程序与管理进程需要的数据结构的集合。
知道了如何描述进程之后,大家可能还有一个疑问?那task_struct和PCB(进程控制块)的区别是啥呢?
这里的task_struct与PCB的关系就像是王婆与媒婆的关系。王婆是众多媒婆中具体的一个人(对象),而媒婆是说媒人的统称。
task_struct包含以下信息:
进程的信息可以通过ls /proc系统文件夹查看,其中有些子目录的目录名是数字
这些数字其实是某个进程的PID(进程id号),对应文件夹当中记录着对应进程的各种信息,我们如果像获取PID为1的进程信息,则查看/proc/1这个文件夹即可
使用ps aux命令可以显示所有进程信息
[root@izuf65cq8kmghsipojlfvpz ~]# ps aux
我们也可以将ps命令与grep命令结合起来使用,显示某一个进程的信息
[root@izuf65cq8kmghsipojlfvpz ~]# ps aux | head -1 && ps aux | grep proc | grep -v grep
- 进程id(PID)
- 父进程id(PPID)
在Linux中我们可以通过系统调用接口getpid()、getppid()来分别获取进程与父进程的id。
下面我们来看一段代码
#include
#include
int main()
{
while(1)
{
printf("hello process! pid: %d ppid: %d\n",getpid(),getppid());
sleep(2);
}
return 0;
}
我们通过man fork来查看一下fork函数:
功能: 创建一个子进程
返回值: fork函数有两个返回值,一个返回值是给父进程返回子进程的PID,还有一个返回值是给子进程返回0(如果子进程创建失败,就会返回-1)
下面我们来看一段代码
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 fork();
6 printf("hello process! pid: %d ppid: %d\n",getpid(),getppid());
7
8 return 0;
9 }
运行结果:
可以看到我们这里的运行结果是对printf语句里的内容打印了两次,但是打印的PID,PPID是不一样的,fork创建的子进程的PPID就是proc进程创建的PID,这也就可以说明proc进程与fork函数创建的进程是父子关系。因此这里也就验证了我们上面说的fork函数的功能是用来创建子进程的。
下面我们再来看一段代码
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 pid_t ret = fork();
6 printf("hello process! pid: %d ppid: %d ret: %d\n",getpid(),getppid(),ret);
7
8 return 0;
9 }
运行结果:
通过运行结果我们可以看到,这里fork确实有两个返回值,第一个返回值是给父进程返回子进程的PID,第二个返回值如果子进程创建成功就返回0,反之则返回-1。这个结果也就验证了我们上面说的fork函数的返回值是有两个的。
对于进程和fork函数我们有以下几个问题:
如何理解进程创建
创建进程,系统就会多一个进程,多了一个进程系统就要多一组管理进程的数据结构和该进程对应的代码和数据。 父子进程代码是共享的,数据是私有的,通过数据私有表现了进程的独立性。
fork为什么会有两个返回值?如何深刻的理解呢?
fork是一个函数,并且它是有返回值的。fork函数返回id前会完成创建子进程的逻辑,并且给子进程创建task_struct。当子进程创建完成后,子进程的进程控制块就会被放到运行队列中等待CPU的调度。由于return是一个语句,父进程要执行,那么子进程也会执行。函数的返回值是数据,因为父子进程数据是各自私有一份的,虽然id的变量名相同,但是由于内存地址不一样,所以最后返回的id是不一样的。
fork父子执行顺序和代码和数据赋值的问题
进程数据包含代码和数据,父进程创建子进程的时候,代码是共享的,数据是各自私有一份的(写时拷贝技术)fork父子进程执行的顺序是不确定的,因为两个进程的PCB都会被放到运行队列中,等待CPU的的调度,而执行顺序是由Linux下的调度器决定的,跟调度器的调度算法有关,因此这里的执行顺序是不确定的。
为什么给父进程返回子进程PID,给子进程返回0?
因为一个父进程是有很多个子进程的,但是子进程只有一个父进程,给父进程返回子进程的PID,父进程可以通过该子进程PID找到该子进程,给子进程返回0是表示当前子进程创建成功了,如果创建失败会给子进程返回-1.
我们上面知道了父子进程代码是共享的,但是我们如果让父子进程做相同的事情,那么创建子进程就没什么意义了。
其实,在fork之后我们一般使用if/else语句对父子进程进行分流,使我们的父子进程做不同的事情。
因为fork给父子进程的返回值是不同的,因此我们可以根据返回值不同,使用if/else语句来让父子进程执行不同的代码,从而使得父进程与子进程做不同的事情。
下面我们来看一段代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 pid_t ret = fork();
6 if(ret>0)
7 {
8 //parent
9 printf("I am parent! pid is: %d ppid is: %d\n",getpid(),getppid());
10 }
11 else if(ret==0)
12 {
13 //child
14 printf("I am child! pid is: %d ppid is: %d\n",getpid()),getppid();
15 }
16 else
17 {
18 printf("fork error\n");
19 }
20 sleep(1);
21 return 0;
22 }
运行结果:
我们看到了一件不可思议的事情,if与else语句里面的内容居然都被执行了,在C/C++中这是不可能的,但是在系统中在多进程中这是可以的。这是因为fork函数后面有两个执行流,并且通过if/else分流我们让子进程与父进程分别执行了不同的代码
为了搞清楚正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以由几个状态(在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 */
};
进程状态:
这份代码我们用sleep函数让它进行休眠50秒,我们再来查看一下这份代码运行之后是什么结果吧:
通过ps命令我们可以看到当前进程是处于S状态的。注意:浅度睡眠状态是可以通过kill命令将其杀掉的。
比如:我们有一个进程想对硬盘进行读写操作,读写操作是要涉及IO的,而IO是很慢的。
硬盘此时会有两个任务:1.找到进程想要的数据. 2.将这些数据拷贝给进程
我们知道硬盘是一个机械设备,它找数据是很慢的,因为数据并没有准备好所以不能直接拷给进程,此时进程就会处于深度睡眠状态。
当我们操作系统可用空间不足时,操作系统会有它自己的内存管理方式,它会通过杀掉一些进程来获得空间。而此时操作系统发现其他进程都处于R状态但是发现你这个进程居然处于休眠等待状态,这个时候操作系统会觉得:我们这都火烧眉毛了,你还在休眠等待,然后操作系统就将这个进程给杀掉了。
但是这个时候假如说硬盘找到数据了,想把这些数据拷给进程,但是硬盘发现这个进程不见了被操作系统给杀掉了。由于操作系统误杀一个进程,从而导致IO失败,进而引起硬件无法操作的这种情况就很难解决了。
注意:处于深度睡眠状态的进程是不可以通过kill命令将其杀掉的,也就是说它不会被操作系统杀掉,因此也就不会出现上面这种误杀的情况。
下面我们来实操一下:
我们再对进程发送SIGCONT信号,该进程就能够继续运行了。
注意:我们可以使用kiil -l命令来查看我们能够给一个进程所能发送的信号列表
[root@izuf65cq8kmghsipojlfvpz ~]# kill -l
在讲僵尸状态前我们先来讲一个小故事:当有一天,你在外边散步,突然你发现了一个老人倒下了,这个时候你打120,120到了之后发现这个人已经死了,医护人员会让你给警察打电话。警察接到你的报警电话后,他们第一步是会进行封锁现场、采集现场信息、将这个人抬走然后交给法医检查(如何死亡的问题),最后通知家属(他(她)已经去世了)。
当一个进程(倒下的老人)将要退出的时候,在系统层面,该进程曾经申请的资源并不会立即被释放,而是要暂时存储一段时间保持进程基本退出消息,方便操作系统或者父进程(警察)进行读取,获取退出的原因。
死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,因此你不会在任务列表里看到这个状态。
了解了Linux进程状态之后我们再来看一张图片吧(帮助我们更好的理解Linux进程状态之间的转换)
我们上面说了,一个进程如果正在等待其退出信息被父进程读取,那么我们就称该进程处在僵尸状态。当一个进程退出并且其父进程没有读取到子进程退出的信息时,我们就称该进程为僵尸进程。
我们下面来看一段僵尸进程的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 int main()
5 {
6 pid_t ret = fork();
7 if(ret==0)
8 {
9 //child
10 int count = 5;
11 while(count--)
12 {
13 printf("I am child pid: %d ppid: %d\n",getpid(),getppid());
14 sleep(1);
15 }
16 printf("child quit\n");
17 exit(1);
18
19 }
20 else if(ret>0)
21 {
22 //parent
23 while(1)
24 {
25 printf("I am parent pid: %d ppid: %d\n",getpid(),getppid());
26 sleep(1);
27 }
28 }
29 else
30 {
31 printf("fork error\n");
32 }
33 return 0;
34 }
运行该代码后,我们可以通过运行下面的监控脚本,每隔一秒对该进程的信息进行检测
[root@izuf65cq8kmghsipojlfvpz ~]# while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
我们可以看到,当子进程退出后,由于父进程没有读取到子进程的退出信息,子进程的状态就变成了僵尸状态。
Linux下的进程一般都是父子关系,我们知道当子进程退出,而父进程没有读取到子进程的退出信息,我们就称该子进程为僵尸进程。那么如果因为一个父进程先退出了,当子进程退出的时候没有人读取它的退出信息而导致该子进程变成僵尸进程,那么这样的子进程又是什么呢?
我们称这样的进程为孤儿进程。
我们知道如果没有人读取子进程的退出信息的话,是会导致子进程变成孤儿进程从而导致内存泄漏的。那么对于孤儿进程操作系统有没有什么解决方法呢?
答案是有的,当出现孤儿进程的时候,如果一直没有人读取它的退出信息的话,孤儿进程会被1号init进程领养,由1号进程来读取它的退出信息。
我们下面来看一段孤儿进程的代码吧
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 int main()
5 {
6 pid_t ret = fork();
7 if(ret==0)
8 {
9 //child
10 while(1)
11 {
12 printf("I am child pid: %d ppid: %d\n",getpid(),getppid());
13 sleep(1);
14 }
15
16 }
17 else if(ret>0)
18 {
19 //parent
20 int count = 5;
21 while(count--)
22 {
23 printf("I am parent pid: %d ppid: %d\n",getpid(),getppid());
24 sleep(1);
25 }
26 printf("parent quit\n");
27 exit(1);
28 }
29 else
30 {
31 printf("fork error\n");
32 }
33 return 0;
34 }
运行结果:
我们可以看到在父进程未退出时,子进程的PPID就是父进程的PID,当父进程退出后,子进程就变成了孤儿进程,此时孤儿进程会被1号进程领养,因此子进程的PPID就会变成1。
优先级: 优先级是我们使用“事物”的先后顺序,在Linux系统中cpu资源分配的先后顺序,就是指进程的优先级。优先级高的进程有优先执行权利。
权限: 权限是表示我们能不能使用某种“事物”。
优先级存在的原因主要就是因为资源是有限的,在操作系统中存在优先级的主要原因是因为CPU的资源是有限的,一般来说我们大家的电脑都是单cpu的,一个cpu每次只能跑一个进程,而在Linux下进程是可以有许多个的,所以就需要有优先级,来确定进程获取CPU资源的先后顺序。
比如说:我们在中午上完课后一般会直接走去食堂打饭。由于中午去食堂吃饭的学生很多,但是打饭的窗口是有限的,每个窗口每次只能一个人打饭菜,所以我们就必须得排队。我们打饭的过程也是一种获取资源的方式,先到窗口就会先打到饭。这就是我们生活中的一种优先级。
在Linux或者Unix系统中,用ps -l命令会类似输出以下几个内容:
[root@izuf65cq8kmghsipojlfvpz ~]# ps -l
我们可以注意到其中的几个重要信息,如下:
注意:在Linux操作系统中,PRI(new)= PRI(old)+ nice,其中PRI(old)默认为80.
我们可以使用ps -al命令查看该进程优先级的信息
[root@izuf65cq8kmghsipojlfvpz ~]# ps -al
注意:在Linux操作系统中,在默认情况下一个进程的优先级PRI默认为80,NI默认为0.
top命令类似于windows下的任务管理器,它能够实时的显示系统中进程的资源占用情况。
使用top命令后再按“r”键,会让你输入待调整nice值的进程的PID
当输入进程PID后,会让为这个进程设置nice值
输入nice值后按q即可退出,如果我们这里输入的nice值为-10,那么此时我们再用ps命令查看当前进程的优先级,我们会发现当前进程优先级的NI变成了-10,而PRI变成了70(70-10)
注意:如果想把NI值设置为负值的话,也就是说提升进程的优先级,是需要root权限的。
使用renice命令,后面跟上进程需要设置的nice值以及进程的PID,就可以更改这个进程的nice值了
注意:使用renice命令,如果想将一个进程的nice值设置为负值的话,也是需要root权限的。