8.1 引言算法
本章介绍UNIX的进程控制,包括建立新进程、执行程序和进程终止。还将说明进程属性的各类ID——实际、有效和保护的用户和组ID,以及它们如何受到进程控制原语的影响。本章还包括了解释器文件和system函数。本章最后讲述大多数UNIX系统所提供的进程会计机制。这种机制使咱们能从另外一个角度了解进程的控制功能。shell
8.2 进程标志符数组
每一个进程都有要给非负整数表示惟一进程ID,虽然进程ID是惟一的,可是能够重用。当一个进程终止后,其进程ID就能够再次使用了。大多数UNIX系统实现延迟重用算法,使得赋予新建进程的ID不一样于最近终止进程所使用的ID。这防止了将新进程误认为是使用同一ID的某个已终止的先前进程。安全
系统中有一些专用进程,但具体细节因实现而异。ID为0的进程一般是调度进程,经常被称为交换进程。该进程是内核的一部分,它并不执行任何磁盘上的程序,所以也被称为系统进程。进程ID 1 一般是init 进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX的早期版本中是/etc/init,在较新版本中是 /sbin/init。此进程负责在自举内核后启动一个UNIX系统。init 一般读与系统有关的初始化文件,并将系统引导到一个状态(例如多用户)。init进程决不会终止。它是一个普通的用户进程(与交换进程不一样,它不是内核中的系统进程),可是它以超级用户特权运行。本章稍后部分会说明 init 如何称为全部孤儿进程的父进程。网络
每一个UNIX 系统实现都有它本身的一套提供操做系统服务的内核进程,例如,在某些UNIX的虚拟存储器实现中,进程ID 2 是页守护进程。此进程负责支持虚拟存储系统的分页操做。编辑器
除了进程 ID,每一个进程还有一些其余的标识符。下列函数返回这些标识符。函数
pid_t getpid(void); pid_t getppid(void);
uid_t getuid(void); // 返回进程的实际用户ID uid_t geteuid(void); // 返回进程的有效用户ID gid_t getgid(void); gid_t getegid(void);
注意,这些函数都没有出错返回,在下一节中讨论 fork 函数时,将进一步讨论父进程 ID。测试
8.3 fork函数优化
一个现有进程能够调用 fork 函数建立一个新进程ui
pid_t fork(void);
由 fork 建立的新进程被称为子进程。fork函数被调用一次,但返回两次。子进程的返回值是0,父进程的返回值是新子进程的进程 ID。
子进程和父进程继续执行 fork 调用以后的指令,子进程是父进程的副本。例如,子进程得到父进程的数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间部分。父子进程共享正文段(见7.6节)
因为在 fork 以后常常跟随着 exec,因此如今的不少实现并不执行一个父进程数据段、栈和堆彻底复制。做为替代,使用写时复制技术。这些区域由父、子进程共享,并且内核将它们的访问权限改变为只读的。若是父、子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制做一个副本,一般是虚拟存储器系统中的一“页”。
通常来讲,在 fork 以后父子进程执行的现后顺序是不肯定的。若是要求父子进程之间相互同步,则要求某种形式的进程间通讯。
文件共享
fork的一个特性是父进程的全部打开文件描述符都被复制到子进程中。父、子进程的每一个相同的打开描述符共享一个文件表项(如图3-3)。
考虑下述状况,一个进程具备三个不一样的打开文件,他们是标准输入、标准输出和标准出错。在从fork返回时,咱们有了下图。
这种共享方式使父、子进程对同一个文件使用了一个文件偏移量。这样就能够实现父子进程交互写同一个文件。
若是父、子进程写到同一描述符文件,但又没有任何形式的同步,那么它们的输出就会相互 混合(假定所用的描述符是在fork以前开打的)。虽然这种状况是可能发生的,但这并非经常使用的操做模式。
在fork以后处理文件描述符有两种常见的状况:
(1)父进程等待子进程完成。在这种状况下,父进程无需对其描述符作任何处理。当子进程终止后,它曾进行读、写操做的任一共享描述符的文件偏移量已执行了相应更新。
(2)父、子进程各自执行不一样的程序段。在这种状况下,在fork以后,父、子进程各自关闭它们不须要使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种是网络服务进程中常用的。
除了打开文件以外,父进程的不少其余属性也由子进程继承,包括
(1)实际用户ID、实际组ID、有效用户ID、有效组ID
(2)附加组ID
(3)进程组ID
(4)会话ID
(5)控制终端
(6)设置用户ID标志和设置组ID标志
(7)当前工做目录
(8)根目录
(9)文件模式建立屏蔽字
(10)信号屏蔽和安排
(11)针对任一打开文件描述符的在执行时关闭标志。
(12)环境
(13)链接的共享存储段
(14)存储映射
(15)资源限制
父子进程之间的区别是:
(1)fork的返回值
(2)进程ID不一样
(3)两个进程具备不一样的父进程ID
(4)进程的 tms_utime、tms_stime、tms_cutime以及tms_ustime均被设置为0。
(5)父进程设置的文件锁不会被子进程继承。
(6)子进程的未处理闹钟(alarm)被清除
(7)子进程的未处理信号集设置为空集。
使fork失败的主要缘由是进程太多了。
fork有两种用法:
(1)一个父进程但愿复制本身,使父、子进程同时执行不一样的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种状况到达时,父进程调用 fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
(2)一个进程要执行一个不一样的程序。这对shell是常见的状况。在这种状况下,子进程从fork返回后当即调用 exec。
某些操做系统将(2)中的两个操做(fork、exec)组合成一个,并称其为spawn。UNIX将这两个操做分开,使得子进程在 fork 和 exec 之间能够更改本身的属性。例如 I/O 重定向、用户ID、信号安排等。在15章中有不少这方面的例子。
8.4 vfork函数
vfork函数的调用序列和返回值与fork相同,但二者的语义不一样。
vfork被认为有瑕疵,应被弃用。
vfork用于建立一个新进程,而该新进程的目的是exec一个新程序。因为vfork不会将父进程的地址空间彻底复制到子进程中,由于子进程会当即调用exec或exit,因而就不会访问该地址空间。相反,在子进程调用 exec 或 exit 以前,他在父进程的空间中运行。这种优化工做方式在某些UNIX的页式虚拟存储器实现中提升了效率。(这与上一节中说起的在fork以后跟随exec,并采用在写时复制技术类似,并且不复制比部分复制要更快一些。)
vfork和 fork之间的另外一个区别是:vfork保证子进程先运行,在它调用exec或exit以后父进程才可能被调度运行。(若是在调用这两个函数以前子进程依赖于父进程的进一步动做,则会致使死锁)。
int glob = 6; int main(void) { int var; pid_t pid; var = 88; printf("before vfork\n"); if ((pid = vfork()) < 0) { err_sys("vfork error"); } else if (pid == 0) { glob++; var++; _exit(0); } printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var); exit(0); }
子进程对变量 glob 和 var 作增1操做,结果改变了父进程中的变量值。由于子进程在父进程的地址空间中运行,因此这并不使人惊讶。可是其做用的确与 fork 不一样。
注意,在程序清单中,调用了 _exit 而不是 exit。正如7.3节所述, _exit并不执行标准 I/O 缓冲的冲洗操做。若是调用的是exit而不是 _exit,则程序的输出是不肯定的。它依赖标准 I/O 库的实现。
若是该实现也关闭标准 I/O 流,那么表示那么标准输出 FILE 对象的相关存储区将被清 0 。注意,父进程的 STDOUT_FILENO 仍旧有效,子进程获得的是父进程的文件描述符数组的副本。但因为没有缓冲区,因此父进程调用 printf 时不会产生任何输出。
8.5 exit函数
如7.3节所述,进程有下面5中正常终止方式:
(1)main函数内return,等效于调用 exit。
(2)调用 exit 函数。其操做包括调用各终止处理程序(终止处理程序在调用 atexit 函数时登记),而后关闭全部标准 I/O 流等。由于 ISO C 并不处理文件描述符、多进程(父、子进程)以及做业控制,因此这必定义对UNIX系统而言是不完整的。
(3)调用 _exit 或 _Exit 函数。其存在的目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。在 UNIX 中, _Exit 和 _exit 是同义的,并不清洗标准 I/O 流。_exit函数有 exit 调用。
(4)进程最后一个线程在启动例程中执行返回语句。可是,该线程的返回值不会用做进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。
(5)进程的最后一个线程调用 pthread_exit 函数。如同前面同样,在这种状况下,进程终止状态老是0,这与传送给 pthread_exit 的参数无关。
三种异常终止方式以下
(1)调用 abort。它产生 SIGABORT 信号,这是下一种异常终止的一种特例。
(2)当进程接受到某些信号时。信号可由进程自身(例如调用abort函数)、其余进程或内核产生。例如,进程越出其余地址空间访问存储单元或者除以0,内核就会为该进程产生相应的信号。
(3)最后一个线程对“取消”请求作出响应。按系统默认,“取消”以延迟方式发生:一个线程要求取消另外一个线程,一段时间后,目标线程终止。
无论进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭全部打开描述符,释放它所使用的存储器等。
对于上述任意一种情形,咱们都但愿终止进程可以通知其父进程它是如何终止的。对于三个终止函数(exit、_exit和_Exit),实现这一点的方法是,将其退出状态做为参数传给函数。在异常终止状况下,内核(不是进程自己)产生一个指示其异常终止缘由的终止状态。在任意一种状况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
注意,这里使用“退出状态”(它是传向exit或 _exit 的参数,或 main 的返回值)和“终止状态”两个术语,以表示有所区别。在最后调用_exit时,内核将退出状态换成终止状态(回忆图7-1)。下一节中的表8-1说明父进程检查子进程终止状态的不一样方法。若是子进程正常终止,则父进程能够得到 子进程的退出状态。
当子进程退出后,将其终止状态返回给父进程,可是若是父进程提早终止,那么init成为父进程。其操做过程大体以下:在一个进程终止时,内核逐个检查全部活动进程,以判断它是否正要终止进程的子进程,若是是,则将该进程的父进程ID更改成1(init进程的ID)。这种处理方法保证了每一个进程都有一个父进程。
另外一个咱们关系的状况是若是子进程在父进程以前终止,那么父进程又如何能在作相应及检查时获得子进程的终止状态呢?对此问题的回答是:内核为每一个终止子进程保持了必定量的信息,因此当终止进程的父进程调用 wait 或 waitpid 时,能够获得这些信息。这些信息至少包括进程 ID、该进程的终止状态、以及该进程使用 CPU 时间总量。内核能够释放终止进程所使用的全部存储区,关闭其全部打开文件。在UNIX术语中,一个已经终止,可是其父进程还没有对其进行善后处理(获取终止子进程的有关信息,释放它仍占用的资源)的进程被称为僵死进程(zombie)。ps(1)命令将僵死进程的状态打印为 Z。若是编写一个长期运行的程序,它调用 fork 产生了不少子进程,那么除非父进程等待取得子进程的终止状态,不然这些子进程终止后就会变成僵死进程。
最后一个要考虑的问题是:一个由Init进程领养的进程终止时会发生什么?它会不会变成一个僵死进程?答案是“否”,由于Init被编写成不管什么时候只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。
8.6 wait和waitpid函数
当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHILD 信号。父进程能够选择忽略,或者捕捉,对于这种信号的系统默认动做是忽略它。
如今须要知道的是调用 wait 或 waitpid的进程可能发生什么状况:
(1)若是其全部子进程都还在运行,则阻塞。
(2)若是一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态当即返回。
(3)若是他没有任何子进程,则当即出错返回
pid_t wait(int *statloc); pid_t waitpid(pid_t pid, int *statloc, int options); 两个函数的返回值:若成功则返回进程ID,0(见后面说明),若出错则返回-1
两个函数的区别以下:
(1)waitpid能够设置不阻塞。
(2)waitpid并不等的在其调用以后的第一个终止子进程,它有若干个选项,能够控制它所等待的进程。
对于 statloc 用于得到返回的状态,其中某些为表示退出状态(正常返回),其余位则指示信号编号(异常返回),有一个位指示是否产生一个core文件等。使用下列的宏来查看
void pre_exit(int status) { if (WIFEXITED(status)) printf("normal termination, exit status = %d\n", WEXITSTATUS(status)); else if (WIFSIGNALED(status)) printf("abnormal termination, signal number = %d%s\n", WTERMGIS(status)), #ifdef WCOREDUMP WCOREDUMP(status) ? " (core file generated)" : ""); #else ""); #endif else if (WIFSTOPPED(status)) printf("child stopped, signal number = %d\n", WSTOPSIG(status)); }
对于 waitpid 的函数中 pid 参数的做用解释以下:
pid == -1 等待任一子进程。与 wait 等效。
pid > 0 等待其进程ID == pid 的子进程
pid == 0 等待其组ID等于调用进程组ID的任一子进程
pid < -1 等待其组ID等于pid绝对值的任一子进程。
对于 wait ,其惟一出错是调用进程没有子进程(函数调用被一个信号中断时,也可能返回另外一种出错。第10章讨论)。可是对于 waitpid,若是指定的进程或进程组不存在,或者参数pid指定的进程不是调用进程的子进程则都将出错。
options参数使咱们能进一步控制waitpid的操做。此参数能够是0。或者按照下表常量位或运算的结果。
waitpid函数提供了wait函数没有提供的三个功能:
(1)waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。
(2)waitpid提供了一个wait的非阻塞版本。
(3)waitpid支持做用控制(利用 WUNTRACED 和 WCONTINUED 选项)。
若是一个进程fork一个子进程,但不要它等待子进程终止,也不但愿子进程处于僵死状态直到父进程终止,实现这一要求的技巧是调用 fork 两次。(本质上就是让 init 进程管理孙子进程)
// 调用 fork 两次以免僵死进程 int main(void) { pid_t pid; if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { if ((pid = fork()) < 0) err_sys("fork error"); else if (pid > 0) exit(0); sleep(2); printf("second child, parent pid = %d\n", getppid()); exit(0); } if (waitpid(pid, NULL, 0) != pid) err_sys("waitpid error"); exit(0); }
8.9 竞态条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序,则咱们认为这发生了竞态条件。
在父、子进程的关系中,经常出现下述状况,在调用 fork 以后,父、子进程都有一些事情要作。例如,父进程可能要用子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程建立一个文件,在本例中,要求每一个进程在执行完它的一套初始化操做后要通知对方,而且在继续运行以前,要等待另外一方完成其初始化操做。这种方案能够用代码描述以下:
TELL_WAIT(); // set thing up for TELL_XXX & WAIT_XXX if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { // child // child does whatever is necessary .. TELL_PARENT(getppid()); //tell parent we're done WAIT_PARENT(); // and wait for parent // and the child continues on its way ... exit(0); } // parent does whatever is necessary ... TELL_CHILD(pid); // tell child we're done WAIT_CHILD(); // and wait for child // and the parent continues on its way... exit(0);
TELL_WAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT、WAIT_CHILD能够是宏,也能够是函数。
在后面几章会说明实现这些TELL和WAIT例程的不一样方法:10.16节中说明使用信号的一种实现,程序清单15-3说明使用管道的一种实现。下面先看一个使用这5各例程的实例。
程序清单8-6输出两个字符串:一个由子进程输出,另外一个由父进程输出。由于输出依赖内核使用这两个进程运行的顺序及每一个进程运行的时间程度,因此该程序包含了一个竞争条件。
static void charatatime(char *); int main(void) { pid_t pid; if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { charatatime("output from child\n"); } else { charatatime("output from parent\n"); } exit(0); } static void charatatime(char *str) { char *ptr; int c; setbuf(stdout, NULL); // set unbuffered for (ptr = str; (c = *ptr++) != 0;) putc(c, stdout); }
在程序中将标准输出设置为不带缓冲的,因而每一个字符输出都须要调用一次write。本例的目的是使内核能尽量地在两个进程间屡次切换,以便演示竞态条件。结果以下 :
修改程序清单上面程序,使用 TELL 和WAIT 函数,因而有以下:
运行此程序则可以获得预期的输出。
8.10 exec函数
当进程调用 exec函数时,该进程的程序彻底替换为新程序,而新程序则从其main函数开始执行。由于exec并不建立新进程,因此先后的进程ID并无改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。
有6种不一样的exec函数可以使用。
这些函数之间的第一个区别是前4个取路径名做为参数,后两个则取文件名做为参数。而当指定filename做为参数时:
(1)若是filename中包含/,则将其视为路径名。
(2)不然就按PATH环境变量,在它所指定的各目录中搜索可执行文件。
PATH变量包含了一张目录表(称为路径前缀),目录之间用冒号(:)分隔。例如,name=value环境字符串
PATH=/bin:/usr/bin:/usr/local/bin:.
指定在4各目录中进行搜索。最后的路径前缀表示当前目录。(零长前缀也表示当前目录。在value的开始处可用 : 表示,在行中间则要用 :: 表示,在行尾则以 : 表示。)
若是execlp或execvp使用路径前缀找到了一个可执行文件,可是该文件不是由链接编辑器产生的机器可执行文件,则认为该文件是一个shell脚本,因而试着调用/bin/sh,并以该filename做为shell的输入。
第二个区别与参数表的传递有关(l表示list,v表示矢量vector)。函数execl、execlp和execle要求将新程序的每一个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。对于另外三个函数(execv、execvp和execve),则应先构造一个指向各参数的指针数组,而后将该数组地址做为这三个函数的参数。
execl、execle和execlp三个函数表示命令行参数的通常方法是:
char *arg0, char *arg1,..., char*argn, (char *)0
应当特别指出的是:在最后一个命令行参数以后跟了一个空指针,若是用常数0来表示一个空指针,则必须将他强制转换为一个字符指针,不然将他解释为整形参数。若是一个整形数的长度与char *的长度不一样,那么exec函数的实际参数就将出错。
最后一个区别与新程序传递环境表相关。以e结尾的两个函数(execle和execve)能够传递一个指向环境字符串指针数组的指针。其余四个函数则使用调用进程中的environ变量为新程序复制现有环境(回忆7.9节,其中曾说起若是系统支持setenv和putenv这样的函数,则可更改当前环境和后面生成的子进程的环境,但不能影响父进程的环境)。一般,一个进程容许将其环境传播给其子进程,但有时也有 这种状况,即进程想要为子进程指定某一个肯定的环境。例如,在初始化一个新登录的shell时,login程序一般建立一个只定义少数几个变量的特殊环境,而在咱们登录时,能够经过shell启动文件,将其余变量加到环境中。
execle的参数是:
char *pathname, char *arg0, ..., char *argn, (char *)0, char *envp[]
从中可见,最后一个参数是指向环境字符串的各字符指针构成的数组的地址。而函数原型中,全部命令行参数、空指针和envp指针都用省略号(...)表示。
这6个exec函数的参数很难记忆。函数名中的字符会给咱们一些帮助。字母p表示该函数取filename做为参数,而且用PATH环境变量寻找可执行文件。字母 l 表示该函数取一个参数表,它与字母 v 互斥。 v 表示该函数取一个 argv[] 矢量。最后,字母 e 表示该函数取 envp[] 数组,而不使用当前环境。下表显示了这 6 个函数之间的区别。
前面曾说起在执行 exec 后,进程 ID 没有改变。除此以外,执行新程序的进程还保持了原进程的如下特征:
(1)进程ID和父进程ID
(2)实际用户ID和实际组ID
(3)附加组ID
(4)进程组ID
(5)会话ID
(6)控制终端
(7)闹钟尚余留的时间
(8)当前工做目录
(9)根目录
(10)文件模式建立屏蔽字
(11)文件锁
(12)进程信号屏蔽
(13)未处理信号
(14)资源限制
(15)tms_utime、tms_stime、tms_cutime以及tms_cstime值。
对打开文件的处理与每一个描述符的执行时关闭(close-on-exec)标志有关。见图3-1以及3.14节中对 FD_CLOEXEC 的说明,进程中每一个打开描述符都有一个执行时关闭标志。若此标志设置,则在执行exec时关闭该描述符,不然该描述符仍然打开。除非特意用 fcntl 设置了该标志,不然系统的默认操做是在执行exec后仍保持这种描述符打开。
POSIX.1明确要求在执行exec时关闭打开的目录流(见4.21节中所述的opendir函数),这一般是由opendir函数实现的,它调用fcntl函数为对应于打开目录流的描述符设置执行时关闭标志。
注意,在执行exec先后实际用户ID和实际 组ID保持不变,而有效ID是否改变则取决于所执行程序文件的设置用户ID位和设置组ID位是否设置。若是新程序的设置用户ID位已设置,则有效用户ID变成程序文件全部者的ID,不然有效用户ID不变,对组ID的处理方式与此相同。
6个函数之间的关系以下图:
在这种安排中,库函数execlp和execvp使用PATH环境变量,查找第一个包含名为filename的可执行文件的路径名前缀。
char *env_init[] = {"USR=unknow", "PATH=/tmp", NULL}; int main(void) { pid_t pid; if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { /* specify pathname, specify environment */ if (execle("/home/sar/bin/echoall", "myarg1", "MY ARG2", (char *)0, env_init) < 0) err_sys("execle error"); } if (waitpid(pid, NULL, 0) < 0) err_sys("wait error"); if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { /* specify filename, inherit environment */ if (execlp("echoall", "echoall", "only 1 arg", (char *)0) < 0) err_sys("execlp error"); } exit(0); }
// echoall int main(int argc, char *argv[]) { int i; char **ptr; extern char **environ; for (i = 0; i < argc; i++) printf("argv[%d]: %s\n", i, argv[i]); for (ptr = environ; *ptr != 0; ptr++) printf("%s\n", *ptr); exit(0); }
注意,shell提示符出如今第二个exec打印argv[0]以前。这是由于父进程并不等待该子进程结束。
8.11 更改用户ID和组ID
进程的用户ID和组ID绝对了其特权的大小。
通常而言,在设计应用程序时,咱们老是试图使用最小特权模型。依照此模型,咱们的程序应当只具备为完成给定任务所需的最小特权。这减小了安全性受到损害可能性,这种安全性损害是因为恶意用户试图哄骗咱们程序以未预料的方式使用特权所形成的。
可使用 setuid 函数设置实际用户ID和有效用户ID。与此相似,能够用setgid函数设置实际组ID和有效组ID
int setuid(uid_t uid); int setgid(gid_t gid);
8.12 解释器文件
现在UNIX系统都支持解释器文件,这种文件时文本文件,其起始行形式是:
#! pathname [optional-argument]
感叹号和pathname之间的空格是可选的。最多见的解释器文件如下列行开始:
#!/bin/sh
pathname一般是绝对路径,对它不进行什么特殊处理(即不使用PATH进行 路径搜索)。内核调用exec函数的进程实际执行的并非该解释器文件,而是该解释器文件第一行中 pathname 所指定的文件。必定要将解释器文件(文本文件,它以 #! 开头)和解释器(由该解释器文件第一行中的pathname指定)区分开来。
让咱们观察一个实例,从中可了解当被执行文件是解释器文件时,内核如何处理exec函数的参数及解释器文件第一行的可选参数。
int main(void) { pid_t pid; if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { if (execl("/home/sar/bin/testinterp", "testinterp", "myarg1", "MY ARG2", (char *)0) < 0) err_sys("execl error"); } if (waitpid(pid, NULL, 0) < 0) err_sys("waitpid error"); exit(0);
程序echoarg(解释器)回送每个命令行参数(他就是程序清单7-3).注意,这里 argv[0] 是该解释器的pathname,argv[1]是解释器文件中的可选参数,其他参数是pathname(/home/sar/bin/testinterp),以及程序清单8-10中调用 execl 的第二个和第三个参数(myarg1和 MY ARG2)。调用execl时的 argv[1] 和 argv[2] 已右移两个位置。注意,内核取 execl 调用中的 pathname 而非第一个参数 (testinterp),由于通常而言, pathname 包含了比第一个参数更多的信息。
8.13 system函数
在程序中执行一个命令字符串很方便。例如,假定要将时间和日期放到某个文件中,则可以使用6.10节中说明的函数实现这一点。调用 time 获得当前日历时间,接着调用 localtime 将日历时间转换为年、月、日、时、分、秒、周日形式,而后调用 strftime 对上面的结果进行格式化处理,最后将结果写到文件中。可是用下面的 system 函数则更容易作到这一点。
int system(const char *cmdstring);
system("data > file");
若是 cmdstring 是一个空指针,则仅当命令处理程序可用时,system 返回值非 0 值,这一特征能够肯定在一个给定操做系统上是否支持 system 函数。
由于 system 在其实现中调用了 fork、exec和 waitpid,所以有三种返回值:
(1)若是 fork 失败或者 waitpid 返回除 EINTR 以外的出错,则 system 返回 -1,并且 errno 中设置了错误类型值。
(2)若是 exec 失败(表示不能执行 shell),则其返回值如同 shell 执行了 exit(127)同样。
(3)不然全部三个函数(fork、exec和waitpid)都执行成功,而且 system 的返回值是 shell 的终止状态,其格式已在 waitpid 中说明。
若是 waitpid 由一个捕捉到的信号中断,则某些早期的 system 实现都返回错误类型值 EINTR,可是,由于没有可用的清理策略能让应用程序从这种错误类型中恢复,因此 POSIX 后来增长了下列要求:在这种状况下 system 不返回一个错误(10.5节将讨论被中断的系统调用)。
下面是 system 函数的一种实现。它没有对信号进行处理。10.18节中将修改此函数使其进行信号处理。
int system(const char *cmdstring) /* version without signal handling */ { pid_t pid; int status; if (cmdstring == NULL) return 1; /* always a command processor with UNIX */ if ((pid = fork()) < 0) { status = -1 /* probably out of processes */ } else if (pid == 0) { /* child */ execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); _exit(127); /* execl error */ } else { /* parent */ while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) { status = -1; /* error other than EINTR from waitpid() */ break; } } } return status; }
shell 的 -c 选项告诉shell 程序取下一个命令参数(在这里是 cmdstring)做为命令输入(而不是从标准输入或从一个给定的文件中读命令)。shell对以 null 字符终止的命令字符串进行语法分析,将它们分红命令行参数。传递给 shell 的实际命令字符串能够包含任一有效 shell 命令。例如,能够用<和>重定向输入和输出。
若是不使用 shell 执行此命令,而是试图由咱们本身去执行它,那么将至关困难。首先,咱们必须用 execlp 而不是 execl,像 shell 那样使用 PATH 变量。咱们必须将 null 结尾的命令字符串分红各个命令参数,以便调用 execlp。最后,咱们也不能使用任何一个 shell 元字符。
注意,咱们调用 _exit 而不是 exit。这是为了防止任一标准 I/O 缓冲区(这些缓冲区会在 fork 中由父进程复制到子进程)在子进程中被冲洗。
用程序清单8-13对system的这种版本进行了测试(pr_exit函数定义在程序清单8-3中)
int main(void) { int status; if ((status = system("date")) < 0) err_sys("system() error"); pr_exit(status); if ((status = system("nosuchcommand")) < 0) err_sys("system() error"); pr_exit(status); if ((status = system("who; exit 44")) < 0) err_sys("system() error"); pr_exit(status); exit(0); }
使用 system 而不是直接使用 fork 和 exec 的优势是:system 进行了所需的各类出错处理,以及各类信号(在10.18节中的 system 函数的下一个版本中)。
在UNIX 早期版本中,都没有 waitpid 函数,因而父进程用下列形式的语句等待子进程:
while ((lastpid = wait(&status) != pid && lastpid != -1)) ;
若是调用 system 的进程在调用它以前已经生成它本身的子进程,那么将引发问题。由于上面的 while 语句一直循环执行,直到由 system 产生的子进程终止才中止,若是不是 pid 标识的任一子进程在 pid 子进程以前终止,则它们的进程 ID 和 终止状态都会被 while 语句丢弃。实际上,因为 wait不能等待一个指定的进程以及其余一些缘由,POSIX 才定义了 waitpid 函数。若是不提供 waitpid 函数,popen 和 pclose 函数也会发生一样的问题。
设置用户 ID 程序
若是在一个设置用户 ID 程序中调用 system,那么发生什么呢?这是一个安全性方面的漏洞,毫不应当这样作。下面程序对其命令行参数调用 system 函数。
int main(int argc, char *argv[]) { int status; if (argc < 2) err_quit("command-line argument required"); if ((status = system(argv[1])) < 0) err_sys("system() error"); pr_exit(status); exit(0); }
将此程序编译称可执行文件 tsys。
程序清单8-15是另外一个简单程序,它打印其实际和有效用户ID
int main(void) { printf("read uid = %d, effective uid = %d\n", getuid(), geteuid()); exit(0); }
将此程序编译成可执行文件 printuids。运行这两个程序,获得下列结果:
咱们给予 tsys 程序的超级用户权限在 system 中执行了 fork 和 exec 以后仍会保持下来。
若是一个进程正以特殊权限(设置用户ID或设置组ID)运行,它又想生成另外一个进程执行另外一个程序,则它应当直接使用 fork 和 exec,并且在 fork 以后、exec 以前要改回到普通权限。设置用户ID或设置组ID程序决不该调用 system 函数。
8.14进程会计
8.15用户标识
8.16 进程时间
在 1.10 节中说明了咱们能够测量的三种时间:墙上时钟时间、用户cpu时间和系统cpu时间。任一进程均可调用 times 函数以得到它本身及终止进程的上述值。
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 */ };
注意,此结构没有包含墙上时钟时间的任何测量值。做为替代,times函数返回墙上时钟时间做为其函数值。此值是相对于过去的某一时刻测量的,因此不能用其绝对值,而必须使用其相对值。例如,调用 times,保存其返回值。在之后某个时间再次调用 times,重新的返回值中减去之前的返回值,此差值就是墙上时钟时间(一个长期运行的进程可能会使墙上时钟时间溢出,固然这种可能性极小)
该结构中两个针对子进程的字段包含了此进程用 wait、waitid或waitpid已等待到的各个子进程的值。
全部由此函数返回的 clock_t 值都用 _SC_CLK_TCK 变换成秒数。
// 时间以及执行全部命令行参数 static void pr_times(clock_t, struct tms *, struct tms *); static void do_cmd(char *); int main(int argc, char *argv[]) { int i; setbuf(stdout, NULL); for (i = 1; i < argc; i++) do_cmd(argv[i]); /* once for each command-line arg */ exit(0); } static void do_cmd(char *cmd) { struct tms tmsstart, tmsend; clock_t start, end; int status; printf("\ncommand: %s\n", cmd); if ((start = times(&tmsstart)) == -1) /* starting values */ err_sys("time error"); if ((status = system(cmd)) < 0) /* execute command */ err_sys("system() error"); if ((end = times(&tmsend)) == -1) err_sys("times error"); pr_times(end_start, &tmsstart, &tmsend); pr_exit(status); } static void pr_times(clock_t real, struct tms *tmsstart, struct tms *tmsend) { static long clktck = 0; if (clktck == 0) if ((clktck = sysconf(_SC_CLK_TCK)) < 0) err_sys("sysconf error"); printf(" real: %7.2f\n", real/(double)clktck); printf(" user: %7.2f\n", (tmsend->tms_utime - tmsstart->tms_utime)/(double)clktck); printf(" sys: %7.2f\n", (tmsend->tms_cutime - tmsstart->tms_sutime) / (double)clktck); printf(" child user: %7.2f\n", (tmsend->tms_cutime - tmsstart->tms_cutime)/ (double) clktck); printf(" child sys: %7.2f\n", (tmsend->tms_cstime - tmsstart->tms_cstime) / (double)clktck); }
运行程序,获得:
在这两个实例中,子进程中显示的全部CPU时间都是执行shell和命令的子进程所使用的CPU时间。