进程概念
在大部分教材中,它们如下描述进程:
正在运行的程序就是进程
以上描述并没有错误,但是有点过于笼统了,现在我们要深入Linux
底层看一看程序是如何被管理的,进而更加全面地了解什么是进程。
在Linux中,我们可以一次运行多个程序,既然操作系统要给我们运行程序,那么操作系统就要得到该程序代码和数据。也就是说执行进程的时候,要把数据和代码段加载到内存中。
而操作系统中往往不止一个进程,比如你可以在Windows中打开QQ,微信,浏览器等等,它们同时运行。因此操作系统要一次性管理多个进程,也就是会有多个程序加载到内存中。
那么操作系统要如何管理这些进程呢?答案是先描述,再组织,也就是先用结构体把各个进程描述出来,比如这个进程的状态,优先级等等。然后再用数据结构把这些进程组织起来。
这个描述进程的结构体叫做PCB,再具体一点,在Linux源码中,PCB的结构体名为task_struct。
进程是可以排队的,在一个时间片里面,轮到哪一个进程,就运行哪一个进程,那么进程就要排好队,让操作系统一个一个的去运行。难道我们是让程序亲自去排队吗?这当然不是,我们已经用PCB把进程描述了出来,后续只需要让PCB这个结构体去排队即可。
在Linux中,有一个运行队列,其是一个链表,然后把PCB一个接一个地连入链表中,操作系统只需要遍历链表,遍历到谁就运行该PCB对应的可执行程序。当谁运行完了,就把谁的PCB移出队列,谁想要被执行,就把谁的PCB移入队列。
因此进程排队,本质上不是进程在排队,而是进程的PCB在排队。
而进程的管理行为,就是先用PCB结构体来描述进程,然后用数据结构链表来组织各个PCB。
那么我们再来描述什么是进程:
进程 = 可执行程序 + PCB
而操作系统中一切管理进程的行为,本质都是管理进程的PCB。
我们再来简单讲解一下PCB,也就是task_struct中最常用的成员:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程
- 状态:任务状态,退出代码,退出信号等
- 优先级:相对于其他进程的优先级
- 程序计数器: 程序中即将被执行的下一条指令的地址
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
比如说内存指针
,其用于标识代码执行到哪一个语句,在下次运行程序时,就可以直接继续运行。
再比如记账信息,其存储着该进程总共占用了多久的CPU
,这样操作系统就可以更好的决策,防止一个进程过久占用资源。
查看进程
我们可以通过指令ps ajx
或者ps aux
来查看当前的所有进程:
当然这会造成大量刷屏,一般来说,我们会选择配合grep
来进行查找。
在上图中,第二栏 PID
代表进程的唯一标识符
现在我们有一个自己写的程序test.exe
,其内部是一个死循环,让其一直运行:
int main() { while(1) { sleep(1); } }
我们运行后,用指令ps ajx | grep test.exe
:
此时进程test.exe可以被查看到了,其PID为31390。不过我们这里出现了两个进程,第二个进程其实是grep指令,因为我们向grep中写入了test.exe字符串,因此查找进程的时候,也可以查找到grep自己。
我们不仅仅可以通过ps ajx查找来获得进程的PID,函数getpid也可以获得当前进程的PID。
getpid被包含在头文件<unistd.h>中,其返回值为pid_t类型,本质上是一个int类型。这个pid_t类型包含在<sys/types.h>中。
比如test.c
中有以下代码:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { pid_t id = getpid(); while(1) { sleep(1); printf("pid = %d\n", id); } }
编译后执行进程test.exe
,代码就开始输出了:
此时我们也可以通过ps ajx
指令来查看这个PID
,ps ajx | head -1 && ps ajx | grep 20759
:
可以看到,我们确实可以查到PID
为20759
的进程,并且COMMAND
属性为./test.exe
,意思就是我们通过指令./test.exe
执行了该进程。
父子进程
在Linux中,每个进程都有它的父进程,ps ajx
的第一栏PPID
就是父进程的PID
,比如刚刚图片中,./test.exe
的父进程就是20689
:
函数getppid
可以获取父进程的PID
,其包含在<unistd.h>
头文件中,返回值类型也是pid_t
。
现在在test.c
中写入以下代码:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { pid_t pid = getpid(); pid_t ppid = getppid(); while(1) { printf("ppid = %d,pid = %d\n", ppid, pid); sleep(1); } }
编译后执行test.exe
,输出结果为:
可知其父进程的PID
为20689
,我们现在通过指令ps ajx
来查询一下20689
:
其中PID
为20689
的进程为bash
进程,这里要提出一个重要概念:
一切在命令行调用的进程,都是bash
的子进程
/proc
proc
全称process
,也就是进程的英文名,其处于根目录下,内部存储各个进程的相关信息,并且每个进程以PID
作为目录名,将所有信息整合到对应的目录中。
先简单查看一下proc
目录下面有什么,ls /proc
:
在我的xshell
中,蓝色的文件代表目录,可以看出/proc
内部大部分是以数字命名的目录,这个数字代表进程对应的PID
,刚刚查看到bash
进程的PID
为20689
,那我们就看看/proc/20689
目录下面有什么:
可以看到其内部存放了很多描述性质的文件。我这里列举两个重要的:
cwd
:代表该进程的当前工作目录
上图就指明了,bash
进程当前的工作目录为/home/box-he/CSDN/process/conception
exe
:代表该进程对应的可执行文件的路径此处
bash
进程对应的可执行文件就是/usr/bin/bash
fork
fork
函数可以用于在程序内部创建子进程,其包含在头文件<unistd.h>
中,直接调用fork()
就可以创建子进程了。
示例代码:
#include <stdio.h> #include <unistd.h> int main() { printf("before: ppid = %d,pid = %d\n", getppid(), getpid()); fork(); printf("after: ppid = %d,pid = %d\n", getppid(), getpid()); return 0; }
以上代码中,我们在fork
前输出了一个before
以及进程的PID
和PPID
。在fork
后,又输出了after
以及进程的PID
和PPID
。
运行结果:
可以看到,我们的before
输出了一次,也就是我们调用的进程./test.exe
输出的,而after
输出了两次,但是我们只有一个after
语句,说明有两个不同的进程执行了这个语句,也就是fork
成功创建了一个进程。
对于第一条语句before
,毫无疑问这是进程./test.exe
,PID
为22840
,PPID
为20689
也就是bash
。
第二条语句after
,其PID
和PPID
都和./test.exe
一致,说明这个语句也是原先的./test.exe
输出的。
第三条语句after
,其PID
为22842
,没有出现过,说明这个是通过fork
创建出来的进程,其PPID
为22840
,也就是./test.exe
,说明fork
创建出来的进程,是原先进程的子进程。
以上示例可以总结为:
fork
之后,会出现两个进程
- 一个是原先的进程
- 另外一个是通过
fork
创建的进程
- 新创建的进程,是原先进程的子进程
fork
函数也是有返回值的,其返回规则如下:
- 对于父进程,返回值为新的进程的
PID
- 对于子进程,返回值为
0
此时我们就可以根据fork
的返回值,来判断父子进程了:
代码示例:
#include <stdlib.h> #include <unistd.h> #include <sys/types.h> int main() { pid_t id = fork(); if(id == 0) { printf("child: ppid = %d,pid = %d\n", getppid(), getpid()); } else { printf("father: ppid = %d,pid = %d\n", getppid(), getpid()); } return 0; }
输出结果:
子进程输出了child:
开头的语句,父进程输出了father:
开头的语句。
我们确实通过这样的分支语句,利用父子进程的fork
返回值不同的特性,完成了父子进程输出不同的代码。
其实fork创建子进程的时候,是以父进程为模板的,子进程会继承父进程的PCB,然后把PCB内部需要修改的地方改为自己的,比如PID,PPID是不同的。
子进程还和父进程共用代码段,因为两者的代码逻辑是一样的。比如说刚才的示例中,父子进程都要执行if-else的判断,两者都共用这一段代码。
但是两者的数据不一定相同,一开始父子进程共用一段数据,一旦父子进程有一方要对数据进行修改,那么就发生写时拷贝,此时数据就互不影响了。如果某个数据从头到尾都没有被修改,那么这个数据从头到尾都被父子进程共享,不会额外开辟内存。
我们在一开始讲过,PCB内部有一个叫做内存指针的成员,如果不记得了,复习一下:
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
其可以标识当前代码执行到了哪里,那么在父进程执行到fork
的时候,此时就要开始创建子进程了,子进程会继承父进程的PCB
。
由于父进程此时执行到fork,那么内存指针就指向fork这个语句,因此子进程继承的内存指针也指向fork,所以子进程是从fork开始往后执行的。
那么下一个问题就是:fork函数是如何做到,一个函数返回两个值的呢?
回答这个问题之前,我先反问你一个问题,创建子进程是在什么时候创建的?
你也许会回答,就是fork的时候创建的,但是深究一下,你就会发现,一定是在fork函数内部创建的子进程。这个内部很关键,也就是说在fork函数还没有return返回的时候,就已经是两个进程了。
于是两个进程共用这个fork的代码,但是两个进程的数据不一样,所以其实pid_t id = fork();这个过程中,父子进程分别return了一次。所以本质上不是fork函数返回了两个值,而是同一段return代码,被父子进程分别调用了。