《UNIX环境高级编程》啃书笔记(第7章进程环境、第8章进程控制、第11章线程)

进程环境

main函数

C程序总是从main函数开始执行,main函数的原型是:

int main(int argc,char *argv[]);

其中argc是命令行参数的数目,argc是指向参数的各个指针所构成的数组。

当内核执行C程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址—这是由连接编辑器设置的,而连接编辑器则由C编译器调用。启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排。

命令行参数:当执行一个程序时,调用exec的进程可将命令行参数传递给该新程序。且argv[argc]是一个空指针。

进程终止

有8种方式使进程终止,其中5种为正常终止:

  1. 从main返回
  2. 调用exit
  3. 调用_exit或_Exit
  4. 最后一个线程从其启动例程返回。该进程以终止状态0返回,而非线程的返回值
  5. 从最后一个线程调用pthread_exit。同上。

异常终止有3种方式:

  1. 调用abort,产生SIGABRT信号
  2. 接到一个信号
  3. 最后一个线程对取消请求做出响应

不管进程如何终止,最后都会执行内核中的同一段代码,这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。

退出函数

3个函数用于正常终止一个程序:_exit和_Exit立即进入内核,exit则先执行一些清理处理,然后返回内核。

#include<stdlib.h>
void exit(int status);
void _Exit(int status);

#include<unistd.h>
void _exit(int status);

三个函数中,exit函数总是执行一个标准I/O库的清理关闭操作:对于所有打开流调用fclose函数,使得输出缓冲中的所有数据都被冲洗(写到文件上)。还会调用各终止处理程序。

3个退出函数都带一个整型参数,称为终止状态(或退出状态)。若
(a)调用这些函数时不带终止状态
(b)main执行了一个无返回值的return语句
(c)main没有声明返回类型为整型
则该进程的终止状态是未定义的。

若main的返回类型是整型,并且main执行到最后一条语句时返回(隐式返回),那么该进程的终止状态是0。注意main函数返回一个整型值与用该值调用exit是等价的即exit(0);等价于return 0;

对于以上3个终止函数,我们可以通过将其退出状态作为参数传递给函数来通知父进程它是如何终止的。在异常情况下,内核(而非进程本身)产生一个指示其异常终止原因的终止状态。如果子进程正常终止,则父进程可以获得子进程的退出状态。但在任意一种情况下,该终止进程的父进程都能调用wait和waitpid函数取得其终止状态。

对于父进程已经终止的所有进程,它们的父进程都改变为init进程,我们称这些进程由init进程收养:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。这样做的目的在于保证每个进程有一个父进程。

若子进程先终止,内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。另外,若一个已经终止、但其父进程尚未对其进行善后处理(获取有关信息并释放资源)的进程被称为僵死进程,也就是说除非父进程等待取得子进程的终止状态,不然这些子进程终止后就会变成僵死进程。对于init,无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态进行处理。

函数atexit

规定一个进程可以登记多至32个函数,这些函数将由exit自动调用。称这些函数为终止处理程序,并调用atexit函数来登记这些函数。

#include<stdlib.h>
int atexit(void (*func)(void));

若成功则返回0,出错则返回非0。

其中atexit的参数是一个函数地址,当调用此函数时无需向它传递任何参数,也不期望它返回一个值。exit调用这些函数的顺序与它们登记时候的顺序相反。同一函数如若登记多次,也会被调用多次。

一个C程序是如何启动和终止的:
一个C程序是如何启动和终止的

注意内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式或隐式地(通过调用exit)调用_exit或_Exit。进程也可非自愿地由一个信号使其终止。

共享库

共享库使得可执行文件中不再需要包含公用的库函数,而只需要在所有进程都可引用的存储区中保存这种库例程的一个副本。程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链接。这减少了每个可执行文件的长度,但增加了一些运行时间开销。这种时间开销发生在该程序第一次被执行时,或者每个共享库函数第一次被调用时。

典型的进程存储空间安排

典型的进程存储空间安排

环境变量

每个程序都接收到一张环境表,它是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。全局变量environ则包含了该指针数组的地址:

extern char **environ;

我们称environ为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串。环境由name=value 这样的字符串组成。下图为由5个字符串组成的环境:
由5个字符串组成的环境

unix内核并不查看这些字符串,它们的解释完全取决于各个应用程序。

函数getenv可以取环境变量值:

#include<stdlib.h>
char *getenv(const char *name);

若成功返回指向与name关联的value的指针,若未找到返回NULL。我们应当使用getenv从环境中取一个指定环境变量的值,而不是直接访问environ。
定义的环境变量

环境变量的删除增加操作:

#include<stdlib.h>
int putenv(char *str);               //若成功返回0,出错返回非0
int setenv(const char *name,const char *value,int rewrite);
int unsetenv(const char *name);      //若成功返回0,出错返回-1
  • putenv取形式为name=value 的字符串,将其放到环境表中。若name已经存在,则先删除其原来的定义。
  • setenv将name设置为value。若在环境中name已经存在,那么(a)若rewrite非0,则首先删除其现有的定义;(b)若rewrite为0,则不删除其现有定义(name不设置为新的value)。
  • unsetenv删除name的定义。即使不存在这种定义也不算出错。

