第八章 进程控制
1、进程标识符 PID 的概念
A. 进程 ID(PID)唯一的标识了系统中的当前进程;
B. 已结束的进程,其 PID 以后将给信的进程使用,但一般不是马上;
C. 0 号进程(PID == 0)是内核的一部分,属于系统进程,其它进程均属于用户进程;
D. 1 号进程通常是 init,是一个以 root 特权运行的系统进程,孤儿进程都将由 init 进程接管;
E. 获取当前进程一些相关标识符的 API:
#include <unistd.h>
pid_t getpid(void); /* 返回当前的 PID */
pid_t getppid(void); /* 返回父进程的 PID */
uid_t getuid(void); /* 返回进程的 UID */
uid_t geteuid(void); /* 返回进程的 EUID */
gid_t getgid(void); /* 返回进程的 GID */
gid_t getegid(void); /* 返回进程的 EGID */
注意:以上函数都没有出错返回。
2、fork(2)函数
#include <unistd.h>
pid_t fork(void);
A. fork(2)生成一个新的进程,该进程是以当前进程的上下文为依据生成的子进程;fork 返回 0 表示当前处于子进程中,而在父进程中则返回所生成的子进程的PID,失败时返回-1;
B. 父进程和子进程的 text 段是共享的,但子进程实际上是从执行 fork 之后的代码段开始执行的;bss 段、堆、栈等在现代的系统中通常使用写时复制(COW,copy-on-write)的技术;
C. fork 之后是父进程还是子进程先被运行一般是不可知的。对于 Linux,为避免父进程先执行引起不必要的 COW(因为很多时候子进程将很快执行 exec(3)),生成新进程时曾试图使子进程先于父进程执行(参考 Understanding Linux Kernel 的“3.4.1.1. The do_fork( ) function”一节)。观察Linux 2.6.x 进程创建的相关源码,在版本 2.6.22 之前我们可以看到曾经存在这样一行注释(位于 kernel/sched.c 的 wake_up_new_task 函数中):
/*
* The VM isn't cloned, so we're in a good position to
* do child-runs-first in anticipation of an exec. This
* usually avoids a lot of COW overhead.
*/
但Linux Kernel Development 一书指出,这并非总能如此。事实上,在2.6.23 采用了新的进程调度机制以后,同时把 child-runs-first的技术放弃了。
D. 子进程生成时复制了父进程打开的文件描述符,包括 stdin,stdout、stderr,应注意对这些资源的共享可能引起并发问题;
E. 对于书中的程序清单 8-1,编译后的程序在命令行下直接执行和重定向到文件中执行,字符串"before fork\n"在前者只输出一次而后者输出了两次的原因是:前者的stdout是字符设备tty,默认使用行缓冲,子进程生成之前 stdout 的缓冲区已经被由于输出带换行符而进行了冲洗;而后者的stdout是一个普通文件,默认使用全缓冲,子进程生成时尚未通过输出冲洗的stdout缓冲区通过 fork 复制给了子进程,父进程和子进程退出时 exit(3)对缓冲区进行冲洗时输出了各自的"before fork\n";
F. 父子进程的主要区别包括:
- fork 的返回值、PID、PPID 不同;
- 子进程的 tms、utime 等相关时间统计信息在 fork()后被清零;
- 子进程不继承父进程的记录锁;
- 子进程的未决闹钟定时(alarm(2))将被清除;
- 子进程不继承父进程的未决信号集;
G. 关于 vfork(2)函数
- 该函数是专为子进程生成后不需要父进程的进程数据而直接执行 exec 而设计的,它只生成子进程,但永不拷贝父进程内存区域的数据,而且父进程将一直阻塞到子进程执行了 execve(2)或者_exit(2)调用为止。
- 事实上,在 Linux 中,vfork(2)和 fork(2)都使用了 clone(2)系统调用,但使用不同的参数。Linux 的手册页指出了 vfork(2)是为避免 fork(2)的实现未必使用了 copy-on-write 技术而为降低子进程生成的代价而实现的,手册页同时指出了vfork(2)的标准描述与Linux 语境下的描述,及其历史描述。
- 使用 vfork(2)时应注意:防止子进程因依赖父进程引起的死锁,特别是子进程并不继承父进程的记录锁,这时使用父进程打开的文件时可能会被阻塞。
- 如果在子函数中调用 vfork(2),从子函数中返回后再进行其它处理,这种情况下子进程可能将修改父子进程所共享的栈,从而将带来副作用。
#include <sys/wait.h>
pid_t wait(int *status);
wait 函数阻塞等待直到有一个子进程退出,并将相关状态记录到 status 处,返回该子进程的PID,出错时返回-1(例如不存在子进程);
- WIFEXITED():返回真时表示子进程正常终止;
- WIFSIGNALED():返回真时表示子进程收到信号而导致异常终止;
- WIFSTOPPED():返回真时表示子进程处于停止状态;
- WIFCONTINUED():返回真时表示子进程进入暂停后继续的状态。
- WTERMSIG():返回导致子进程终止的信号;
- WCOREDUMP():返回真时表示子进程异常终止并导致了内核转储
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
waitpid 扩展了 wait 的功能,它可以指定子进程的 PID,并设置相关阻塞选项。
- -1:等待任何子进程;
- 正整数:等待指定 pid 的子进程;
- 0:等待其进程组 GID 等于当前进程组 GID 的任意子进程;
- 负整数:等待其进程组 GID 等于 abs(pid)的任意子进程;
- WCONTINUED:等待到子进程的状态从暂停变为继续,但未报告时取其状态;
- WNOHANG:不阻塞并返回 0;
- WUNTRACED:等待到子进程变成暂停状态,但未报告时取其状态。
waitpid(-1, &status, 0);
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
waitid 的参数 idtype 包括:
- P_PID:等待指定的进程,此时参数 id 表示所等待的子进程 PID;
- P_PGID:等待指定的进程组中的子进程,此时参数 id 表示进程组 GID;
- P_ALL:等待任何子进程,此时参数 id 被忽略;
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resources.h>
#include <sys/wait.h>
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
这两个函数用于等待并收集子进程及其所有子进程的全部资源信息到rusage 中。对Linux 而言,wait4 是 wait 家族各个函数的系统调用入口,其它几个函数都可以基于 wait4 重新实现。
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ..., /* (char *)0 */);
int execlp(const char *filename, const char *arg, ..., /* (char *)0 */);
int execle(const char *pathname, const char *arg0, ..., /* (char *)0, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *filename, char *const argv[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
这些函数的第一个参数为要装入的程序,后面的参数为该程序执行时的命令行参数表。函数执行失败时将返回-1并设置 errno,成功后不会在原来程序的 main 中返回;
#include "apue.h"
int main(void)
{ char *arg[] =
{"uname", "-r"};
if (execv("/bin/uname", arg) < 0)
{
perror("execv");
exit(EXIT_FAILURE);
} exit(EXIT_SUCCESS);
}
在 Linux 下编译执行:
mjxian@fedora ~/apuetest
$ uname
Linux
mjxian@fedora ~/apuetest
$ cc -o execvinFedora testexecv.c && ./execvinFedora
execv: Bad address
mjxian@fedora ~/apuetest
$ echo $?
1
而在Solaris 下编译执行:
mjxian@t1000 ~/apuetest
$ uname
SunOS
mjxian@t1000 ~/apuetest
$ cc -o execvinSolaris testexecv.c && ./execvinSolaris
5.10
mjxian@ t1000 ~/apuetest
$ echo $?
0
在 linux 下,要成功执行 uname -r这个命令,exec家族各个函数的例子为:
char *arg[] = {"uname", "-r", NULL};
execv("/bin/uname", arg);
execl(3):
execl("/bin/uname", "uname", "-r", NULL);
execve(2):
char *arg[] = {"uname", "-r", NULL};
char *env[] = {NULL};
execve("/bin/uname", arg, env);
execvp(3):
char *arg[] = {"uname", "-r", NULL};
execvp("uname", arg);
execle(3):
char *env[] = {NULL};
execle(“/bin/uname", "uname", "-r", NULL, env);
exelp(3):
execlp("uname", "uname", "-r", NULL);
关于 exec 家族函数的更多细节可以参考 Understanding Linux Kernel, 3rd Edition 的“20.4. The exec Functions” 。
#! cmd
"#!"必须顶格(即位于行首),cmd 之前可以有空格。如果#!不顶格或者没有这行声明,则以当前shell 来执行此文件;
对一个解释器文件进行 exec(3)函数调用时,指定的交互式命令所收到的参数依次为:#!后面的命令串、exec(3)函数指定的可执行文件,exec(3)函数指定的参数表;
#include <unistd.h>
int setuid(uid_t suid);
int setgid(gid_t sgid);
进程中调用函数 setuid(2)时:
- 如果是 root 进程,将会同时改变进程的 UID、EUID 和备份 EUID(在 setuid(2)成功时之前的EUID 将备份到内核中的进程表,但 Unix 没有提供接口函数用于读取其值)为参数 suid
- 非 root 进程,在参数 suid 为 EUID 或者备份 EUID 时,则将进程的 EUID 改为 suid;
- 不符合这些条件的,调用 setuid(2)时将返回-1 并置 errno 为 EPERM;
- 在不需要 SetUID 带来的权限时,使用 setuid(getuid())降低 EUID 的特权;
- 在 setuid(2)之前,宜通过 geteuid(2)拿当前的 EUID 作备份,完成所需的工作时,再通过setuid(euid_backup)恢复;
- 在子进程执行 exec(3)之前,应 setuid(getuid())以避免 SetUID 的进程传递特权;
#include <stdlib.h>
int system(cont char *cmdstring);
这个函数使用/bin/sh执行指定的命令串执行标准的shell 命令。形如:
$ /bin/sh -c cmdstring
应注意的是,设置了 SetUID 或 SetGID 的程序不应使用 system 函数。另外,作为服务器程序时,也不应使用 system 处理客户程序提供的字符串参数,以避免恶意用户利用 shell 中的特殊操作符进行越权操作。
#include <sched.h>
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *p);
int sched_getscheduler(pid_t pid);
int sched_get_priority_max(int policy);
int sched_get_priority_min(int policy);
int getpriority(int which, int who);
int setpriority(int which, int who, int prio);
int nice(int inc);
sched_setscheduler (2)和 sched_getscheduler(2)分别设置和取得与某个特定进程相关的策略和参数。策略 policy 包括SCHED_OTHER、 SCHED_FIFO、SCHED_RR。后两者是用于特别看重时间的策略,会抢先于使用默认策略SHED_OTHER 的进程执行;