Linux高级编程——fork进程控制
一、进程标识
每个进程都有一个非负整数表示的唯一的进程 ID,因为进程 ID 标识符总是唯一的,所以常常把 ID 作为其他标识符的一部分用来保证唯一性。
并且进程的 ID 也是可以复用的,当一个进程终止后,其进程 ID 就可能被其他刚创建的进程所使用。
Linux 系统提供了一些获取进程 ID 的函数:
getpid 获取当前进程ID
getppid 获取父进程ID
这两个函数比较简单,就是直接调用然后输出即可,可以自己尝试输出试试。
二、进程的 system 接口
我们在 Windows 下有 system
接口可以使用,例如打开记事本:
system("notepad");
同样在 Linux 下也有这个接口,可以执行相关的程序:
例如,使用 system 接口来执行 ls 命令:
system 接口底层其实还是使用系统调用 fork,exec,waitpid 来执行程序,只是在上层又封装了一次。
三、创建进程 fork
1. fork 的定义
在 Linux 中,我们使用 fork
来创建一个子进程:
2. fork 的返回值
fork 函数有些特殊,成功它返回 2 次,失败返回 -1,利用这个特性可以判断当前的进程是子进程还是父进程:
- 在子进程中返回 0
- 在父进程中返回子进程的进程 ID
我们编译运行看看效果:
可以看到父进程的返回值是子进程的 PID:12557,子进程的 PID 正是 12557,这也验证了 fork 的返回值的特点。
3. fork 的写时复制技术
通过执行 fork,子进程得到父进程的一个副本,例如子进程获得父进程的数据空间,堆和栈的副本,但是它们并不共享存储空间,它们只共享代码段。
但是在现在的系统实现中,并不执行拷贝父进程的副本,作为替代方案,而是使用写时复制(Copy - On - Write)技术。
写时复制:在 fork 之后,这些区域由父子进程共享,而且内核将它们的访问权限改变为只读,如果父子进程中的任何一个试图修改这些区域,内核只为修改区域的那片内存制作一个副本给子进程。
不管是哪种技术实现,最后父子进程的数据都是独立的,不会相互影响,我们来看一个实际的例子:
编译运行:
结果是父子进程中的 count = 1
,如果父子进程的 count 不独立的话,子进程的 count 应该等于 2,但是实际上是等于 1,说明父子进程的数据是独立的。
4. 子进程的执行位置
fork 还有一个特点:子进程不是从 main
函数开始执行的,而是从 fork 返回的地方开始,我们来看个实际的例子:
编译运行结果:
看到只打印一个 Before
信息,没有打印 2 个 Before
原因是:
内核通过复制父进程 9670 来创建子进程 9671,并将父进程 9670 代码和当前运行到的位置都复制到子进程 9671。
所以新的子进程 9671 从 fork 返回的地方开始运行,而不是从头开始,也就不会打印开头的 Before
了。
四、创建进程 vfork
还有一个创建进程的系统调用 vfork
,它跟 fork 很相似,但是也有几点不同:
- vfork 的目的是创建一个子进程来运行一个程序
- vfork 并不复制父进程地址空间,子进程在父进程地址空间中运行,并阻塞父进程直到子进程返回
- vfork 保证子进程先运行
- 子进程需要调用 exec 或 exit 函数退出,否则会带来未知结果。
来看个实际的例子:
编译运行:
这个结果跟前面使用 fork 的例子是不同的,使用 vfork 先打印的子进程信息,再打印父进程的信息,是因为父进程被阻塞了,直到子进程执行完了才有机会执行。
并且由于子进程在父进程的地址空间中运行,所以子进程中对 count 加一的操作对父进程也是有效的,因此最后父进程的 count = 2
。
exec
fork 函数里面最后也是调用 exec 等函数来执行程序的,我们有必须要了解这个函数:
exec 有很多变种函数,例如 execlp
,execle
,等等,但基本的用法都是差不多的,这里就以 execl
为例来看个程序:
运行结果就相当与 shell 命令:ps - ef
,其他的变种函数可以通过 man exec
来查看。
五、进程等待 wait
父进程可以使用 wait 系统调用主动等待子进程或者指定进程结束,并获得子进程的结束信息:
这个系统调用的过程如下:
- wait 暂停调用它的进程直到子进程结束
- wait 调用成功返回子进程的 PID
- wstatus 存储子进程的返回信息(正常退出,异常退出,被信号杀死),以此来知道子进程是如何结束的
大致的流程如下:
来看看 wait 是如何使用的:
编译运行:
可以看到父进程成功等待了子进程 3 s,并得到了存储在 status
中的返回值,这个 status 有 2 种状态:
- 如果子进程调用 exit 退出,那么内核将 exit 的退出状态码放在 status 中
- 如果进程被杀死,内核将信号序列放在 status 中
实际使用时,wait 提供了相关的宏来判断 status 的状态,详情参考 man wait
。
另外 waitpid
与 wait
几乎的相同的,作为锻炼,就留给你自己去学习吧,参考 man waitpid
。
六、进程结束
既然能够创建进程,那肯定能够结束进程,在 Linux 中进程退出又分为正常和异常退出,分别来了解了解。
1. 正常退出
有 5 种正常退出进程的方法:
- 在 main 内执行
return
,等价于调用 exit - 调用
exit
- 调用
_exit
或_Exit
- 进程的最后一个线程在其启动例程中执行 return 语句
- 进程的最后一个线程调用
pthread_exit
函数
2. 异常终止
有 3 种异常终止的方法:
- 调用
abort
,产生SIGABRT
信号 - 当进程接受到某些信号时
- 最后一个线程对「取消」请求作出响应
不管是哪种终止情况,我们都可以使用 wait
或者 waitpid
来得到子进程的退出状态。