[单刷APUE系列]第八章——进程控制[1]

目录

[单刷APUE系列]第一章——Unix基础知识[1]
[单刷APUE系列]第一章——Unix基础知识[2]
[单刷APUE系列]第二章——Unix标准及实现
[单刷APUE系列]第三章——文件I/O
[单刷APUE系列]第四章——文件和目录[1]
[单刷APUE系列]第四章——文件和目录[2]
[单刷APUE系列]第五章——标准I/O库
[单刷APUE系列]第六章——系统数据文件和信息
[单刷APUE系列]第七章——进程环境
[单刷APUE系列]第八章——进程控制[1]
[单刷APUE系列]第八章——进程控制[2]
[单刷APUE系列]第九章——进程关系
[单刷APUE系列]第十章——信号[1]shell

进程标识

在平常的开发使用过程中,以及以往的开发经验,都应该知道进程是存在一个ID的,也就是进程ID(process ID),进程ID是惟一的,用以保证进程是惟一存在而且能被惟一得到。可是,在Unix系统中,进程ID是惟一的,可是在进程退出后,系统很是有可能将这个pid交付给新启动的进程,可是这样会致使新进程被误认为是以前存在的进程,因此现有的Unix系统有一个队列,用于pid的延时复用。
Linux系统是类Unix系统,它的实现也是很具备表明性的,你们都知道,Linux实际上应该叫GNU/Linux,并且Linux只是一个内核,当将这个内核和软件集合打包,就造成了一个Linux发行版,固然其中不包含各个发行商的修改内容,内核是启动后得到控制权,内核有一部分就造成了pid为0的进程,也叫做调度进程,没有什么卵用,而后pid为1的进程被启动,也就是其余进程的父进程,通常都是init程序,它负责启动整个Unix系统,并将系统根据配置文件引导到一个可以使用的状态,init和前面的调度进程不同,调度进程其实是内核的一部分,而init是内核启动的一个普通进程,可是它拥有root权限,在苹果系统中,init进程被launchd进程替代,可是其做用也是差很少的。
除了上面的两个进程之外,还有不少和系统密切相关的内核进程,这些进程都以守护进程的形式常驻。系统提供了一系列函数用于获取当前进程的各项属性。segmentfault

pid_t getpid(void);
pid_t getppid(void);

uid_t getuid(void);
uid_t geteuid(void);

gid_t getgid(void);
gid_t getegid(void);

上面6个函数,分别是获取当前进程pid、父进程pid、真实用户ID、有效用户ID、真实组ID和有效组ID。至于这些属性的解释,前面几章已经都提到过了,因此这里就再也不解释。网络

进程派生

进程能够派生出子进程,这是一个很广泛的行为,Unix系统也为此提供了函数运维

pid_t fork(void);

Fork() causes creation of a new process.  The new process (child process) is an exact copy of the calling process (parent process) except for the following:

o   The child process has a unique process ID.

o   The child process has a different parent process ID (i.e., the process ID of the parent process).

o   The child process has its own copy of the parent's descriptors.  These descriptors reference the same underlying objects, so that, for instance,file pointers in file objects are  shared between the child and the parent, so that an lseek(2) on a descriptor in the child process can affect a subsequent read or write by the parent.  This descriptor copying is also used by the shell to establish standard input and output for newly cre-ated processes as well as to set up pipes.

o   The child processes resource utilizations are set to 0; see setrlimit(2).

