Chapter 8 进程控制
1.进程标识符
要点:
1).每个进程都有一个非负整型表示的唯一的进程ID。因为进程ID是一个总是唯一的,常将其用作其他标示符的一部分以保证其唯一性。例如:应用程序有时包含进程ID作为文件名的一部分,来产生唯一的文件名。
2).虽然唯一,但是进程ID可以重用
3).进程ID 0通常是调用进程,常常称为对换程序(swapper)。在硬盘上没有对应于这个进程的程序,它是内核的一部分而被熟知为一个系统进程。进程ID 1通常是init进程,在启动过程结束后被内核调用。
下列函数返回这些标识符:
#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函数
一个存在的进程可以调用fork函数来创建一个新的进程
#include <unistd.h> pid_t fork(void); //在子进程里返回0,在父进程里返回子进程ID,错误返回-1。
1).由fork函数创建的新进程被称为子进程。函数被调用一次却被返回两次。返回的唯一的区别是子进程的返回值是0,而父进程的返回值是新的子进程的ID。
2).子进程和父进程都持续执行fork调用之后的指令。子进程是父进程的一个复制品。例如,子进程得到父进程数据空间,堆和栈的拷贝。注意这对一个子进程来说是一个拷贝。父进程和子进程没有共享内存。父进程和子进程共享代码段。
3).当前实现不执行一个父进程数据、栈和堆的完全拷贝,因为fork后通常接着一个exec。(详见COW技术)
在fork之后有两种处理描述符的普通情况:
4).父进程等待子进程完成。在这种情况下,父进程不用对它的描述符做任何事情。当子进程终止时,子进程写过或读过的任何共享的描述符都有相应地更新它们的偏移量。
5).父进程和子进程独立工作。这里,在fork之后,父进程关闭它不需要的描述符,而子进程做同样的事。这样,两者都不干涉另一个打开的描述符。这种情景通常是网络服务的情况。
有两种fork的使用:
6).当一个进程想复制它自己以便父进程和子进程可以同一时间执行不同的代码段
7).当一个进程想执行一个不同的程序时。这对shell很普遍。
3.vfork函数
1).vfork函数倾向于创建一个新的进程,当这个新进程的目的是exec一个新的程序时。vfork函数创建这个进程,就像是fork一样,并不把父进程的地址空间拷贝到子进程里,因为子进程不会引用那个地址空间。子进程在vfork后马上简单地调用exec(或exit)。相反,当子进程在运行时并直到它调用exec或exit,子进程在它父进程的地址空间运行。这种优化在UNIX的一些换页虚拟内存实现上提供了一个效率上的收获。(如我们在上一节看到的,实现用写时复制来提高fork之后紧接exec的效率,然而完全不拷贝仍然比做一些拷贝要快。)
2).两个函数之间的另一个区别是,vfork保证子进程先运行,直到子进程调用exec或exit。当子进程调用这些函数的任一个时,父进程恢复执行。(如果子进程在调用这两个函数的任一个之前依赖于父进程更多的操作,这可能会造成死锁。)
4.exit函数
一个进程的5种退出方式:
1).从main函数里执行一个return。
2).调用exit函数。这个函数由ISO C定义并包含调用所有通过调用atexit注册的退出处理器,和关闭所有的标准I/O流。因为ISO C不处理文件描述符、多进程(父进程和子进程)、和工作控制,所以这个函数的定义对于UNIX系统来说是不完全的。
3).调用_exit或_Exit函数。ISO C定义_Exit为一个进程提供不运行exit处理器或信号处理器的终止方法。标准I/O流是否被冲洗取决于实现。在UNIX系统上,_Exit和 _exit是同义词,并不冲洗标准I/O流。_exit函数被exit函数调用并处理UNIX系统相关的细节
4).在进程的最后一个线程的启动例程里执行一个return。但是这个线程的返回值不被用途进程的返回值。当最后一个线程从它的启动例程里返回时,进程以终止状态0退出。
5).在进程的最后一个线程里调用ptherad_exit函数。和上一种情况一样,进程的返回状态始终为0,而不管传递给pthread_exit的参数。我们将在11.5节看到更多关于pthread_exit的细节
以下是三种异常退出的形式:
1)调用abort。这是下一项的特殊情况,因为它产生SIGABRT信号。
2).当进程收到特定信号时。(我们在第10章描述信号。)系统可以由进程本身产生--例如,通过调用abort函数--由别的进程产生,或者由内核产生。由内核产生信号的例子包括进程引用一块不在其地址空间的内存地址,或者尝试除以0。
3).最后一个线程响应一个取消请求。默认情况下,取消发生在一个延后的行为:一个线程请求另一个取消,在一段时间后,目标线程终止。
5.wait和waitpid函数
一个调用wait或waitpid的进程会:
- 阻塞,如果它所有的子进程都还在运行;
- 如果一个子进程终止并等待它的终止状态被获取,则随着子进程的终止状态立即返回;
- 如果它没有任何子进程,则立即返回一个错误。
如果进程是因为收到SIGCHLD信号而正调用wait,那么我们预期wait会立即返回。但是如果我们在任何随机的时间点时调用它,那么它会阻塞。
#include <sys/wait.h> pid_t wait(int *statloc); pit_t waitpid(pid_t pid, int *statloc, int options); //两者成功返回进程ID,失败返回0或-1
这两个函数的区别如下:
- wait函数可以阻塞调用者,直到一个子进程终止,而waitpid有选项可以避免它阻塞;
- waitpid函数不等待最先终止的子进程;它有许多选项来控制进程等待哪个进程
PS:如果一个子进程已经终止并成为一个僵尸,那么wait会用子进程的状态立即返回。否则,它会阻塞调用者,直到子进程终止。如果调用者阻塞并有多个子进程,那么wait当某个进程终止时返回。我们总是可以知道哪个子进程终止了,因为这个函数返回这个进程ID。
waitpid函数提供了三个不被wait函数提供的特性:
1).waitpid函数让我们等待一个特定的进程,而wait函数返回任何终止的子进程的状态。
2).waitpid函数提供了一个wait的非阻塞版本。有时我们想得到子进程的状态,而不想阻塞。
3).waitpid函数用WUNTRACED和WCONTINUED选项提供了对工作控制的支持。
6.waitid函数
waitid允许一个进程指定要等待哪些子进程。不是把这个信息和进程ID或进程组ID一起合并到单个参数里,而是使用两个单独的参数,参数选项详见APUE
#include <sys/wait.h> int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options); //成功返回0,失败返回-1
7.竞争条件
1).竞争条件当多个进程尝试用共享数据时发生,而最终的结果取决于进程运行的顺序,fork函数被调用时候容易产生竞争条件
2).对于一个父进程和子进程的关系,我们经常有以下的情景。在fork之后,父进程和子进程都有一些事情做。例如,父进程可能利用子进程ID更新一个日志文件的记录,而子进程可能必须为父进程创建一个文件。
8.exec函数
fork的一个用法是创建一个新进程(子进程)然后调用某个exec函数然执行另一个程序。当一个进程调用某个exec函数时,这个进程被新程序完全取代,而新程序开始执行它的main函数。在调用exec时进程的ID并没有发生变化,因为没有一个新的进程被创建;exec只是把当前的进程--它的代码、数据、堆和栈--替换为从硬盘而来的全新的程序。
6个不同的exec函数
#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[]); //六个函数错误都返回-1,成功不返回。
6个exec函数的区别 | ||||||
函数 | pathname | filename | 参数列表 | argv[] | environ | envp[] |
execl | * | * | * | |||
execlp | * | * | * | |||
execle | * | * | * | |||
execv | * | * | * | |||
execvp | * | * | * | |||
execve | * | * | * | |||
名字里的字母 | p | l | v | e |
9.更改用户ID和组ID
1).可以用setuid函数来设置真实用户ID和有效用户ID。相似地,我们可以用setgid函数设置真实组ID和有效组ID。
#include <unistd.h> int setuid(uid_t uid); int setgid(gid_t gid); //两者成功都返回0,失败返回-1.
- 如果进程有超级用户权限,那么setuid函数设置真实用户ID、用效用户ID和保留的设置用户ID。
- 如果进程没有超级用户权限,但是uid等于真实用户ID或保存的设置用户ID,那么setuid只把有效用户ID设置为uid。真实的用户ID和保存的设置用户ID不会改变。
- 如果这两个条件没有一个成立,那么errno被设为EPERM,并返回-1.
改变三个用户ID的方法 | ||||
ID | exec | setuid(uid) | ||
设置用户ID位关 | 设置用户ID位开 | 超级用户 | 没有特权的用户 | |
真实用户ID | 不变 | 不变 | 设置为uid | 不变 |
有效用户ID | 不变 | 从程序文件的用户ID拷贝 | 设置为uid | 设置为uid |
保存的设置用户ID | 从有效用户ID拷贝 | 从有效用户ID拷贝 | 设置为uid | 不变 |
2).setreuid和setregid函数
历史上,BSD支持使用setreuid函数来交换真实用户ID和有效用户ID。
#include <unistd.h> int setreuid(uid_t ruid, uid_t euid); int setregid(gid_t rgid, gid_t egid); //两者成功返回0,错误返回-1.
规则:一个没有权限的用户总是可以在真实用户ID和有效用户ID之间交换
3).seteuid和setegid函数
POSIX.1包含了两个函数seteuid和setegid。这些函数和setuid和setgid相似,但是只改变有效用户ID和有效组ID
#include <unistd.h> int seteuid(uid_t uid); int setegid(gid_t gid); //两者成功都返回0;错误返回-1.
PS:以上函数功能其实就是为了确保权限安全,防止违规用户进行违法操作
10.system函数
ISO C定义了system函数,但是它的操作有很强的系统依赖性。POSIX.1包含了system接口,基于ISO C定义展开来描述在一个POSIX环境里的它的行为。
#include <stdlib.h> int system(const char *cmdstring); //返回值如下。
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命>令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略
因为system实现中调用fork、exec和waitpid来,所以有三种返回值的类型:
1).如果fork失败或waitpid返回一个错误而不是EINTR,system返回-1,并设置errno为指定错误。
2).如果exec失败,暗示shell不能被执行,返回值就好像shell执行了exit(127)。
3).否则,所有三个函数--fork、exec和waitpid--成功,从system的返回值是外壳的终止状态,以waitpid指定的格式。一些 system的早期实现返回一个错误(EINTR),如果waitpid被一个捕获到的信号中断。因为没有一个程序可以使用的清理策略来从这种错误类型返回,POSIX随后加上需求,system在这种情况不返回一个错误
system代码实现见APUE
11.进程记账
多数UNIX系统提供了一个执行进程记帐的选项。当被启动时,内核每次在一个进程终止时写一个记帐记录。
记账记录的结构体定义在<sys/acct.h>头文件里,并看起来像:
typedef u_short comp_t; /* 3-bit base 8 exponent; 13-bit fraction */ struct acct { char ac_flag; /* flag (see following Figure) */ char ac_stat; /* termination status (signal & core flag only) */ /* Solaris Only */ uid_t ac_uid; /* real user ID */ gid_t ac_gid; /*real group ID */ dev_t ac_tty; /* controlling terminal */ time_t ac_btime; /* starting calendar time */ comp_t ac_utime; /* user CPU time (clock ticks) */ comp_t ac_stime; /* system CPU time (clock ticks) */ comp_t ac_etime; /* elapsed time (clock ticks) */ comp_t ac_mem; /* average memory usage */ comp_t ac_io; /* bytes transfered (by read and write) */ /* "blocks on BSD systems */ comp_t ac_rw; /* blocks read or written */ /* (not present on BSD systems) */ char ac_comm[8]; /* command name: [8] for Solaris, [10] for Mac OS X, [16] for FreeBSD, and [17] for Linux */ };
12.用户标识
系统通常记录用户登录时候使用的名字,用getlogin函数可以获取此登录名。
#include <unistd.h> char *getlogin(void); //成功返回登录名的指针,错误返回NULL。
如果调用此函数的进程没有连接着在一个用户登录时所用的终端,那么函数会失败。
13.进程时间
前面描述过三个可以测量的三种时间:挂钟时间、用户CPU时间和系统CPU时间。任一进程可以调用times函数来为它自己和任何终止的子程序来获得这些值。
#include <sys/times.h> clock_t times(struct tms *buf); //如果成功返回逝去的挂钟时间,错误返回-1
这个函数填写由buf指向的tms结构体,该结构体定义如下:
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 */ };