putenv与setenv的差别:
setenv必须分配存储空间。putenv可以自由地将传递给它的参数字符串直接放到环境中。
不能将存放在栈中的字符串作为参数传递给putenv,因为从当前函数返回时其栈帧占用的存储区可能将被重用。

删除增加操作的实现:

如上节所示,环境表(指向实际name=value字符串的指针数组)和环境字符串通常存放在进程存储空间的顶部(栈之上)。

  1. 删除一个字符串只要先在环境表中找到该指针,然后将所有后续指针都向环境表首部顺次移动一个位置。
  2. 修改一个现有name:
    (a)新value长度小于等于现有的,直接复制
    (b)新value长度大于原长度,调用malloc为新字符串分配空间,然后将新字符串复制到该空间中,接着使环境表中针对name的指针指向新分配区
  3. 增加一个新的name:
    (a)若为第一次增加, 调用malloc为新的指针表分配空间,接着将原来的环境表复制到新分配区,并将指向新name=value 字符串的指针存放在该指针表的表尾,然后又将一个空指针存放在其后,最后使environ指向新指针表。
    (b)若不是第一次增加,只要调用realloc,以分配比原空间多存放一个指针的空间,然后将指向新name=value 字符串的指针存放在该表表尾,后面跟着一个空指针。

非局部goto:setjmp和longjmp

goto语句是不能跨越函数的,而执行这种类型跳转功能的是函数setjmp和longjmp。例:

void do_line(char *);
void cmd_add(void);
int get_token(void);

int main(void){
    char line[MAXLINE];
    while(fgets(line,MAXLINE,stdin)) do_line(line);
    exit(0);
}

void do_line(char *ptr){
    int cmd;
    cmd_add();
}

void cmd_add(void){
    int token;
}

例如此处cmd_add函数发现一个错误,这时我们不得不以检查返回值的方法逐层返回,十分麻烦。

注意调用cmd_add后的各个栈帧,line在main栈帧,cmd在do_line栈帧,token在cmd_add栈帧:
调用cmd_add后的各个栈帧

非局部goto是指在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数中。

#include<setjmp.h>
int setjmp(jmp_buf env);              //若直接调用返回0,若从longjmp调用返回val的值
void longjmp(jmp_buf env,int val);

在希望返回到的位置调用setjmp,setjmp参数env的类型是一个特殊类型jmp_buf。这一数据类型是某种形式的数组,其中存放在调用longjmp时能用来恢复栈状态的所有信息。因为需在另一个函数中引用env变量,所有通常将env变量定义为全局变量。当检查到一个错误时,则以两个参数调用longjmp函数,第一个就是在调用setjmp时所用的env,第二个参数是具有非0值的val,它将成为从setjmp处返回的值。使用val就可以对于一个setjmp有多个longjmp,通过返回值判断错误在何处。

修订版

jmp_buf jmpbuffer;

int main(void){
    char line[MAXLINE];

    if(setjmp(jmpbuffer)!=0) printf("error");
    while(fgets(line,MAXLINE,stdin)) do_line(line);
    exit(0);
}

void cmd_add(void){
    int token;
    if(token<0) longjmp(jmpbuffer,1);
}

执行main时,调用setjmp,它将所需的信息记入变量jmpbuffer中并返回0。然后调用do_line,又接着调用cmd_add,假定在longjmp之前检测到一个错误,longjmp使栈反绕到执行main函数时的情况,也就是抛弃了cmd_add和do_line的栈帧。从而使setjmp返回1。

注意调用longjmp时main函数中的自动变量和寄存器变量的值不确定,可能不变,可能回滚,故而声明自动变量的函数已经返回后,不能再引用这些自动变量。若不想让一自动变量的值回滚,则可定义其为具有volatile属性。声明为全局变量或静态变量的值在执行longjmp时保持不变。

资源限制getrlimit和setrlimit函数

每个进程都有一组资源限制,进程的资源限制通常是在系统初始化时由0进程建立的,然后由后续进程继承。其中一些可以用getrlimit和setrlimit函数查询和更改。

#include<sys/resource.h>
int getrlimit(int resource,struct rlimit *rlptr);
int setrlimit(int resource,const struct rlimit *rlptr);

若成功返回0,出错返回非0。

对这两个函数的每一次调用都指定一个资源以及一个指向下列结构的指针:

struct rlimit{
    rlim_t rlim_cur;         //软限制:当前限定值
    rlim_t rlim_max;         //硬限制:软限制的最大值
};

在更改资源限制时,须遵循3条规则:

  1. 任何进程都可将一个软限制值更改为小于或等于其硬限制值。
  2. 任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值。
  3. 只有超级用户可以提高硬限制值。

常量RLIM_INFINITY指定了一个无限量的限制。这两个函数的resource参数取下列值之一:
resource参数取值
resource参数取值

资源限制影响到调用进程并由其子进程继承。

进程控制

进程标识

每个进程都有一个非负整型表示的唯一进程ID。虽然唯一但进程ID是可复用的,当一个进程终止后,其进程ID就成为复用的候选者。一般使用延迟复用算法防止将新进程误认为是使用同一ID的某个已终止的先前进程。