这是一个很重要的函数,前面也使用过fork函数派生子进程,fork函数建立一个新进程,新进程是父进程的完整复制,固然,子进程的pid是从新生成的,子进程也拥有父进程pid做为ppid属性,子进程拥有父进程描述符的一份拷贝,可是实际上引用了相同的底层对象,因此实际上父进程子进程都是共享一样的文件对象,就像第三章里面讲到的,进程只维护了文件描述符和文件指针的映射,内核为全部的打开的文件维护了一个文件表,每一个文件表项包含了文件状态标志、文件偏移量等等,在学习文件共享这块的时候,笔者还提到一个重点就是内核维护的文件表是为全部打开的文件,同一个文件被不一样进程打开是两个文件表项,可是父子进程其实是拷贝了完整的进程空间,因此说子进程拥有父进程的文件描述符和文件指针的映射,因此说,父子进程的文件描述符指向了同一个文件表项目,因此lseek这样的修改偏移量的函数会影响到父子进程,这个特性也被shell用于创建标准输入输出错误给新启动的进程。固然,子进程的资源限制和父进程是不一样的,将被重置。
前面几章中提到,fork函数被调用一次,可是返回两次,子进程和父进程都会获得返回值,可是子进程获得的返回值是0,父进程的返回值则是子进程的pid。子进程复制了整个父进程的进程空间,例如堆和栈等,固然,这只是个副本,父子进程实际共享的只有正文段,这样能够节约空间,而且前面提到正文段其实是只读的。
在实际的Unix系统实现中,经常使用差分存储的技术,也就是说,原来的堆栈不会被复制,两个进程以只读的形式共享同一个堆栈区域,当须要修改区域内容的时候,则在新的区域制做差分存储。函数

#include "include/apue.h"

int globalVar = 6;
char buf[] = "a write to stdout\n";

int main(int argc, char *argv[])
{
    int var;
    pid_t pid;
    
    var = 80;
    if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1)
        err_sys("write error");
    printf("before fork\n");
    
    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        ++globalVar;
        ++var;
    } else {
        sleep(2);
    }
    
    printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globalVar, var);
    exit(0);
}

而后执行的结果以下学习

~/Development/Unix » ./a.out
a write to stdout
before fork
pid = 8905, glob = 7, var = 81
pid = 8904, glob = 6, var = 80
~/Development/Unix » ./a.out > temp.out
~/Development/Unix » cat temp.out
a write to stdout
before fork
pid = 8916, glob = 7, var = 81
before fork
pid = 8915, glob = 6, var = 80

很明显的能够看到两个现象,就是子进程修改了变量不会致使父进程的变化,还有就是输出到文件和输出到终端发生了区别。看上面的代码,能够发现使用write函数向标准输出写的时候,使用了sizeof(buf) - 1,这是由于strlen函数计算长度的时候是不计算终止的null字节的,可是sizeof则会包括null字节,这个其实很好理解,strlen函数其实是一个函数调用,为了保证开发者使用的便捷,因此默认认为字符串长度实际上不该该包含null字节,可是sizeof则是一个单目运算符,它和其余的运算符同样都不是函数,sizeof操做符以字节形式给出了其操做数的存储大小。操做数能够是一个表达式或括在括号内的类型名。操做数的存储大小由操做数的类型决定。换言之,这是一个编译时计算。
在第三章中讲到,write函数是不带缓冲的IO,而标准C库提供的则是带有缓冲的,在前面的章节中也提到了缓冲的不一样状况,若是标准输出是链接到终端设备,那么它是行缓冲的,不然就是全缓冲的,在标准输出是终端设备的状况下,咱们只看到了一行输出,由于换行符冲洗了缓冲区,而当标准输出重定向到文件的时候,输出是全缓冲的,这样换行符不会致使系统的自动写入,当fork函数执行的时候,这行输出依旧被存储在缓冲区中,而后随着fork函数被共享给了子进程,随着后续继续的写入,两个进程都同时写入了before fork字符串。
实际上,文件共享一直是很重要的概念,咱们知道,用户启动的进程通常都是shell启动的,也就是说是shell的子进程,因此shell将进程的输入输出能够进行重定向,当父进程的标准输入输出被重定向的时候,因为子进程继承了父进程的文件描述符,因此子进程也被重定向了,前面也说过,父子进程相同的文件描述符是指向同一个文件表项的,因为这个缘由,二者任意一个进程修改了偏移量,下一个进程会跟在这个偏移量后,能够变相的实现一种交互。固然咱们知道,因为多进程操做系统调度,进程之间的切换是很频繁的,若是没有父子进程的同步措施,二者的输出颇有可能混合,因此对于派生子进程,有如下两种方式处理文件描述符优化

  1. 父进程使用函数等待子进程完成。这个很是简单,因为共享同一个文件表项,子进程的输出也会更新父进程的偏移量,因此等待子进程完成后直接就能读写。ui

  2. 父进程和子进程各自执行不一样的程序段。在这种状况下,在fork之后,父子进程各自只使用不冲突的文件描述符。lua

