Linux进程控制
1. 进程创建
1.1 fork
在Linux中,我们通常使用fork函数来为一个已经存在的进程创建一个新进程。而这个新创建出来的进程被称为原进程的子进程,原进程被称为该进程的父进程。
该函数其实是一个系统调用接口,原型如下:
#include <unistd.h>
pid_t fork(void);
特性:子进程会复制父进程的PCB,二者之间代码共享,数据独有,拥有各自的进程虚拟地址空间。
此时可能会有一个疑问,既然代码共享,并且子进程是拷贝了父进程的PCB,虽然他们各自拥有自己的进程虚拟地址空间,但其中的数据必然是相同的(拷贝而来),并且通过页表映射到同一块物理内存中,那么又如何做到数据独有呢?答案是:通过写时拷贝技术。
写时拷贝技术:子进程创建出来后,与父进程映射访问同一块物理内存,但当父子进程当中有任意一个进程更改了内存中的数据时,会给子进程重新在物理内存中开辟一块空间,并将数据拷贝过去。 这样避免了直接给子进程重新开辟内存空间,造成内存数据冗余。换句话说,如果父子进程都不更改内存中的值,那他们二者各自的进程虚拟地址空间通过页表映射,始终是指向同一块物理内存。
正是通过这样的写时拷贝技术,才保证了父子进程代码共享但数据独有的这一特性。 对于一些小萌新来说,可能上述文字描述并不是那么直观,此处有必要上图来进一步说明一下:
如父进程中有全局变量g_val初值为10,子进程创建之后通过复制父进程的PCB,并且二者的进程虚拟地址空间通过页表映射到同一块物理内存,但如果子进程更改了g_val的值,就会在物理内存中开辟新的空间并保存属于子进程的g_val:
在知道了以上特性后,下面我们来认识一下fork函数的返回值,相当重要!
通过以上函数原型我们可以看到起返回值是pid_t类型,其实就是int,在内核中是通过typedef重命名过的,我们把其当做int类型即可。
如果创建子进程失败,会返回-1,是小于0的,而如果创建子进程成功,该函数则会返回俩个值,这一点和普通的函数有很大区别。它会给子进程返回0值,而给父进程返回子进程的pid(一个大于0的数),也正是通过给父子进程返回值的不同,从而我们可以使用选择语句对齐进行分流,从而让父子进程执行不同的代码,而达到我们创建子进程的某种目的。
在了解到这一点之后,我们便可以通过代码来创建子进程并且进一步验证前面说到的一些特性。
#include <stdio.h>
#include <unistd.h>
//父子进程代码共享,但数据独有
int g_val = 100;
int main()
{
pid_t pid = fork();//创建子进程
if(pid < 0) {
printf("fork error!\n");
return -1;
}
else if(pid == 0) {
//子进程
g_val = 200;
printf("This is Child! g_val = %d p = %p\n",g_val,&g_val);
}
else {
//父进程
sleep(1);
printf("This is Parent! g_val = %d p = %p\n",g_val,&g_val);
}
return 0;
}
运行程序,得到如下结果:
对于这一结果感到惊讶吗?其实只要你看懂了我上面所说的内容,相信这个结果并不难理解:子进程拷贝父进程的PCB,拥有和父进程一模一样的进程虚拟地空间以及数据,但子进程将自己的g_val更改后,会在物理内存中为其重新开辟空间来存储子进程更改后的数据,而结果中看到的地址完全相同,则是因为它们仅仅是虚拟的地址空间,真正的值是存储在物理内存中的。而这时通过页表的映射,这俩个看似相同的地址已经指向了不同的物理内存。
1.2 vfork
不止可以通过fork来创建子进程,vfork也同样是用来创建子进程的系统调用函数,那么它和fork有什么区别呢?
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
通过函数原型我们似乎并不能看出什么端倪,的确,vfork在使用时和fork几乎没有什么区别,返回值及其含义也和fork完全相同。其和fork的区别在于,用v_fork创建出来的子进程,也是拷贝父进程的PCB,但它和父进程共享同一个进程虚拟地址空间。也就是如下图所示的这样:
但是我们要思考一个问题,父子进程共享同一个进程虚拟地址空间不会有问题吗?会的!会造成调用栈混乱的问题! 举个例子,如果父进程中调用Test函数首先压栈,之后子进程则调用Fun函数,由于二者共享同一个栈空间,则Fun函数也会继续压栈,但如果此时父进程的Test函数调用完毕想要返回,却发现其并不在栈顶位置,无法出栈,这不就有问题了吗?
那怎么解决呢?vfork采用的方案是,在其创建出子进程之后,让子进程先执行,而父进程则会阻塞,直到子进程执行完毕,父进程才会开始执行,这样就避免了调用栈混乱的问题。
但是!这个问题是解决了,可是新的问题也随之而来了呀,我们创建子进程难道不是为了让其而父进程并发的跑或者说更高效的完成一些任务吗,而现在再子进程退出前父进程什么都不能做,这难道不会影响效率吗?或者说的再直白一些,不是浪费时间吗???
不得不说,确实。可能也正是因为这些种种的缺点,vfork这个函数已然逐渐的被时代淘汰了,fork它不香吗?为什么要用vfork呢? 博主也理解不了它存在的意义…不过也罢,我们只需稍作了解,然后还是把爱全都给fork吧!
2. 进程终止
含义:进程终止的含义就是一个进程的退出。
进程退出的场景:
- 程序运行完毕,从main函数中退出
1.1 运行完毕,结果正确
1.2 运行完毕,结果不正确 - 程序没有运行完毕,中途奔溃了
进程常见退出方法:
1. 正常退出:
- 从main函数返回
- 调用exit函数
- 调用_exit函数
2. 异常退出: Ctrl+C,信号终止等
exit函数:
#include <stdlib.h>
void exit(int status);
其中,stauts定义了进程的终止状态,由用户自己传递&#