系统有些专用进程。ID为0的进程通常是调度进程,常常被称为交换进程,该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1通常是init进程,在自举过程结束时由内核调用,此进程负责在自举内核后启动一个UNIX系统,init通常读取与系统有关的初始化文件,并将系统引导到一个状态,init进程决不会终止,它是一个以超级用户特权运行的普通用户进程(非内核中的系统进程),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

注意这些函数都没有出错返回。

创建新进程fork函数

一个现有的进程可以调用fork函数创建一个新进程:

#include<unistd.h>
pid_t fork(void);

子进程返回0,父进程返回子进程ID,如出错返回-1。

有fork创建的新进程称为子进程。fork函数被调用一次但返回两次,子进程返回0,父进程返回新建子进程的进程ID。理由在于一个进程的子进程有多个,但没有一个函数使一个进程可以获得其所有子进程的进程ID,而一个进程只会有一个父进程且可以调用getppid函数获得父进程的进程ID(进程ID 0总是由内核交换进程使用,故子进程的进程ID不可能为0)。

子进程和父进程继续执行fork调用后的指令。子进程是父进程的副本,例如子进程获得父进程数据空间、堆栈的副本,注意是副本,两者并不共享这些存储空间部分,子进程对变量所做的改变并不影响父进程中该变量的值。但如今很多实现并不执行完全副本,而是使用了写时复制(Copy-On-Write,COW)技术,这些区域两者共享,而内核将它们的访问权限改变为只读,若父进程或子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本。

父进程和子进程共享正文段。

注意在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。若要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。

文件共享

fork的一个特性是父进程的所有打开文件描述符都被复制到子进程,也就是说父进程和子进程每个相同的打开描述符共享一个文件表项。另外由于父进程和子进程共享同一个文件偏移量,所以若父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么它们的输出就会相互混合。

在fork之后处理文件描述符有以下两种常见的情况:

  1. 父进程等待子进程完成。此时父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新。
  2. 父进程和子进程各自执行不同的程序段。此时在fork后父进程和子进程各自关闭它们不需要使用的文件描述符,这样也不会干扰对方使用的文件描述符。

除打开文件外,父进程还有很多其它属性也由子进程继承:
继承的属性
继承的属性

父进程与子进程的区别如下:
区别

使fork失败的两个主要原因是(a)系统中已经有了太多进程。(b)该实际用户ID的进程总数超过了系统限制。

fork有两种用法:

  1. 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。
  2. 一个进程要执行一个不同的程序。

另一个创建新进程vfork函数

vfork函数的调用序列和返回值与fork相同,但两者的语义不同。

vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit,。不过在子进程调用exec或exit之前,它在父进程的空间中运行。

另外,vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数值中的任意一个时,父进程会恢复运行。(所以如果在调用这两个函数之前子进程依赖于父进程的进一步动作会导致死锁)

获取进程终止状态wait和waitpid函数

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。父进程可以选择忽略该信号(默认),或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。

当调用wait或waitpid时,进程的行为:

  • 若所有子进程都还在运行则阻塞
  • 若有一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回
  • 若它没有任何子进程,则立即出错返回

其中waitpid函数提供了wait函数没有的3个功能:

  1. waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态
  2. waitpid提供了一个wait的非阻塞版本
  3. waitpid通过WUNTRACED和WCONTINUED选项支持作业控制
#include<sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);

若成功返回进程ID,若出错返回0或-1

两函数区别如下:

  • 在一子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞
  • waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程

如果子进程已终止,并且是一个僵死进程,则wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞,直到一个子进程终止。若有多个子进程,则只要有一个子进程终止,wait就立即返回并得到其进程ID。

参数statloc

是一个整型指针。若statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。若不关心终止状态,则可将该参数指定为空指针。

整型状态字中某些位表示退出状态(正常返回),其他位则指示信号编号(异常返回),有一位指示是否产生了core文件等。可以使用以下4个互斥的宏是否为真来取得进程终止的原因:

说明
WIFEXITED(status) 正常终止的子进程,可执行WEXITSTATUS(status)来取得子进程传递给eixt或_eixt参数的低8位
WIFSIGNALED(status) 异常终止的子进程,可执行WTERMSIG(status)获取使子进程终止的信号编号
WIFSTOPPED(status) 当前暂停的子进程,可执行WSTOPSIG(status)获取使子进程暂停的信号编号
WIFCONTINUED(status) 作业控制暂停后已经继续的子进程,仅用于waitpid

参数pid

pid的值 说明
pid==-1 等待任一进程
pid>0 等待进程ID为pid的子进程
pid==0 等待组ID等于调用进程组ID的任一子进程
pid<-1 等待组进程ID等于pid绝对值的任一子进程

waitpid函数返回终止子进程的进程ID,并将该子进程的终止状态存放在由statloc指向的存储单元中。若指定的进程或进程组不存在,或参数pid指定的进程不是调用进程的子进程,则出错。

options参数

或为0,或为常量按位或运算的结果。

options的值 说明
0 不指定选项
WNOHANG 若pid指定的子进程尚未终止,则waitpid不阻塞,返回0
WCONTINUED 对实现支持作业控制,若pid指定的任一子进程在停止后已经继续,返回其状态
WUNTRACED 对实现支持作业控制,若pid指定的任一子进程已处于停止状态,返回其状态