前面也提到过不少关于父子进程的继承,例如各类用户组ID,当前工做目录,资源限制环境变量等,一般状况下,使用fork函数有两种缘由spa

  1. 父进程复制自身,各自执行不一样的代码段,也就是网络服务中典型的多进程模型。

  2. 一个进程想要执行不一样的程序。shell就是这样的,因此子进程能够在fork后马上使用exec,让新程序运行。

进程派生变体

pid_t vfork(void);

Vfork() can be used to create new processes without fully copying the address space of the old process, which is horrendously inefficient in a paged envi-ronment.  It is useful when the purpose of fork(2) would have been to create a new system context for an execve.  Vfork() differs from fork in that the child borrows the parent's memory and thread of control until a call to execve(2) or an exit (either by a call to exit(2) or abnormally.)  The parent process is suspended while the child is using its resources.

Vfork() returns 0 in the child's context and (later) the pid of the child in the parent's context.

Vfork() can normally be used just like fork.  It does not work, however, to return while running in the childs context from the procedure that called vfork() since the eventual return from vfork() would then return to a no longer existent stack frame.  Be careful, also, to call _exit rather than exit if you can't execve, since exit will flush and close standard I/O channels, and thereby mess up the parent processes standard I/O data structures.  (Even with fork it is wrong to call exit since buffered data would then be flushed twice.)

vfork函数也是建立一个新进程,可是不彻底拷贝父进程地址空间,这个函数spawn new process in a virtual memory efficient way,实际上这个函数主要用于spawn一个新进程而作的优化,不须要复制父进程的地址空间,从而加快了函数的执行,除此之外,vfork函数还会保证子进程先运行,

#include "include/apue.h"

int globalVar = 6;

int main(int argc, char *argv[])
{
    int var;
    pid_t pid;
    
    var = 88;
    printf("before vfork\n");
    if ((pid = vfork()) < 0) {
        err_sys("vfork error");
    } else if (pid == 0) {
        ++globalVar;
        ++var;
        _exit(0);
    } else {
        printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globalVar, var);
    }
    exit(0);
}

而后运行这个程序

~/Development/Unix » ./a.out
before vfork
pid = 10616, glob = 7, var = 89

从运行结果能够很清楚的看到,子进程对变量的改变确实更改了父进程的变量值,其次,在上面的代码中,子进程使用了_exit函数关闭进程,在前面咱们能够了解到,exit函数会在关闭进程以前进行一系列的操做,而子进程其实是和父进程共享同一个内存空间,因此极可能会致使没有任何输出,因此在vfork函数只是用于spawn一个进程的前置操做,而不是正常的派生子进程,这个也在Unix系统手册中给予了警告。

退出进程

就像前面一章讲的,有5种正常退出和3种异常终止,下面是5中正常退出

  1. main函数返回,实际上等效于调用exit

  2. exit函数。exit函数其实是ISO C定义的函数,在前面也有过详细的工做流程描述

  3. 调用_exit和_Exit函数。二者能够当作等价,只是一个是ISO C库函数,一个是Unix系统函数

  4. 进程的最后一个线程执行return语句

  5. 进程的最后一个函数使用pthread_exit函数

3种异常终止以下

  1. 调用abort函数产生SIGABRT信号

  2. 进程接收到信号

  3. 进程接收到取消请求

不管是如何退出进程,实际上在最后都须要内核进行执行清理工做,包括打开的描述符什么的,对于上面5中正常退出,都会有一个退出状态能够传递,对于3种异常终止,内核一样会产生一个终止状态,最终,都会变成退出状态,这样父进程就能获得子进程的退出状态。
在正常的使用过程当中,子进程都是先于父进程退出,可是在某些特殊状况下,父进程会先于子进程结束,可是实际上在终止每一个进程的时候,内核会检查全部现有的进程,若是是正在终止的进程的子进程,就将其父进程修改成init进程,也就是pid为1的进程。
在Unix系统运维中,会碰到僵尸进程,在开发的概念上来讲,就是子进程已经终止,可是父进程还没有对其进行善后处理。

