1.进程标识
每个进程都有一个非负整型表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性。进程ID是可复用的,当一个进程终止后,其进程ID就成为复用的候选者。
ID为0的进程通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,它不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1 通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX早期版本中是/etc/init,在较新版本中是/sbin/init。此进程负责在自举内核后启动一个UNIX系统。init 通常读取与系统有关的出事后文件(/etc/rc*文件或/etc/inittab文件,以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。init进程决不会终止。它是一个以超级用户特权运行的普通用户进程(不同于交换进程,它不是内核的系统进程)。init 会成为所有孤儿进程的父进程。
每个UNIX系统实现都有它自己的一套操作系统服务的内核进程,如在某些UNIX的虚拟存储器实现中,进程ID 2是页守护进程,此进程负责支持虚拟存储器系统的分页操作。
下面这些函数返回进程的相关标识:
#include <unistd.h>
pid_t getpid(void); 返回值:调用进程ID
pid_t getppid(void); 返回值:调用进程的父进程ID
uid_t getuid(void); 返回值:调用进程实际用户ID
uid_t geteuid(void); 返回值:调用进程有效用户ID
gid_t getgid(void); 返回值:调用进程实际组ID
gid_t getegid(void); 返回值:调用进程有效组ID
2.函数fork和vfork
一个现有的进程可以调用fork函数创建一个新进程。
#include <unistd.h>
pid_t fork(void);
pid_t vfork(void);
由fork创建的新进程被称为子进程,fork被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新建子进程的进程ID。
将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid获得其父进程的进程ID(进程ID 0 为内核交换进程使用,所有一个子进程的进程ID不可能为0)。
由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程的数据段、栈和堆的完全副本。作为替代,使用了写时复制(Copy-On-Write, COW)技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限修改为只读。如父进程或子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。
fork之后是由父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。
fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。且共享同一文件偏移量。
fork之后处理文件描述符有以下两种常见情况:
子进程会继承很多父进程的属性,包括:文件描述符、实际用户ID、实际组ID、有效用户ID、有效组ID、附属组ID、进程组ID、会话ID、控制终端、设置用户ID标志和设置组ID标志、当前工作目录、根目录、文件模式创建屏蔽字、信号屏蔽和安排、对任一打开文件描述符的执行时关闭(close-on-exec)标志、环境、连接的共享存储段、存储映像、资源限制
父子进程之间的区别具体如下:
fork失败的主要原因:
fork的两种用法:
vfork和fork一样都创建一个子进程,但它并不完全复制父进程的地址空间,因为子进程会立刻调用exec(或exit),于是也就不会引用该地址空间。不过在子进程调用exec和exit之前,它在父进程的空间中运行。
vfork与fork的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用者两个函数其中任意一个时,父进程会恢复运行。
3.函数exit
进程的5种正常终止及3种异常终止方式:
对于上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于3个终止函数(exit、_exit、_Exit),实现这一点的方法是,将其退出状态作为参数传送给函数。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait、waitpid函数取得其终止状态。
退出状态与终止状态有所区别。在最后调用_exit时,内核将退出状态转换成终止状态。
对于父进程已经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程收养。其操作过程大致是:一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。
如果子进程在父进程之前终止,父进程如何在做相应检查时得到子进程的终止状态呢?如果子进程完全消失了,父进程无法获取其终止状态。内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或者waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。
一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占有的资源)的进程被称为僵死进程。 ps命令将僵死进程的状态打印为 Z 。如果编写一个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,这些子进程终止后就会变成僵死进程。
init进程收养的进程终止时不会变成僵死进程,因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。
4.函数wait和waitpid
当一个进程正常或者异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是异步事件,所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号,系统的默认动作是忽略它。
调用wait和waitpid的进程可能发生:
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
这两个函数的区别如下:
两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,可将该参数指定为空指针。
waitpid根据参数pid等待一个特定的进程。options使得我们能进一步控制waitpid的操作。
POSIX.1规定,终止状态用定义在sys/wait.h中的各个宏来查看,有4个互斥的宏可用来取得进程终止的原因,它们的名字都以WIF开始。
宏 | 说明 |
---|---|
WIFEXITED | 返回真时表示子进程正常终止 |
WIFSIGNALED | 返回真时表示子进程收到信号而导致异常终止 |
WIFSTOPPED | 返回真时表示子进程处于停止状态 |
WIFCONTINUED | 返回真时表示子进程进入暂停后继续的状态 |
fork两次可以避免僵死进程。
第一个子进程fork后在第二个子进程之前终止,则第二个子进程会由init进程收养,然后用wait函数获取第一个子进程的终止状态防止其成为僵死进程。
5.函数waitid和wait3、wait4
Single UNIX Specification 包括了另一个获取进程终止状态的函数,即waitid,此函数类似于waitpid,但提供了更多的灵活性。
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
与waitpid相似,waitid允许一个进程指定要等待在子进程。但它使用两个独立的参数表示要等待的子进程所属的类型,而不是将此进程与进程ID或进程组ID组合成一个参数,id参数的作用与idtype的值相关。
常量 | 说明 |
---|---|
P_PID | 等待一特定进程:id包含要等待子进程的进程ID |
P_PGID | 等待一特定进程组中任一子进程:id包含要等待子进程的进程组ID |
P_ALL | 等待任一子进程:忽略id |
options参数是下列各标志的按位或运算。这些标志指示调用者关注哪些状态变化。
常量 | 说明 |
---|---|
WCONTINUED | 等待一子进程,它以前曾被停止,此后又已继续,但其状态尚未报告 |
WEXITED | 等待已退出的进程 |
WNOHANG | 如无可用的子进程退出状态,立即返回而非阻塞 |
WNOWAIT | 不破坏子进程退出状态。该子进程的退出状态可由后续的wait函数获取 |
WSTOPPED | 等待一子进程,它已经停止,但其状态尚未报告 |
WCONTINUED、WEXITED或WSTOPPED这3个常量之一必须在options参数中指定。
wait3和wait4函数比起wait、waitpid和waitid提供的功能多一个。这与附加参数有关。该参数允许内核返回由终止进程及其所有子进程使用的资源概况。资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接受到信号的次数等。
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
6.竞争条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则认为发生了竞争条件。
7.函数exec
fork函数创建新的子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
有7种不同的exec函数可供使用,它们常常被统称为exec函数,可以使用这7个函数中的任一个。这些exec函数使得UNIX系统进程控制原语更加完善。用fork可以创建新进程,用exec可以初始执行新的程序。exit函数和wait函数处理终止和等待终止。这些是我们需要的基本的进程控制原语。使用这些原语可构造另外一些如popen和system之类的函数。
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
8.更改用户id和更改组id
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
int seteuid(uid_t uid);
int setegid(gid_t gid);
设置不同用户ID的各函数如下图所示:
9.解释器文件
所有现今的UNIX系统都支持解释器文件(interpreter file )。这种文件是文本文件,其起始行的形式是:
#! pathname [optional-argument]
pathname通常是绝对路径名,对它不进行什么特殊的处理(不使用PATH进行路径搜索)。对这种文件的识别是由内核作为exec系统调用处理的一部分来完成的。内核使调用exec函数的进程实际执行的并不是该解释器文件,而是在该解释器文件第一行中pathname所指定的文件。一定要将解释器文件(文本文件,它以#!开头)和解释器(由该解释器文件第一行中的pathname指定)区分开来。
很多系统对解释器文件第一行有长度限制。这包括#!、pathname、可选参数、终止换行符以及空格数。
10.函数system
这个函数使用/bin/sh执行指定的命令串执行标准的shell命令。形如:
$?/bin/sh?-c?cmdstring
#include <stdlib.h>
int system(const char *cmdstring);
应注意的是,设置了SetUID 或 SetGID 的程序不应使用 system 函数。另外,作为服务器程序时,也不应使用system 处理客户程序提供的字符串参数,以避免恶意用户利用 shell 中的特殊操作符进行越权操作。
使用system而不是直接使用fork和exec的优点是:system进行了所需的各种出错处理以及各种信号处理。
11.进程调度
UNIX系统历史上对进程提供的只是基于调度优先级的粗粒度的控制。调度策略和调度优先级是由内核确定的。进程可以通过调整nice值选择以更低优先级运行(通过调整nice值降低它对CPU的占有,因此该进程是“友好的”)。只有特权进程允许提高调度权限。
#include <unistd.h>
int nice(int incr);
incr参数被增加到调用进程的nice值上。如果incr太大,系统直接把它降到最大合法值,不给出提示。类似地,如果incr太小,系统也会无声息地把它提高到最小合法值。由于-1是合法的成功返回值,在调用nice函数之前需要清楚errno,在nice函数返回一1时,需要检查它的值。如果nice调用成功,并且返回值为-1,那么errno仍然为0。如果errno不为0,说明nice调用失败。
getpriority函数可以像nice函数那样用于获取进程的nice值,但是getpriority还可以获取一组相关进程的nice值。setpriority函数可用于为进程、进程组和属于特定用户ID的所有进程设置优先级。
#include <sys/resource.h>
int getpriority(int which, id_t who);
int setpriority(int which, id_t who, int value);
12.进程时间
我们可以度量的3个时间:墙上时钟时间、用户CPU时间和系统CPU时间。任一进程都可调用times函数获得它自己以及己终止子进程的上述值。
#include <sys/times.h>
clock_t times(struct tms *buf);
struct tms {
clock_t tms_utime; /* user CPU time */
clock_t tms_stime; /* system CPU time */
clock_t tms_cutime; /* user CPU time, terminated children */
clock_t tms_cstime; /* system CPU time, terminated children */
};