另一获取进程终止状态waitid函数

#include<sys/wait.h>
int waitid(idtype_t idtype,id_t id,siginfo_t *infop,int options);

waitid不同于waitpid,它允许一个进程指定要等待的子进程,但它使用两个单独的参数表示要等待的子进程所属的类型,而不是将此与进程ID或进程组ID组合成一个参数。

id参数的作用与idtype有关:

常量 说明
P_PID 等待一特定进程,id包含要等待子进程的进程ID
P_PGID 等待一特定进程组中的任一子进程,id包含要等待子进程的进程组ID
P_ALL 等待任一子进程,忽略id

options参数是个常量的按位或运算,且WCONTINUED、WEXITED、WSTOPPED这3常量之一必须在options参数中指定。

常量 说明
WCONTINUED 等待一进程,它以前被停止,此后又已继续,但状态尚未报告
WEXITED 等待已退出的进程
WNOHANG 如无可用的子进程退出状态,立即返回而非阻塞
WNOWAIT 不破坏子进程退出状态
WSTOPPED 等待一进程,它已经停止但其状态尚未报告

infop参数是指向siginfo结构的指针。该结构包含了造成子进程状态改变有关信号的详细信息。

exec函数

exec是UNIX系统进程控制原语之一:fork创建新进程,exec执行新程序,exit和wait函数处理终止和等待终止。

存放在硬盘上的可执行程序文件能够被unix执行的唯一方法是:由一个现有进程调用六个exec函数中的某一个。当进程调用一种exec函数时,该进程执行的程序完全被替换为新程序,而新程序则从其main函数开始执行,因为调用exec并不创建新进程,所有前后的进程ID并未改变,exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。我们称调用exec的进程为调用进程,称新执行的程序为新程序。

7个exec函数之间的区别在于:
1. 待执行的程序文件是由文件名还是由路径名指定,最后一个取文件描述符
2. 新程序的参数是一一列出还是由一个指针数组来引用
3. 把调用进程的环境传递给新程序还是给新程序指定新的环境

函数名中字母p表示该函数取filename作为参数,并且用PATH环境变量寻找可执行文件。字母l表示该函数取一个参数表,v表示该函数取一个argv[]矢量。字母e表示该函数取envp[]数组,而不使用当前环境。

#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[]);

这些函数只在出错时才返回到调用者-1。若成功则不返回,控制将被传递给新程序的起始点,通常就是main函数。

只有execve是内核中的系统调用,其它6个都是调用execve的库函数,下图为7个exec函数的关系:
7个exec函数的关系

第一个参数

当指定filename为参数时:

  • 若filename中包含/,则就将其视为路径名
  • 否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件

PATH变量包含了一张目录表(称为路径前缀),目录之间用冒号(:)分隔,如PATH=/bin:/user/bin:/usr/local/bin:.。最后的路径前缀. 表示在当前目录。(零长前缀也表示当前目录。在value的开始处和行尾可用: 表示,在行中间则要用:: 表示)

execlp或execvp使用PATH环境变量,查找第一个包含名为filename的可执行文件的路径名前缀。若execlp或execvp使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则就认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入。

fexecve函数避免了寻找正确的可执行文件,而是依赖调用进程来完成这项工作。调用进程可以使用文件描述符验证所需要的文件并且无竞争地执行该文件。该函数使用/proc把文件描述符参数转换成路径名。

第二个参数

l表示每个命令行都说明为一个单独的参数,并要求以显式的空指针(NULL)作为命令行参数的结尾,如果用常量0来表示一个空指针则必须强制转换为一个指针(char*)0,否则将被解释为整型参数。若整型数的长度与char*的长度不同,那么exec函数的实际参数将出错。

v则先构造一个指向各参数的指针数组,然后将该数组地址作为参数。

第三个参数

以e结尾表示函数可以传递一个指向环境字符串指针数组的指针,若无则使用调用进程中的environ变量为新程序复制现有的环境。

前面提到的setenv和putenv函数,两者可更改当前环境和后面生成的子进程的环境,但不能影响父进程的环境

新程序继承的属性

在执行exec后,进程ID 没有改变,另外新程序还从调用进程继承了下列属性:
新程序继承的属性

若没有用fcntl设置了FD_CLOSEXEC标志,即执行时关闭标志,否则系统默认是在exec后仍保持进程中的打开描述符打开。

另外有效ID是否改变取决于所执行程序文件的设置用户ID为和设置组ID位是否设置。若新程序的设置用户ID位已设置,则有效用户ID变为程序文件所有者的ID,否则不变。组ID类似。

线程

线程标识

每个线程都包含表示执行环境所必需的信息,其中包含进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。

每个线程都有一个线程ID,进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。线程ID是用pthread_t数据类型来表示的,实现的时候可以用一个结构来代表pthread_t数据类型,所以必须使用一个函数来对两个线程ID进程比较。

#include<pthread.h>
int pthread_equal(pthread_t tid1,pthread_t tid2);

若相等则返回非0数值,否则返回0。

线程可以通过调用pthread_self函数获得自身的线程ID:

#include<pthread.h>
pthread_t pthread_self(void);