wait函数族

pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);

实际上,当一个进程退出,不管是正常仍是异常,内核都会向父进程发送SIGCHLD信号,在默认状况下,都是选择忽略这个信号,这里只须要知道使用wait函数族会发生什么

The wait() function suspends execution of its calling process until stat_loc information is available for a terminated child process, or a signal is received.  On return from a successful wait() call, the stat_loc area contains termination information about the process that exited as defined below.

wait函数会阻塞父进程知道子进程终止或者受到SIGCHLD信号,当wait函数返回时,stat_loc将会包含进程结束信息。
waitwaitpid函数就在于参数的区别,waitpid能够传入一个options选项用于行为的改变,还有就是能够指定进程ID。wait函数则是等待直到第一个子进程退出.

The options parameter contains the bitwise OR of any of the following options.  The WNOHANG option is used to indi-cate that the call should not block if there are no processes that wish to report status.  If the WUNTRACED option is set, children of the current process that are stopped due to a SIGTTIN, SIGTTOU, SIGTSTP, or SIGSTOP signal also have their status reported.

WNOHANG参数指示没有进程报告状态则当即返回,WUNTRACED选项则是子进程因为SIGTTINSIGTTOUSIGTSTPSIGSTOP信号进入暂停状态,还有一个是WCONTINUED,头文件中存在,可是说明手册上不存在,由POSIX1.x规定。

The following macros may be used to test the manner of exit of the process.  One of the first three macros will evaluate to a non-zero (true) value:
WIFEXITED(status)
        True if the process terminated normally by a call to _exit(2) or exit(3).

WIFSIGNALED(status)
        True if the process terminated due to receipt of a signal.

WIFSTOPPED(status)
        True if the process has not terminated, but has stopped and can be restarted.  This macro can be true only if the wait call specified the WUNTRACED option or if the child process is being traced (see ptrace(2)).

Depending on the values of those macros, the following macros produce the remaining status information about the child process:

WEXITSTATUS(status)
        If WIFEXITED(status) is true, evaluates to the low-order 8 bits of the argument passed to _exit(2) or exit(3) by the child.

WTERMSIG(status)
        If WIFSIGNALED(status) is true, evaluates to the number of the signal that caused the termination of the process.

WCOREDUMP(status)
        If WIFSIGNALED(status) is true, evaluates as true if the termination of the process was accompanied by the creation of a core file containing an image of the process when the signal was received.

WSTOPSIG(status)
        If WIFSTOPPED(status) is true, evaluates to the number of the signal that caused the process to stop.

上面就是苹果系统支持的一系列宏,原著中的WIFCONTINUED宏没有出如今说明手册中,可是实际上在头文件中是存在的。

#include "include/apue.h"
#include <sys/wait.h>

void pr_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", WTERMSIG(status),
#ifdef WCOREDUMP
        WCOREDUMP(status) ? " (core file generated)" : "");
#else
        "");
#endif
    else if (WIFSTOPPED(status))
        printf("child stopped, signal number = %d\n", WSTOPSIG(status));
}

上面是原著提供的打印终端终止状态的函数,能够按照之前的方法将其打包为静态库。须要注意的是,你须要指定-D_DARWIN_C_SOURCE来保证编译添加上WCOREDUMP支持。

#include "include/apue.h"
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    pid_t pid;
    int status;
    
    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        exit(7);
    
    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit(status);
    
    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        abort();
    
    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit(status);
    
    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        status /= 0;
    
    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit(status);
    
    exit(0);
}

编译运行

> ./a.out
normal termination, exit status = 7
abnormal termination, signal number = 6
abnormal termination, signal number = 8

并无像原著同样出现(core file generated)字样,多是由于系统虽然支持这个宏,可是只对少数错误会进行转储。
waitpid函数的options选项的三个可选值实际上起到了两种做用,WNOHANG是非阻塞,而其余两个参数则是做业控制。

相关文章
相关标签/搜索