目录
进程标识符
查看进程的标识符
ps axj | head -1&& ps axj | grep 程序名
ls /proc/进程标识符
获得进程标识符
getpid()函数
getppid()函数
创建一个子进程
fork函数解析
fork函数返回子进程的pid给父进程的原因
fork函数有两个返回值的原因
一个进程实质上就是一个被执行的程序。操作系统会将这个程序使用PCB结构体保管起来,之后通过链表的形式将各个进程的PCB结构体和其相对应的代码数据管理起来。
在Linux当中PCB结构体有很多种,最主要的是task_ struct。在task_ struct结构体当中存储着各种进程的的属性。例如:标示符,状态,优先级等等。为了更加清楚的认识进程相关的知识,我们将会依次介绍其中的几个主要的属性以及集中最主要的进程。
进程标识符
查看进程的标识符
在我们使用计算机的时候总会打开很多进程。比如我们会在使用浏览器写博客的同时,打开Linux软件执行Linux指令,还有可能打开画图,画思维导图便于我们理解。这几个窗口就是很多不同的进程。
同样的Linux操作系统当中也是一样的。我们之前说过所有的可执行程序都是一个进程,所以我们编写的代码也是一样。我们可以通过特定的指令查看我们的进程编号。
ps axj | head -1&& ps axj | grep 程序名
其中ps指令就是我们最经常使用的查看程序进程号的命令,axj表示的是参数,我们在后面会详细介绍相关参数的含义,我们在这里直接使用即可。head -1表示显示所有进程的属性名,可以被省略。
主要发挥查找进程的作用的是后面的ps axj | grep 程序名。ps会显示所有的进程相关的内容,之后通过grep过滤符可以查找特定进程的相关信息,忽略其他进程等繁
杂的内容。
我们输入上述的命令之后会发现我们进程的表示符为12218。
ls /proc/进程标识符
在找到进程标识符之后我们可以通过ls指令进行查看文件夹当中进程具体的内容,也就是task_ struct当中所包含的进程的的属性和数据。执行指令效果如下:
其中cwd当中所示的就是我们数据所保存的位置,也就是我们编写的代码的位置。
获得进程标识符
获得进程标识符的方法有很多种,使用Linux指令查看进程标识符只是其中的方式之一。我们还可以通过对程序代码的编写,也就是调用系统函数获取进程的标识符。
getpid()函数
我们可以使用getpid函数获取我们该进程的标识符。关于getpid函数的详细介绍我们可以通过man手册进行查看。
我们会发现函数的函数原型为:pid_t getpid(void) 其中的pid_t实质上是我们操作系统自己重命名生成的一个数据类型。它本身也就是一个int类型的数据。也就是说我们在调用getpid函数的时候会有一个整数也就是我们该程序的标识符编号。
程序运行的效果:
我们会发现获得的程序的进程标识符都是相同的。
getppid()函数
我们可以通过getppid函数获得父进程的标识符。父进程就是创建子进程的程序。我们所有的程序都是通过操作系统所运行的,当我们有很多用户的时候为了方便操作系统区分不同的用户,所以就会创建很多不同的进程。我们在该进程当中继续执行特定的指令或者编写代码,生成进程。所以新生成的进程就是子进程,原本就存在的进程就叫做父进程。
同样的我们可以通过函数获得父进程的标识符。获得父进程的标识符的函数就是我们上面所展示的getppid。返回值同样是pid_t(一个整形值)。我们通过代码进行测试如下:
程序运行的结果如下:
创建一个子进程
那么除了运行我们的程序代码还有其他方法创建一个进程吗?答案是有,我们同样可以通过系统调用函数fork函数创建一个子进程。
我们先通过函数原型进行认识fork函数。
在使用fork函数的时候我们需要添加<unistd.h>的头文件,我们可以看出fork函数的作用是创建一个子进程。之后我们继续阅读函数的返回值:
如果我们成功创建一个子进程的话,我们创建的子进程就会返回孩子进程的标识符,返回0给我们的孩子进程。如果失败就会返回-1给我们的父进程。
看到这里我们可能会很奇怪,我们之前学习的不是函数只能有一个返回值吗?为什么这里会有两个呢?要想知道使用fork函数会有两个返回值的原因,我们可以先尝试使用fork函数创建一个进程试一试。
运行结果如下:
我们会发现原本应该运行打印的hello world在这里打印了两遍。这就是我们fork函数创建一个进程之后发挥的作用。我们两个进程在执行代码的时候会共同执行公共部分的代码,也就是说子进程执行打印了一遍hello world父进程也执行了一遍hello world。
fork函数解析
fork函数的返回值有两个,一个是针对我们的子进程的,一个是针对我们的父进程的。所以我们想要辨别子进程和父进程就可以通过使用变量进行接收fork函数的返回值进行判断。我们可以书写以下的代码:
运行结果:
我们可以发现执行代码之后确实产生了两个进程,运行结果和我们编写的代码相同,但是又有一些不同。只有在if判断里面的代码会出现一遍,在公共部分的代码出现了两边。运行的效果也和我们的预期相同,我们会先运行父进程当中的代码,之后再运行子进程当中的代码。(其中父进程的父进程为bash进程)
fork函数返回子进程的pid给父进程的原因
我们重新进行分析:为什么fork函数的两个返回值分别是给父进程返回子进程的pid,给子进程返回0呢?
这是因为我们每一个进程都可以拥有多个子进程,但是我们的子进程却只能拥有一个父进程。跟树形结构很类似。所以我们子进程对于自己并不需要获得标识符信息,但是为了方便我们的父进程进行进程方面的管理。我们才需要向父进程返回子进程的pid。
fork函数有两个返回值的原因
那么第二个问题来了:正常的函数都是有一个返回值,那么我们的fork函数是怎么做到有两个返回值的呢?
我们之前说到过操作系统在管理进程实质上是操作系统对PCB结构体以及代码相关的数据进行管理。所以我们在创建一个新的进程的时候就需要创建一个新的PCB结构体和相关的数据,并将PCB结构体链入到我们的结构体构成的链表当中。
但是我们存储两份数据对我们操作系统当中的空间很不友好,会大量的浪费我们的内存空间。因此我们就想能不能存储一份数据。因为我们创建的子进程和父进程共同执行相同的部分,所以我们只需要将新旧进程当中指向同一份数据代码即可。我们可以使用ls代码进行验证我们的猜想:
其中24563为我们父进程的进程标识符,24564为我们子进程的进程标识符。我们发现其中的cwd都是指向同一个目录地址。
但是我们这样使用同一份数据真的是合理的吗?假如我们修改一个子进程当中的变量的值,是不是父进程当中的变量的值也对应进行了改变呢?就好比我们给两个QQ好友发消息,给一个人发的消息另一个人也可以看到,这样肯定不合理。
所以我们就采用了写时拷贝的特点,当我们的程序需要对数据进行修改的时候,就将这个数据进行备份,将我们修改出来的数据放到我们备份好的变量当中。
而我们的函数在返回的时候也是一种进行数据写入的操作,所以就需要进行数据的拷贝,也正是因此fork函数会有两个返回值。
第三个问题是为什么我们一个变量当中能够存储两个数据。这个问题我们需要在之后学习过地址空间之后再向大家进行详细的介绍。
我们使用fork函数创建出来的子进程和父进程的运行顺序由我们的调度器进行决定,没有具体的先后运行的顺序。
因为我们父进程也有一个父进程,也就是我们的bash进程。因此我们很容易想到bash进程创建“父进程”的原理其实和我们父进程创建子进程的原理是相同的。