返回调用线程的线程ID。

工作队列实例

如图,主线程可能把工作任务放在一个队列中,用线程ID来控制每个工作线程处理哪些作业。主线程把新的作业放到一个工作队列中,由3个工作线程组成的线程池从队列中移出作业。主线程不允许每个线程任意处理从队列顶端取出的作业,而是由主线程控制作业的分配,主线程会在每个待处理作业的结构中放置处理该作业的线程ID,每个工作线程只能移出标有自己线程ID的作业。

线程创建

#include<pthread.h>
pthread_create(pthread_t *tidp,const pthread_attr_t *attr,void *(*start_rtn)(void*),void *arg);

若成功返回0,否则返回错误编码。

当pthread_create成功返回时,新创建线程的线程ID会被设置成tidp指向的内存单元。attr参数用于定制各种不同的线程属性,目前将其置为NULL表示创建一个具有默认属性的线程。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递的参数有一个以上,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。

线程创建时并不能保证哪个线程会先运行:是新创建的线程还是调用线程。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。

注意pthread函数在调用失败时通常返回错误码。

void * thr_func(void *arg){
    //线程运行函数
    return ((void*)0);             //注意返回值为void*类型
}
int main(void){
    pthread_t ntid;
    int err=pthread_create(&ntid,NULL,thr_func,NULL);  //线程属性默认,无传入参数
    if(err!=) err_exit("can't create thread");         //错误处理
    sleep(1);                                          //主线程等待新线程完成
    exit(0);
}

线程终止

单个线程可以通过3种方式退出:

  1. 线程可以简单地从启动例程中返回,返回值是线程的退出码。
  2. 线程可以被同一进程中的其他线程取消。
  3. 线程调用pthread_exit。
#include<pthread.h>
void pthread_exit(void *rval_ptr);

rval_ptr参数是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数访问到这个指针。

#include<pthread.h>
int pthread_join(pthread_t thread,void **rval_ptr);

若成功返回0,否则返回错误编码。

调用pthread_join函数的线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。如果线程简单地从它的启动例程返回,rval_ptr就包含返回码。如果线程被取消,由rval_ptr指定的内存单元就设置为PTHREAD_CANCELED。

可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL,尽管这种行为是与具体实现相关的。如果对线程的返回值并不感兴趣,那么可以把rval_ptr设置为NULL,此时调用pthread_join函数可以等待指定的线程终止,但并不获取线程的终止状态。

void thr_func(void *arg){
    //线程运行函数
    pthread_exit((void*)2);          //注意退出码仍为void*类型
}
int main(void){
    ptread_t tid;
    int err=pthread_create(&tid,NULL,thr_func,NULL);
    if(err!=0) err_exit("can't create thread");

    err=pthread_join(tid,&tret);                      //获取新线程的终止状态
    if(err!=0) err_exit("can't join with thread 1");  //调用失败
    printf("%ld",(long)tret);                          //输出退出码
}

由上述代码看到,当一个线程通过调用pthread_exit退出或者简单的从启动例程中返回时,进程中的其他线程可以通过调用pthread_join函数获得该线程的退出状态。

pthread_create和pthread_exit函数无类型指针参数可以传递不止一个值,比如它可以传递包含复杂信息的结构的地址,但要求吃结构所使用的内存在调用者完成调用以后必须仍然是有效的——例如线程栈上的临时变量就不行。

线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。

#include<pthread.h>
int pthread_cancel(pthread_t tid);

若成功返回0,否则返回错误编码。

在默认情况下,pthread_cancel函数会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数,但线程可以选择忽略取消或者控制如何被取消。pthread_cancel并不等待线程终止,它仅仅提出请求

线程可以安排它退出时需要调用的函数,称线程清理处理程序。一个线程可以建立多个清理处理程序。处理程序记录在栈中,这说明它们的执行顺序与它们注册时相反。

#include<pthread.h>
void pthread_cleanup_push(void (*rtn)(void*),void *arg);
void pthread_cleanup_pop(int execute);

当线程执行以下动作时,清理函数rtn是由pthread_cleanup_push函数调度的,调用时只有一个参数arg:

  • 调用pthread_exit时
  • 响应取消请求时
  • 用非零execute参数调用pthread_cleanup_pop时

注意如果execute参数设置为0,清理函数将不被调用。

void cleanup(void *arg){
    printf("%s",(char*)arg);
}
void thr_func(void *arg){
    pthread_cleanup_push(cleanup,"thread 1 push");      //调度清理函数并传入参数
    if(arg) pthread_exit((void*)1);                     //注意要使用exit退出而非return                        
    pthread_cleanup_pop(1);                             //线程退出前会执行清理函数
    pthread_exit((void*)2);
}

注意如果线程是通过从它的启动例程中返回而终止的话,它的清理处理程序就不会被调用。

在默认情况下,线程的终止状态会保存直到对该线程调用pthread_join。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,我们不能用pthread_join函数等待它的终止状态,因为对分离状态的线程调用pthread_join会产生未定义行为。可以调用pthread_detach分离线程:

#include<pthread.h>
int pthread_detach(pthread_t tid);

若成功返回0,否则返回错误编号。

可以看出线程函数与进程函数之间存在的相似之处,总结如下:
进程与线程原语比较

