8.1 概述
本章在上一章(进程环境)的基础上,进一步介绍了UNIX系统的进程知识,包括创建进程、执行进程和终止进程,以及进程的各种属性和机制。
注:本章内容较多,作为笔记,我已经尽量限制了文章的篇幅,很多只是提了一下关键的概念性内容,更详细的实现可以参考原书。
8.2 进程标识
每一个进程都有一个非负整数作为其ID,该ID标识一个唯一进程,不同的进程不会拥有相同的进程ID(即使使用下文提到的fork函数创建新进程,新进程和父进程共享完全相同的内存,它们的进程ID也不同)。一个已经终止的进程,其ID会被操作系统回收,以便在未来的某个时刻分配给新创建的进程。
系统内有一些特殊进程,比如ID为0的进程是调度进程,常被称为交换进程(swapper),该进程是内核的一部分,是系统进程(system process)。ID为1的init进程,该进程负责在自举内核后启动一个UNIX系统。init进程绝不会终止,但init进程是一个普通的用户进程(user process),它以超级用户特权运行,通常init进程会作为其他孤儿进程(orphaned child process)的父进程。
以下函数可以获得进程ID
#include <unistd.h>
// 返回当前进程的进程ID
pid_t getpid(void);
// 返回当前进程的父进程ID
pid_t getppid(void);
8.3 创建进程
一个现有的进程可以调用fork函数来创建一个新进程
#include <unistd.h>
pid_t fork(void);
fork函数返回两次,一次是父进程内返回,一次是在子进程内返回。父进程内的fork函数返回子进程的进程ID,而子进程内的fork函数返回0,所以我们可以通过判断fork函数来区别当前进程是父进程还是新创建的子进程。
int main(void) {
if (fork() == 0) {
// child process
printf("child processn");
return 0;
}
// parrent process
printf("parent processn");
return 0;
}
这里忽略了fork函数的返回值,仅仅做了判断,并且没有进行错误处理,该实例仅为了让读者明白如何通过fork函数的返回值来判断父子进程。
子进程和父进程”共享“相同的数据段、堆和栈,之所以加引号是因为这里的 “共享”并不是真正意义上的共享,调用fork函数后,子进程实际上获得父进程资源的拷贝,也就是说此时子进程和父进程的内容是完全一样的,但为了节省内存空间和减少复制资源所带来的开销,操作系统使用了写时拷贝(Copy-On-Write,COW)技术,这些进程资源由父进程和子进程共享,并且访问权限为只读(read only),当父进程和子进程中任意一方试图修改这些区域时,内核只为修改区域的那块内存制作一个副本(通常就是我们在操作系统概念里提到的内存页),这样就降低了将整个内存数据进行拷贝的巨额开销。
值得一提的是,父进程和子进程共享打开的文件描述符和相同的文件偏移量(因为底层指向相同的文件表),更多细节可以查看【第三章 标准I/O】的文件共享章节。
8.4 终止进程
在进程环境一章中我们提到过,使进程终止有很多种方法,其中return和exit是等价操作,它们调用终止处理程序(使用atexit函数注册),关闭标准I/O流等操作。而_exit和_Exit则直接进入内核态,不冲洗(flush)标准I/O。
进程的终止又分为正常终止(normal termination) 和 异常终止(abnormal termination)。
其中,在main函数内调用return、调用exit、_exit或_Exit属于正常终止。而调用abort或收到某些信号(signal)属于异常终止。不管进程如何终止,最后都会执行内核中的同一段代码,这段代码关闭进程打开的所有文件描述符,释放它所使用的存储器等(所以不会出现已经终止的进程依然占用某个文件的情况)。
不管哪一种终止情况,父进程都能够知晓子进程是如何终止的。
对于三个终止函数(exit、_exit和_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传送给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的中止状态(termination status)。在任意一种情况下,该终止进程的父进程都能够用wait或waitpid函数取得其终止状态。
注意,这里使用了“退出状态”和“终止状态”两个术语,以表示有所区别。
如果子进程完全消失了,父进程是无法获取它的终止状态的。内核为每个终止子进程保存了一定量的信息,当终止进程的父进程调用wait或waitpid时,可以得到这些信息,此时改进程才算是真正意义上的“消失”。
一个已经终止,但是父进程尚未对其进行善后处理(没有获取子进程的终止信息,释放它仍然占用的资源)的进程被称为僵尸进程(zombie)。
如果父进程在子进程之前终止,则该进程的所有子进程的父进程都改变为init进程,我们称这些进程由init进程收养。在这些进程结束时,init进程会调用wait函数来获取其终止状态,避免其成为僵尸进程。
8.4.2 wait和waitpid函数
当一个进程终止时,内核向它的父进程发送SIGCHLD信号(该信号是异步的),当收到该信号时,父进程可以选择忽略或者调用wait、waitpid函数来获得其终止状态。
#include <sys/wait.h>
// 两个函数若成功返回进程id,失败返回0
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
在调用wait和waitpid时:
-
若所有子进程都还在运行则阻塞 -
若一个子进程终止,其终止状态还没有被获取,则返回该进程的终止状态 -
如果没有任何子进程,则立刻出错返回
waitpid和wait相比,提供了更加多的选择(如等待指定进程,不阻塞等),这里就不一一介绍了,有兴趣可以去看原书【第八章 进程控制】的8.6节,此外还有waitid
,wait3
,wait4
等函数,他们的作用都是相同的,只不过提供了不同的选项和粒度。
8.5 竞争条件
当多个进程都企图对共享数据进行处理,而运行结果又取决于进程的运行顺序时,就发生了竞争条件(race condition),比如父子进程同时向标准输出打印Hello, world,而我们事先又将标准输出设为了无缓冲(unbuffered),则可能会出现下面的情况:
# 示例8.5-1
butn-linux> ./a.out
HHello,world
ello,world
butn-linux> ./a.out
Hello,world
Hello,world
输出的结果是不确定的,因为父子进程之间没有同步机制(比如锁等),这种情况在别的编程语言中也会出现,和语言无关。所以在处理共享资源时,一定要注意进程或线程间的同步。
8.6 exec函数
exec用来执行一个新的程序,拿示例8.5-1的shell举例,我们在shell中输入a.out这个程序并回车执行时,shell会先fork出一个进程,然后在该进程上调用exec函数来执行a.out这个程序。
exec并不创建新进程,exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆和栈。
exec函数有7种,它们分别提供了不同的参数以提供不同的执行方式。
毕竟本文只是笔记性质的文章,为防止篇幅过长,详细可参考原书【第八章 进程控制】8.10节。
8.7 解释器文件
所有的UNIX系统都支持解释器文件(interpreter file),这种文件的起始行形式是#! pathname [optional-argument]
,最常见的就是#! /bin/sh
,内核调用解释器文件第一行所指定的程序来解释执行该解释器文件,而不是直接执行该解释器文件,比如这里的#! /bin/sh
就是告诉操作系统,调用/bin目录下的sh来执行这个文件。
请注意将解释器文件(文本文件)和解释器(由解释器文件第一行所指定)区分开。
8.8 system函数
使用system来执行一个命令字符串很方便,假如我们要将时间和日期放到某一个文件中,可以使用system("date > file")
来实现这一操作。
system函数内部实现会调用fork、exec、和waitpid,因此有三种返回值。使用system而不是直接使用fork和exec的优点是:system进行了所需的各种出错处理以及各种信号处理。
8.9 进程会计
大多数UNIX系统提供了一个选项以进行进程会计(process accounting)处理。启用该选项后,每当进程结束时,内核就会写一个会计记录,一般包括命令名,所使用的的CPU时间总量,用户ID和组ID、启动时间等。
会计记录结构定义在头文件<sys/acct.h>
中,各种系统的实现各不相同。
8.10 进程调度
调度策略和调度优先级是由内核确定的,我们可以通过调整nice值来更改进程的优先级。nice值越低,优先级越低。
进程可以通过nice函数获取或者更改它的nice值,进程只能影响自己的nice值,不能影响其他任何进程的nice值。
#include <unistd.h>
// 成功返回新的nice值,失败返回-1
int nice(int incr);
除此之外,还有getpriority(),setpriority()等函数来获取和设置进程优先,它们提供不同的参数列表,以提供不同选项和粒度的控制。
8.11 进程时间
任何进程都可以使用times函数来获取自己以及已终止子进程的墙上时钟时间(wall clock time),用户CPU时间和系统CPU时间。
#include <sys/times.h>
// 成功返回流逝的墙上时钟时间(以时钟滴答数为单位),出错返回-1
clock_t times(struct tms *buf);
8.12 总结
其实本章需要熟练掌握的只有几个函数——fork、exec系列、_exit、wait和waitpid,这几个简单的函数在UNIX的进程控制中起到了至关重要的作用。
我是赵不贪,喜欢写点有的没的,长按扫码关注我
原文始发于微信公众号(梦真日记):《APUE》 – Chapter8 进程控制
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/167886.html