线程同步

当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。当一个线程可以修改的变量,其它线程也可以读取或者修改的时候,就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。

互斥量

互斥量是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。对互斥量进行加锁后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁,其它线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用,也就是说,每次只有一个线程可以向前执行。

互斥变量用pthread_mutex_t数据类型表示。在使用互斥变量之前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER(只适用于静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态分配互斥量,在释放内存前需要调用pthread_mutex_destroy。

#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

若成功返回0,否则返回错误编号。

要用默认的属性初始化互斥量,只需把attr设为NULL。

对互斥量进行加锁,需要调用pthread_mutex_lock。如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要调用pthread_mutex_unlock。

#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

若成功返回0,否则返回错误编号。

如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞直接返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,返回EBUSY。

当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock互斥量原语允许绑定线程阻塞时间。此函数与上述lock函数基本等价,但在达到超时时间值时,pthread_mutex_timedlock不会对互斥量进行加锁,而是返回错误码ETIMEDOUT。

#include<pthread.h>
#include<time.h>
int pthread_mutex_timedlock(pthread_mutex_t *mutex,const struct timespec *tsptr);

若成功返回0,否则返回错误编号。

超时指定愿意等待的绝对时间(在时间X之前可以阻塞等待而非阻塞X秒)。

读写锁

互斥量要么是锁住状态,要么就是不加锁状态,而是一次只有一个线程可以对其加锁。读写锁可以有3种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止。

与互斥量相比,读写锁在使用之前必须初始化,在释放它们底层的内存之前必须销毁。

#include<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock,const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

若成功返回0,否则返回错误编号。

同样attr为NULL时表示默认属性。在释放读写锁占用的内存之前,需要调用pthread_rwlock_destroy做清理工作。如果在调用pthread_rwlock_destroy之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就会丢失。

要在读模式下锁定读写锁,需要调用pthread_rwlock_rdlock,要在写模式下锁定读写锁,需要调用pthread_rwlock_wrlock。不管以何种方式锁住读写锁,都可以调用pthread_rwlock_unlock进行解锁。同样try表示可以获取锁时返回0,否则返回错误EBUSY。

#include<pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

若成功返回0,否则返回错误编号。

类似的定时的读写锁:

#include<pthread.h>
#include<time.h>
int pthread_rwlock_timerdlock(pthread_rwlock_t *rwlock,const struct timespec *tsptr);
int pthread_rwlock_timewrlock(pthread_rwlock_t *rwlock,const struct timespec *tsptr);

成功返回0,否则返回错误编号。

条件变量

条件变量是线程的另一种同步机制,条件变量给多个线程提供了一个回合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身由互斥量保护的,线程在改变条件状态之前必须首先锁住互斥量,其它线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。

在使用条件变量之前,必须先对它进行初始化,pthread_cond_t数据类型可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,也可以使用pthread_cond_init函数对动态分配的条件变量初始化。

在释放条件变量底层的内存空间之前,可以使用pthread_cond_destroy函数对条件变量进行反初始化(deinitialize)。

#include<pthread.h>
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t attr);
int pthread_cond_destroy(pthread_cond_t *cond);

成功返回0,否则返回错误编号。

使用pthread_cond_wait等待条件变量变为真。如果在给定的时间内条件不能满足,那么会生成一个返回错误码的变量。

#include<pthread.h>
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespec *tsptr);

若成功返回0,否则返回错误编号。

传递给pthread_cond_wait的互斥量对条件进行保护。调用者把锁住的互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量解锁。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。

如果超时到期时条件还是没有出现,pthread_cond_timewait将重新获取互斥量,然后返回错误ETIMEDOUT。两函数调用成功返回时,线程需要重新计算条件,因为另一个线程可能已经在运行并改变了条件。

下列两个函数用于通知线程条件已经满足:

#include<pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

若成功返回0,否则返回错误编号。

signal函数至少能唤醒一个等待该条件的线程,而broadcast函数则能唤醒等待该条件的所有线程。调用两函数时,我们说这是在给线程或者条件发信号。

struct msg{
    struct msg *m_next;
};
struct msg *workq;
pthread_cond_t qready=PTHREAD_COND_INITIALIZER;        //条件是工作队列的状态
pthread_mutext_t qlock=PTHREAD_MUTEX_INITIALIZER;

void process_msg(void){
    struct msg *mp;
    for(;;){
        pthread_mutex_lock(&qlock);
        while(workq==NULL){
            pthread_cond_wait(&qready,&qlock);         //等待队列插入了消息的条件为真
            mp=workq;
            workq=mp->m_next;
            pthread_mutex_unlock(&qlock);
        }
    }
}

void enqueue_msg(struct msg *mp){
    pthread_mutex_lock(&qlock);
    mp->m_next=workq;
    workq=mp;
    pthread_mutex_unlock(&qlock);                     //注意发信号时无需占有互斥量,故而先释放互斥量
    pthread_cond_signal(&qready);                     //给等待线程发信号表示条件为真
}

自旋锁

自旋锁与互斥量类似,但它是在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁可用于锁被持有的时间短且线程并不希望在重新调度上花费太多的成本。

注意当线程自旋等待锁变为可用时,CPU不能做其他的事情。自旋锁在非抢占式内核中非常有用:除了提供互斥机制外,它们还会阻塞中断,这样中断处理程序就不会让系统陷入死锁状态,因为它需要获取已被加锁的自旋锁。

很少用。

屏障

屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。之前提到的pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。但屏障允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出。所有线程达到屏障后可以接着工作。

int pthread_barrier_init(pthread_barrier_t *barrier,const pthread_barrierattr_t *attr,unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);

成功返回0,否则返回错误编号。

初始化屏障时,可以使用count参数指定在允许所有线程继续运行之前必须达到屏障的线程数目。attr为NULL表示使用默认属性。

wait函数表明线程已完成工作,准备等所有其他线程赶上来。

int pthread_barrier_wait(pthread_barrier_t *barrier);

若成功返回0或者PTHREAD_BARRIER_SERIAL_THREAD,否则返回错误编号。

调用wait的线程在屏障计数未满足条件时会进入休眠状态。如果该线程是最后一个调用wait的线程,就满足了屏障计数,所有的线程都被唤醒。对于一个任意线程,wait函数返回的是后一个常量,剩下的线程返回的是0,这样就可以使得一个线程为主线程,它可以工作在其它所有线程已完成的工作结果上。(但不需要判断是否为常量来决定哪个线程执行后续操作,其余线程可以直接退出或执行其余操作)

一旦达到屏障计数值,而是线程处于非阻塞状态,屏障就可以被重用,但屏障计数不会改变,除非destroy后又init设新值。

线程控制

线程特定数据

线程特定数据(thread-specific data),也称为线程私有数据,是存储和查询某个特定线程相关数据的一种机制。设置每个线程有自己单独的数据副本的原因在于:(a)需要维护基于每线程(pre-thread)的数据,因为线程ID并非小连续的整数,故而不能简单地分配一个线程数据数组。(b)它提供了让基于进程的接口适应多线程环境的机制。

一个进程中的所有线程都可以访问这个进程的整个地址空间。除了使用寄存器以外,一个线程没有办法阻止另一个线程访问它的数据。在分配线程特定数据之前,需要创建与该数据关联的键。这个键将用于获取对线程特定数据的访问:

int pthread_key_create(pthread_key_t *keyp,void (*destructor)(void*));

若成功返回0,否则返回错误编号。

创建的键存储在keyp指向的内存单元中,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程特定数据地址进行关联。创建新键时,每个线程的数据地址设为空值。

另外可以为该键关联一个可选择的析构函数。当这个线程退出时,如果数据地址已经被置为非空值,那么析构函数就会被调用,它唯一的参数就是该数据地址。如果传入的析构函数为空,就表明没有析构函数与这个键关联。当线程调用pthread_exit或者线程执行返回,正常退出时,析构函数就会被调用。同样线程取消时,只有在最后的清理处理程序返回之后,析构函数才会被调用。如果线程调用了exit、_exit、_Exit或abort,或者出现其他非正常的退出时,就不会调用析构函数。特别注意malloc和new函数一定需要销毁!

线程可以为线程特定数据分配多个键,每个键都可以有一个析构函数与它关联。每个键的析构函数可以互不相同,当然所有键也可以使用相同的析构函数。

线程退出时,线程特定数据的析构函数将按照操作系统实现中定义的顺序被调用。析构函数可能会调用另一个函数,该函数可能会创建新的线程特定数据,并且把这个数据与当前的键关联起来。当所有的析构函数都调用完成以后,系统会检查是否还有非空的线程特定数据值与键关联,如果有的话,再次调用析构函数。这个过程将会一直重复直到线程所有的键都为空线程特定数据值,或者已经做了PTHREAD_DESTRUCTOR_ITERATIONS中定义的最大次数的尝试。

另外我们可以通过调用delete来取消键与线程特定数据值之间的关联关系,但注意delete并不会激活与键关联的析构函数:

int pthread_key_delete(pthread_key_t key);

若成功返回0,否则返回错误编号。

确保所有线程看到的是一个键值需要使用once函数:

pthread_once _t initflag=PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag,void (*initfn)(void));

若成功返回0,否则返回错误编号。

initflag必须是一个非本地变量(如全局变量或静态变量),而且必须初始化为PTHREAD_ONCE_INIT。如果每个线程都调用pthread_once,系统就能保证初始化例程initfn只被调用一次,即系统首次调用pthread_once时。

创建键时避免出现冲突的一个正确方法如下:

void destructor(void *);
pthread_key_t key;
pthread_once_t init_done=PTHREAD_ONCE_INIT;

void thread_init(void){
    err=pthread_key_create(&key,destructor);
}
int threadfunc(void *arg){
    pthread_once(&init_done,thread_init);
}

键一旦创建以后,就可以通过调用setspecific函数把键和线程特定数据关联起来。可以通过getspecific函数获得线程特定数据的地址:

int pthread_setspecific(pthread_key_t key,const void *value);  //成功返回0,否则返回错误编号
void *pthread_getspecific(pthread_key_t key);    //返回线程特定数据值,若无关联值返回NULL

若没有线程特定数据值与键关联,get将返回一个空指针,我们可以根据这个返回值确定是否需要调用set。

线程和信号

每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。这意味着单个线程可以阻止某些信号,但当某个线程修改了与某个给定信号相关的处理行为后,所有的线程都必须共享这个处理行为的改变。这样,如果一个线程选择忽略某个给定信号,那么另一个线程就可以通过两种方式撤销上述线程的信号选择:恢复信号的默认处理行为或为信号设置一个新的信号处理程序。

进程中的信号是递送到单个线程的。若一个信号与硬件故障相关,那么该信号一般会被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。可以使用sigmask阻止信号发送:

#include<signal.h>
int pthread_sigmask(int how,const sigset_t *set,sigset_t *oset);

若成功返回0,否则返回错误编号。

set参数包含线程用于修改信号屏蔽字的信号集。how参数可以取下列3个值之一:

  1. SIG_BLOCK:把信号集添加到线程信号屏蔽字中
  2. SIG_SETMASK:用信号集替换线程的信号屏蔽字
  3. SIG_UNBLOCK:从线程信号屏蔽字中移除信号集

若oset参数不为空,线程之前的信号屏蔽字就存储在它指向的sigset_t结构中。线程可以通过把set参数设置为NULL,并把oset参数设置为sigset_t结构的地址,来获取当前的信号屏蔽字。

线程可以通过调用sigwait等待一个或多个信号的出现:

include<signal.h>
int sigwait(const sigset_t *set,int *signop);

若成功返回0,否则返回错误编号。

set参数指定了线程等待的信号集,返回时signop指向的整数将包含发送信号的数量。

若信号集中的某个信号在sigwait调用的时候处于挂起状态,那么sigwait将无阻塞地返回。在返回之前,sigwait将从进程中移除那些处于挂起等待状态的信号。若具体实现支持排队信号,并且信号的多个实例被挂起,那么sigwait将会移除该信号的一个实例,其他的实例还要继续排队。

为了避免错误行为的发生,线程在调用sigwait之前,必须阻塞那些它正在等待的信号。sigwait函数会原子地取消信号集的阻塞状态,直到有新的信号被递送。在返回之前,sigwait将恢复线程的信号屏蔽字。如果信号在sigwait被调用的时候没有被阻塞,那么在线程完成对sigwait的调用之前会出现一个时间窗,在这个时间窗中,信号就可以被发送给线程。

若多个线程在sigwait的调用中因等待同一个信号而阻塞,那么在信号递送的时候,就只有一个线程可以从sigwait中返回。若一个信号被捕获,且一个线程正在sigwait调用中等待同一信号,那么这时将由操作系统实现来决定以何种方式递送信号,可以让sigwait返回也可以激活信号处理程序。

要把信号发送给进程,可以调用kill。要把信号发送给线程,可以调用pthread_kill:

#include<signal.h>
int pthread_kill(pthread_t thread,int signo);

若成功返回0,否则返回错误编号。

可以传一个0值的signo来检查线程是否存在。若信号的默认处理动作是终止该进程,那么把信号传递给某个线程仍然会杀死整个进程。

线程和fork

当线程调用fork时,就为子进程创建了整个进程地址空间的副本。子进程通过继承整个地址空间的副本,还从父进程那儿继承了每个互斥量、读写锁和条件变量的状态。若父进程包含一个以上的线程,子进程在fork返回以后,如果紧接着不是马上调用exec的话,就需要清理锁状态。

在子进程内部,只存在一个线程,它是由父进程中调用fork的线程的副本构成的。若父进程中的线程占有锁,子进程将同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所有子进程没有办法知道它占有了哪些锁、需要释放哪些锁。若子进程从fork返回以后马上调用其中一个exec函数就可以避免这样的问题,此时旧的地址空间就被丢弃,所有锁的状态无关紧要。但若子进程需要继续处理工作的话就不行。

在多线程的进程中,为了避免不一致状态的问题,在fork返回和子进程调用其中一个exec函数之间,子进程只能调用异步信号安全的函数。

要清除锁状态,可以通过调用atfork函数建立fork处理程序:

#include<pthread.h>
int pthread_atfork(void (*prepare)(void),void (*parent)(void),void (*child)(void));

若成功返回0,否则返回错误编号。

用atfork函数最多可以安装3个帮助清理锁的函数。prepare fork处理程序由父进程在fork创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁。parent fork处理程序是在fork创建子进程以后、返回之前在父进程上下文中调用的,这个fork处理程序的任务是对prepare fork处理程序获取的所有锁进行解锁。child fork处理程序在fork返回之前在子进程上下文中调用,child fork处理程序也必须释放prepare fork处理程序获取的所有锁。

注意不会出现加锁一次解锁两次的情况!

可以多次调用atfork函数从而设置多套fork处理程序。若不需要使用其中某个处理程序,可以给特定的处理程序参数传入空指针,它就不会起任何作用。使用多个fork处理程序时,处理程序的调用顺序并不相同。parent和child fork处理程序是以它们注册时的顺序进行调用的,而prepare fork处理程序的调用顺序与它们注册时的顺序相反。

此函数十分不安全!实际应用范围稀少!慎重使用!

猜你喜欢

转载自blog.csdn.net/sinat_30477313/article/details/80384226
今日推荐