进程通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间
  • 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
  • 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

Table of Contents

Linux 进程间通信(IPC)的发展

Linux 进程间通信(IPC)以下以几部分发展而来:

早期 UNIX 进程间通信、基于 System V 进程间通信、基于Socket进程间通信和POSIX进程间通信。 UNIX进程间通信方式包括:管道、FIFO、信号。 System V进程间通信方式包括:System V消息队列、System V信号灯、System V共享内存、

POSIX进程间通信包括:posix消息队列、posix信号灯、posix共享内存。

Linux 操作系统下进程间通信的主要方式

  1. 管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。
  2. 命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道在文件中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
  3. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。
  4. 消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
  5. 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
  6. 内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。
  7. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
  8. 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字

管道通信

普通的 Linux shell 都允许重定向,而重定向使用的就是管道。

例如: ps | grep vsftpd

管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。 写进程在管道的尾端写入数据,读进程在管道的道端读出数据。 数据读出后将从管道中移走,其它读进程都不能再读到这些数据。

管道提供了简单的流控制机制。 进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。 同样,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。

管道主要用于不同进程间通信

管道创建与关闭

创建一个简单的管道,可以使用系统调用 pipe()。 它接受一个参数,也就是一个包括两个整数的数组。 如果系统调用成功,此数组将包括管道使用的两个文件描述符。 创建一个管道之后,一般情况下进程将产生一个新的进程。

  • 系统调用: pipe();
  • 原型: int pipe(int fd[2]);
  • 返回值
    • 0: 系统调用成功
      • fd[0]用于读取管道,fd[1]用于写入管道。
    • -1: 系统调用失败
      • errno == EMFILE (没有空亲的文件描述符)
      • EMFILE(系统文件表已满)
      • EFAULT(fd数组无效)

管道的创建

int main(int, char **) {
    int pipefds[2];
    if (-1 == pipe(pipefds)) err(-1, "pipe");
    close(pipefds[0]);
    close(pipefds[1]);

    return 0;
}

管道的读写

管道主要用于不同进程间通信。 实际上,通常先创建一个管道,再通过 fork 函数创建一个子进程。图见附件。

子进程写入和父进程读的命名管道:图见附件

管道读写注意事项:

可以通过打开两个管道来创建一个双向的管道。 但需要在子理程中正确地设置文件描述符。 必须在系统调用 fork() 中调用 pipe(),否则子进程将不会继承文件描述符。 当使用半双工管道时,任何关联的进程都必须共享一个相关的祖先进程。 因为管道存在于系统内核之中,所以任何不在创建管道的进程的祖先进程之中的进程都将无法寻址它。 而在命名管道中却不是这样。管道实例见: pipe_rw.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>

#define BUFLEN 128U

int main(int, char **) {
    int pipefds[2];
    if (-1 == pipe(pipefds)) err(-1, "pipe");
    pid_t pid = fork();
    if (-1 == pid) err(-1, "fork");
    if (0 == pid) { // child process
        close(pipefds[1]);
        char buf[BUFLEN];
        if (-1 == read(pipefds[0], buf, BUFLEN)) err(-1, "read");
        printf("child: recv msg [%s] from parentn", buf);
        close(pipefds[0]);
    } else { // parent process
        close(pipefds[0]);
        const char *msg = "Hello World!";
        if (-1 == write(pipefds[1], msg, strlen(msg)+1)) err(-1, "write");
        printf("parent: write msg [%s] to childn", msg);
        close(pipefds[1]);
        errpro(-1 == waitpid(pid, NULL, 0), "waitpid");
    }

    return 0;
}
// 运行结果:
parent: write msg [Hello World!] to child
child: recv msg [Hello World!] from parent

标准流管道

与 Linux 中文件操作有文件流的标准 I/O 一样,管道的操作也支持基于文件流的模式。 接口函数如下:

  • 库函数:popen()
  • 原型:FILE *open (char *command,char *type);
  • 返回值
    • 如果成功,返回一个新的文件流
    • 如果无法创建进程或者管道,返回NULL

管道中数据流的方向是由第二个参数 type 控制的。 此参数可以是 r 或者 w,分别代表读或写。但不能同时为读和写。 在 Linux 系统下,管道将会以参数 type 中第一个字符代表的方式打开。 所以,如果你在参数 type 中写入 rw,管道将会以读的方式打开。

使用 popen() 创建的管道必须使用 pclose() 关闭。 其实,popen/pclose 和标准文件输入/输出流中的 fopen()/fclose() 十分相似。

  • 库函数:pclose()
  • 原型:int pclose(FILE *stream);
  • 返回值:返回系统调用wait4()的状态
    • 如果 stream 无效,或者系统调用 wait4() 失败,则返回 -1。

注意此库函数等待管道进程运行结束,然后关闭文件流。 库函数 pclose() 在使用 popen() 创建的进程上执行 wait4() 函数,它将破坏管道和文件系统。

流管道的例子

#define BUFLEN 128U

int main(int, char **) {
    const char *cmd = "ls -l";
    FILE *fp = popen(cmd, "r");
    errpro(NULL == fp, "popen");
    char buf[BUFLEN];
    while (NULL != fgets(buf, BUFLEN, fp)) {
        printf("read: %s", buf);
    }
    errpro(-1 == pclose(fp), "pclose");

    return 0;
}
// 运行结果:
read: total 64
read: -rw-r--r--  1 oxnz  staff   213 May  2 01:21 Makefile
read: -rw-r--r--  1 oxnz  staff  1034 May  7 20:31 Test.class
read: -rw-r--r--  1 oxnz  staff   456 May  7 20:42 Test.java
read: -rwxr-xr-x  1 oxnz  staff  9260 May  7 23:37 test
read: -rw-r--r--  1 oxnz  staff   454 May  7 23:37 test.cpp
read: drwxr-xr-x  3 oxnz  staff   102 May  7 23:37 test.dSYM
read: -rw-r--r--  1 oxnz  staff    15 May  7 20:05 test.txt

命名管道 (FIFO)

基本概念

命名管道和一般的管道基本相同,但也有一些显著的不同:

  • 命名管道是在文件系统中作为一个特殊的设备文件而存在的
  • 不同祖先的进程之间可以通过管道共享数据
  • 当共享管道的进程执行完所有的 I/O 操作以后,命名管道将继续保存在文件系统中以便以后使用

管道只能由相关进程使用,它们共同的祖先进程创建了管道。 但是,通过 FIFO,不相关的进程也能交换数据。

命名管道创建与操作

命名管道创建

#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *pathname,mode_t mode);

返回: 若成功则为0,若出错返回 -1

一旦已经用 mkfifo 创建了一个 FIFO,就可用 open 打开它。 确实,一般的文件 I/O 函数 (close,read,write,unlink, etc)都可用于 FIFO。

当打开一个 FIFO 时,非阻塞标 (O_NONBLOCK) 产生下列影响:

  1. 在一般情况中(没有说明 O_NONBLOCK ),只读打开要阻塞到某个其他进程为写打开此 FIFO。 类似,为写而打开一个 FIFO 要阻塞到某个其他进程为读而打开它。
  2. 如果指一了 O_NONBLOCK,则只读打开立即返回。 但是,如果没有进程已经为读而打开一个 FIFO,那么只写打开将出错返回,其 errno 是 ENXIO。 类似于管道,若写一个尚无进程为读而打开的 FIFO,则产生信号 SIGPIPE。 若某个 FIFO 的最后一个写进程关闭了该 FIFO,则将为该 FIFO 的读进程产生一个文件结束标志。

FIFO 相关出错信息:

  • EACCES(无存取权限)
  • EEXIST(指定文件不存在)
  • ENAMETOOLONG(路径名太长)
  • ENOENT(包含的目录不存在)
  • ENOSPC(文件系统余空间不足)
  • ENOTDIR(文件路径无效)
  • EROFS(指定的文件存在于只读文件系统中)

fifo_write.c

// fifo_write.c
#define BUFLEN 128U

int main(int, char **) {
    const char *path = "/tmp/test.fifo";
    if (-1 == mkfifo(path, O_CREAT | O_EXCL)) {
        if (EEXIST == errno) {
            printf("already existedn");
        } else {
            errpro(true, "mkfifo");
        }
    }
    int fd = open(path, O_RDONLY | O_NONBLOCK, 0);
    errpro(-1 == fd, "open");
    char buf[BUFLEN];
    for (;;) {
        errpro(-1 == read(fd, buf, BUFLEN), "read");
        printf("read [%s] from fifo [%s]n", buf, path);
        sleep(1);
    }
    errpro(-1 == close(fd), "close");
    errpro(-1 == unlink(path), "unlink");

    return 0;
}

fifo_read.c

// fifo_read.c
#define BUFLEN 128U

int main(int argc, char *argv[]) {
    const char *path = "/tmp/test.fifo";
    int fd = open(path, O_WRONLY | O_NONBLOCK);
    errpro(-1 == fd, "open");
    char *msg = "Hello world!";
    errpro(-1 == write(fd, argv[1], strlen(msg)+1), "write");
    errpro(-1 == close(fd), "close");

    return 0;
}

信号

信号概述

信号是软件中断。 信号(signal)机制是 Unix 系统中最为古老的进程之间的能信机制。 它用于在一个或多个进程之间传递异步信号。很多条件可以产生一个信号。

  • 当用户按某些终端键时,产生信号。在终端上按 CTRL-C 通常产生中断信号 (SIGINT)。这是停止一个已失去控制程序的方法。
  • 硬件异常产生信号: 除数为 0、无效的存储访问等等。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。例如,对于执行一个无效存储访问的进程产生一个 SIGSEGV。
  • 进程用 kill(2) 函数可将信号发送给另一个进程或进程组。自然,有些限制: 接收信号进和发送信号进程的所有都必须相同,或发送信号进程的的所有者必须是超级用户。
  • 用户可用 kill pid 命令将信号发送给其它进程。此程序是 kill 函数的界面。常用此命令终止一个失控的后台进程。
  • 当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。这里并不是指硬件产生条件(如被 0 除),而是软件条件。例如 SIGURG(在网络连接上传来非规定波特率的数据)、SIGPIPE(在管道的读进程已终止后一个进程写此管道),以及 SIGALRM(进程所设置的闹钟时间已经超时)。

内核为进程生产信号,来响应不同的事件,这些事件就是信号源。 主要信号源如下:

  1. 异常:进程运行过程中出现异常
  2. 其它进程:一个进程可以向另一个或一组进程发送信号
  3. 终端中断:CTRL-C, CTRL-D 等
  4. 作业控制:前台、后台进程的管理
  5. 分配额:CPU 超时或文件大小突破限制
  6. 通知:通知进程某事件发生,如 I/O 就绪等
  7. 报警:计时器到期

Linux 中的信号

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

常用的信号

  • SIGHUP:从终端上发出的结束信号
  • SIGINT:来自键盘的中断信号 (CTRL+C)
  • SIGQUIT:来自键盘的退出信号
  • SIGFPE:浮点异常信号(例如浮点运算溢出)
  • SIGKILL:该信号结束接收信号的进程
  • SIGALRM:进程的定时器到期时,发送该信号
  • SIGTERM:kill命令生出的信号
  • SIGCHLD:标识子进程停止或结束的信号
  • SIGSTOP:来自键盘(CTRL-Z) 或调试程序的停止扫行信号

可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作:

  • 忽略此信号。

    大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。它们是:SIGKILLSIGSTOP。这两种信号不能被忽略的,原因是:它们向超级用户提供一种使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法存储访问或除以0),则进程的行为是示定义的。

  • 捕捉信号。

    为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。如果捕捉到 SIGCHLD 信号,则表示子进程已经终止,所以此信号的捕捉函数可以调用 waitpid 以取得该子进程的进程ID以及它的终止状态。

  • 执行系统默认动作。

    对大多数信号的系统默认动作是终止该进程。 每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。 有 5 种缺省的动作:

    • 异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做 core 的文件中,而后终止进程。
    • 退出(exit):不产生core文件,直接终止进程。
    • 忽略(ignore):忽略该信号。
    • 停止(stop):挂起该进程。
    • 继续(contiune):如果进程被挂起,刚恢复进程的动行。否则,忽略信号。

信号的发送与捕捉

kill()raise()

kill() 不仅可以中止进程,也可以向进程发送其他信号。 与 kill 函数不同的是,raise() 函数运行向进程自身发送信号

#include<sys/types.h>
#include
int kill(pid_t pid,int signo);
int raise(int signo);

两个函数返回: 若成功则为 0,若出错则为 -1。

kill 的 pid 参数有四种不同的情况:

  • pid > 0: 将信号发送给进程 ID 为 pid 的进程。
  • pid == 0: 将信号发送给其进程组 ID 等于发送进程的进程组 ID,而且发送进程有许可权向其发送信号的所有进程。
  • pid < 0: 将信号发送给其进程组 ID 等于 pid 绝对值,而且发送进程有许可权向其发送信号的所有进程。如上所述一样,”所有进程”并不包括系统进程集中的进程。
  • pid == -1: POSIX.1 未定义种情况

kill.c

int main(int argc, char *argv[]) {
    pid_t pid = fork();
    errpro(-1 == pid, "fork");
    if (0 == pid) { // child
        raise(SIGSTOP);
        return 1;
    } else { // parent
        printf("child pid = %dn", pid);
        errpro(-1 == waitpid(pid, NULL, WNOHANG), "waitpid");
        errpro(-1 == kill(pid, SIGKILL), "kill");
    }

    return 0;
}

alarm 和 pause 函数

使用 alarm 函数可以设置一个时间值(闹钟时间),在将来的某个时刻时间值会被超过。 当所设置的时间被超过后,产生 SIGALRM 信号。如果不忽略或不捕捉引信号,则其默认动作是终止该进程。

#include<unistd.h>
unsigned int alarm(unsigned int secondss);

返回: 0 或以前设置的闹钟时间的余留秒数。

参数 seconds 的值是秒数,经过了指定的 seconds 秒后产生信号 SIGALRM。 每个进程只能有一个闹钟时间。 如果在调用 alarm 时,以前已为该进程设置过闹钟时间,而且它还没有超时,则该闹钟时间的余留值作为本次 alarm 函数调用的值返回。以前登记的闹钟时间则被新值代换。

如果有以前登记的尚未超过的闹钟时间,而且 seconds 值是 0,则取消以前的闹钟时间,其余留值仍作为函数的返回值。

pause 函数使用调用进程挂起直至捕捉到一个信号

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

返回: -1,errno 设置为 EINTR

只有执行了一信号处理程序并从其返回时,pause 才返回。

alarm.c

int main(int argc, char *argv[]) {
    alarm(1);
    pause();
    printf("wake up!n");

    return 0;
}

信号的处理

当系统捕捉到某个信号时,可以忽略该信号或是使用指定的处理函数来处理该信号,或者使用系统默认的方式。 信号处理的主要方式有两种: 一种是使用简单的 signal 函数,一种是使用信号集函数组。

signal()
#include<signal.h>
void (*signal (int signo,void (*func)(int)))(int)

返回: 成功则为以前的信号处理配置,若出错则为 SIG_ERR

func 的值是:

  • 常数 SIGIGN
  • 常数 SIGDFL
  • 当接到此信号后要调用的的函数的地址

如果指定 SIGIGN,则向内核表示忽略此信号(有两个信号 SIGKILL 和 SIGSTOP 不能忽略)。 如果指定 SIGDFL,则表示接到此信号后的动作是系统默认动作。 当指定函数地址时,我们称此为捕捉此信号。 我们称此函数为信号处理程序 (signal handler) 或信号捕捉函数 (signal-catching funcgion)。 signal 函数原型太复杂了,如果使用下面的 typedef,则可以使其简化。

type void sign(int);
sign *signal(int,handler *);

实例见:sigpro.c

void sigpro(int signo) {
    if (SIGINT == signo) {
        printf("Interruptedn");
    } else if (SIGQUIT == signo) {
        printf("quit signaln");
    } else {
        printf("unknow signaln");
    }
}

int main(int argc, char *argv[]) {
    errpro(SIG_ERR == signal(SIGINT, sigpro), "signal");
    errpro(SIG_ERR == signal(SIGQUIT, sigpro), "signal");
    pause();

    return 0;
}

信号集函数组

我们需要有一个能表示多个信号——信号集 (signal set) 的数据类型。 将在 sigprocmask() 这样的函数中使用这种数据类型,以告诉内核不允许发生该信号集中的信号。 信号集函数组包含水量几大模块:

  • 创建函数集
  • 登记信号集
  • 检测信号集

图见附件

创建函数集

#include<signal.h>
int sigemptyset(sigset_t* set);
int sigfillset(sigset_t* set);
int sigaddset(sigset_t* set,int signo );
int sigdelset(sigset_t* set,int signo);

四个函数返回: 若成功则为 0,若出错则为 -1

int sigismember(const sigset_t* set,int signo);

返回: 若真则为 1,若假则为 0;

  • signemptyset: 初始化信号集合为空
  • sigfillset:初始化信号集合为所有的信号集合
  • sigaddset:将指定信号添加到现存集中
  • sigdelset:从信号集中删除指定信号
  • sigismember:查询指定信号是否在信号集中

登记信号集

登记信号处理机主要用于决定进程如何处理信号。 首先要判断出当前进程阻塞能不能传递给该信号的信号集。这首先使用 sigprocmask 函数判断检测或更改信号屏蔽字,然后使用 sigaction 函数改变进程接受到特定信号之后的行为。

一个进程的信号屏蔽字可以规定当前阻塞而不能递送给该进程的信号集。 调用函数 sigprocmask 可以检测或更改(或两者)进程的信号屏蔽字。

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

返回: 若成功则为 0,若出错则为 -1 oset 是非空指针,进程是当前信号屏蔽字通过 oset 返回。 其次,若 set 是一个非空指针,则参数 how 指示如何修改当前信号屏蔽字。

用 sigprocmask 更改当前信号屏蔽字的方法。how 参数设定:

  • SIG_BLOCK 该进程新的信号屏蔽字是其当前信号屏蔽字和 set 指向信号集的并集。set 包含了我们希望阻塞的附加信号。
  • SIG_NUBLOCK 该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集的交集。set 包含了我们希望解除阻塞的信号。
  • SIG_SETMASK 该进程新的信号屏蔽是 set 指向的值。如果 set 是个空指针,则不改变该进程的信号屏蔽字,how 的值也无意义。

sigaction 函数的功能是检查或修改(或两者)与指定信号相关联的处理动作。 此函数取代了 UNIX 早期版本使用的 signal 函数。

#include<signa.h>

int sigaction(int signo,const struct sigaction* act,struct sigaction* oact);

返回:若成功则为0,若出错则为-1

参数 signo 是要检测或修改具体动作的信号的编号数。 若 act 指针非空,则要修改其动作。如果 oact 指针为空,则系统返回该信号的原先动作。 此函数使用下列结构:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

sa_handler 是一个函数指针,指定信号关联函数,可以是自定义处理函数,还可以 SIG_DEF 或 SIG_IGN;

sa_mask 是一个信号集,它可以指定在信号处理程序执行过程中哪些信号应当被阻塞。

sa_flags 中包含许多标志位,是对信号进行处理的各种选项。具体如下:

  • SA_NODEFER SA_NOMASK:当捕捉到此信号时,在执行其信号捕捉函数时,系统不会自动阻塞此信号。
  • SA_NOCLDSTOP:进程忽略子进程产生的任何 SIGSTOP、SIGTSTP、SIGTTIN 和 SIGTOU 信号
  • SA_RESTART:可让重启的系统调用重新起作用。
  • SA_ONESHOT SA_RESETHAND:自定义信号只执行一次,在执行完毕后恢复信号的系统默认动作。

检测信号是信号处理的后续步骤,但不是必须的。 sigpending 函数运行进程检测”未决”信号(进程不清楚他的存在),并进一步决定对他们做何处理。

sigpending 返回对于调用进程被阻塞不能递送和当前未决的信号集。

#include<signal.h>
int sigpending(sigset_t * set);

返回:若成功则为0,若出错则为-1

信号集实例见:sigaction.c

void sigpro(int signo) {
    if (SIGINT == signo) {
        printf("Interruptedn");
    } else if (SIGQUIT == signo) {
        printf("quit signaln");
    } else {
        printf("unknow signaln");
    }
}

int main(int argc, char *argv[]) {
    sigset_t sset, pendset;
    struct sigaction sa1, sa2;
    errpro(-1 == sigemptyset(&sset), "sigemptyset");
    errpro(-1 == sigaddset(&sset, SIGQUIT), "sigaddset");
    errpro(-1 == sigaddset(&sset, SIGINT), "sigaddset");
    errpro(-1 == sigprocmask(SIG_BLOCK, &sset, NULL), "sigprocmask");
    printf("blockedn");
    sleep(4);
    errpro(-1 == sigprocmask(SIG_UNBLOCK, &sset, NULL), "sigprocmask");
    printf("unblockedn");
    for (;;) {
        if (sigismember(&sset, SIGINT)) {
            errpro(-1 == sigemptyset(&sa1.sa_mask), "sigemptyset");
            sa1.sa_handler = sigpro;
            errpro(-1 == sigaction(SIGINT, &sa1, NULL), "sigaction");
        } else if (sigismember(&sset, SIGQUIT)) {
            sigemptyset(&sa2.sa_mask);
            sa2.sa_handler = SIG_DFL;
            errpro(-1 == sigaction(SIGTERM, &sa2, NULL), "sigaction");
        }
    }

    return 0;
}

Multi-thread Signal Processing

If multiple threads are executing within a process when a signal is delivered to it, the system must select a thread to process it. At the highest level, the selection of the thread is dictated by how the signal was generated, what action caused the signal, and what the effective target of the signal is.

There are three possiblities:

How signal was generated What generated the signal Effective target of the signal How the signal-processing thread is selected
Synchronously The system (because of an exception) A specific thread Always the offending thread
Synchronously An internal thread using pthread_kill A specific thread Always the targeted thread
Asynchronously An external process using kill The process as a whole Per-thread signal masks of all threads in the process
  • 如果是异常产生的信号(比如程序错误,像SIGPIPE、SIGEGV这些),则只有产生异常的线程收到并处理。
  • 如果是用pthread_kill产生的内部信号,则只有pthread_kill参数中指定的目标线程收到并处理。
  • 如果是外部使用kill命令产生的信号,通常是SIGINT、SIGHUP等job control信号,则会遍历所有线程,直到找到一个不阻塞该信号的线程,然后调用它来处理。(一般从主线程找起),注意只有一个线程能收到。

每个线程都有自己独立的signal mask,但所有线程共享进程的signal action。这意味着,你可以在线程中调用pthread_sigmask(不是sigmask)来决定本线程阻塞哪些信号。但你不能调用sigaction来指定单个线程的信号处理方式。如果在某个线程中调用了sigaction处理某个信号,那么这个进程中的未阻塞这个信号的线程在收到这个信号都会按同一种方式处理这个信号。另外,注意子线程的mask是会从主线程继承而来的。

sigaction 共享,每个线程不能按自己的方式处理信号

共享内存 (shared memory)

使得多个进程可以访问同一块内存空间,是最快的可用 IPC 形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

引用自朱云翔,胡平的《精通Unix下C语言编程与项目实践》之八-共享内存的系统调用

共享内存的基本系统调用包括创建共享内存、映射共享内存和释放共享内存映射三种,分别由函数shmget、函数shmat和函数shmdt完成。

1. 共享内存的创建

在Unix中,可以使用函数shmget来创建或获取共享内存,它的原型如下:

#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

函数shmget创建一个新的共享内存,或者访问一个已经存在的共享内存。参数key是共享内存的关键字。size指定了该共享内存的字节大小。参数shmflg的含义与消息队列函数msgget中参数msgflg的含义相类似。它的低9位决定了共享内存属主、属组和其它用户的访问权限,取值与表6-4的文件权限参数类似,但执行权限无意义。它的其它位指定了共享内存的创建方式,其取值与含义如下表所示:

消息队列创建方式参数

参数 描述
IPC_CREAT 创建共享内存,如果共享内存已经存在,就获取该共享内存的标识号。
IPC_EXCL 与宏IPC_CREAT一起使用,单独使用无意义,此时只能创建一个不存在的共享内存,如果内存已存在,则调用失败。

与消息队列类似,当参数key的取值为IPC_PRIVATE时,将创建关键字为0的共享内存,Unix内核可以同时存在多个关键字为0的共享内存。

函数shmget调用成功时,返回共享内存的标识符,否则返回-1。

例1. 创建关键字为0x1234,访问权限为0666,占用空间10K的共享内存,如果已存在则返回其标识号。

int shmid;
shmid = shmget(0x1234, 10*1024, 0666|IPC_CREAT);

例2. 创建关键字为0x1234,访问权限为0666,占用空间10K的共享内存,如果已存在则报错。

int shmid;
shmid = shmget(0x1234, 10*1024, 0666|IPC_CREAT|IPC_EXCL);

2. 共享内存的映射

与消息队列和信号量不同,共享内存在获取标识号后,仍需调用函数shmat将共享内存段映射到进程地址空间后才可以访问。函数shmat的原型如下:

#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

函数shmat将标识号为shmid共享内存映射到调用进程的地址空间中,映射的地址由参数shmaddrshmflg共同确定,其规则为:

  1. 如果参数shmaddr取值为NULL,系统将自动确定共享内存链接到进程空间的首地址。
  2. 如果参数shmaddr取值不为NULL且参数shmflg没有指定SHM_RND标志,系统将使用地址shmaddr链接共享内存。
  3. 如果参数shmaddr取值不为NULL且参数shmflg指定了SHM_RND标志位,系统将地址shmaddr对齐后链接共享内存。其中选项SHM_RND的意思是取整对齐,常数SHMLBA代表了低边界地址的倍数,公式“shmaddr - (shmaddr % SHMLBA)”的含义是将地址shmaddr移动到低边界地址的整数倍上。

以上规则可归纳如下所示:

共享内存地址映射规则

shmaddr shmflg 映射地址
NULL 系统自动
非NULL 未置SHM_RND标志位 shmaddr
非NULL 置SHM_RND标志位 shmaddr - (shmaddr % SHMLBA)

【实践经验】在绝大多数情况下,我们指定参数shmaddr值为NULL,以便系统自动选择映射地址。

除了SHM_RND标志,参数shmflg还可以设置SHM_RDONLY标志位,此时共享内存将以只读的方式映射到内存地址中。

函数shmat调用成功时返回共享内存映射空间的起始地址,否则返回-1并置errno错误信息。

例1. 将创建关键字为0x1234,占用空间10K的共享内存,链接到进程中。

int shmid;
char *pmat;
shmid = shmget(0x1234, 10*1024, 0666|IPC_CREAT);
pmat = shmat(shmid, 0, 0);

指针pmat指向共享内存映射空间的首地址。

3. 共享内存的释放

当进程不再需要共享内存时,可以使用函数shmdt释放共享内存映射,其原型如下:

#include <sys/shm.h>
int shmdt(const void *shmaddr);

函数shmdt释放进程在地址shmaddr处映射的共享内存,参数shmaddr必须为函数shmget的返回值。本函数调用成功时返回0,否则返回-1。

共享内存中有一个映射链接数,进程调用shmat成功时该链接数值自动增加1。调用函数shmdt并不能删除共享内存,它仅仅删除共享内存在进程中的一个链接,并将该共享内存映射数减1。

共享内存可以被进程映射多次,每次映射的首地址是不一样的。进程结束时,系统将自动检查进程中映射的共享内存并释放该映射。

【实践经验】虽然系统会自动释放共享内存在进程中的链接,但显式的调用shmdt释放内存映射是一个良好的编程习惯。

例1. 释放进程中地址paddr处的共享内存映射。

char *paddr;
int ret;
...
ret = shmdt(paddr);

内存映射(mapped memory)

内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。

信号量(semaphore)

引用自 http://blog.chinaunix.net/uid-24943863-id-3193530.html

并发导致竟态,从而导致对共享数据的非控制访问,产生非预期结果,我们要避免竟态的发生。遵循以下原则:

  1. 尽量避免资源共享;
  2. 显示地管理对共享资源的访问。管理技术通常为“锁定”或者“互斥”,保证任何时刻只有一个执行线程可操作共享资源。

几个重要的概念:

  1. 原子操作,顾名思义,就是说该操作是原子性的(原子是保持物质物理新性质的最小单位),不可分割的,亦即操作要么处于不间断地执行的状态,要么处于不执行的状态。
  2. 临界区,临界区是这么一段代码,这段代码在任意给定时刻只能被一个线程执行。
  3. 休眠,休眠是程序处于阻塞的状态,进程到达某个时间点,此时它不能进行任何处理(可能是等待某资源),它让出处理器给别的进程用,直到它能够完成自己的处理为止。相当于进程进入睡觉的状态,但它永远不知道要睡多长时间,也许它几天后才醒来,但是它会认为只是打了个盹。

信号量的本质是一个整数值,可以取0或者正整数,信号量为0表示该信号量标志的资源对当前进程不可用(可能其他线程正在使用),为正整数n表示允许唤醒n个线程对资源进行操作。信号量配合一对函数P和V使用,P是Proberen(荷兰语),测试的意思,V(Verhogen)是增加。P操作对应down()函数,测试信号量的值,若大于0,则信号量值减1,进程继续;若信号量等于0,那么调用进程休眠(调用sleep()),此时down()并没有结束(而是处于休眠状态)。P操作是原子操作,也就是说检查信号量和修改信号量(或者休眠,如果信号量为0的话)是一个单一的不可分割的操作。V操作对应于up()函数,完成对信号量的加1操作,如果有线程在等待该信号量而处于休眠状态(调用down()检查到信号量为0),则由系统唤醒其中一个线程完成down()函数。虽然此刻信号量又回到了0,但是处于等待的进程数少了一个,等该进程完成了对共享资源的访问,需要调用up()函数释放信号量,使得其他等待该信号量的线程能够继续。
要使用信号量,首先需要初始化,使用下面的函数进行初始化一个信号量,val用来指定信号量的初始值,即有最多有val 个进程可以并发访问down()和up()之间保护的资源。
void sema_init(struct semaphore *sem,int val)
内核中P操作有三个版本

void down(struct semaphore *sem)
int down_interruptible(struct semaphore *sem)
int down-trylock(struct semaphore *sem) //检查信号量后立即返回,不进入休眠

down_interruptible()是dwon()的可中断版本,也就是说用户可以中断正在等待该信号量的进程。这个函数是我们需要始终使用的版本。
down()和up()是成对调用的,up()函数用于释放信号量,其函数原型是
void up(struct semaphore *sem)
当调用sema_init()函数初始化信号量,如果val取1,那么该信号量就是一个互斥量!互斥量是只有两个状态的变量,解锁(0)和加锁(非零)。也就是说信号量可以实现互斥,但并不是所有互斥量都是信号量。信号量和互斥量就像有交集的两个集合。对于一些不能休眠的代码,如中断处理handle,不能使用信号量的down()函数,因为down会导致休眠,这时候可以使用自旋锁来实现互斥!自旋锁的工作原理与信号量颇为相似,spin_lock()不断检查锁是否可用,当锁可用时(处于解锁状态,互斥量为0),那么加锁(互斥量设置为非零),进程可以继续,进入临界区执行代码,当锁不可用时,那么spin_lock()进入忙循环并重复检查锁,直到锁可用,代码此刻在这里循环,像不像自旋(down()函数此时则进入休眠)。与信号量一样,使用自旋锁时必须对互斥量进行初始化,有两种方法可以完成初始化,一是声明锁时赋值,二是调用初始化函数传递实参

spinlock_t mylock=SPIN_LOCK_UNLOCKED
void spin_lock_init(spinlock_t *lock)

获得锁的函数(相当于信号量中的P操作)是
void spin_lock(spinlock_t *lock)
同样,用完之后需要释放锁,调用下面的代码
void spin_unlock(spinlock_t *lock)
由于自旋锁会造成死锁,因此需要小心使用。需要遵循以下规则:

  1. 任何拥有自旋锁的代码必须是原子的,它不能休眠!注意,copy_from_user,kmalloc等会导致休眠的函数在拥有spinlock的临界区是不能使用的。
  2. 在拥有自旋锁时禁止中断,使用函数spin_lock_irqsave()spin_lock_irq()代替spin_lock()
  3. 自旋锁必须在可能的最短时间内拥有。也就是锁自旋锁保护的临界区代码执行得越快越好。
  4. 必须避免获得锁的函数调用同样试图获得该锁的函数,否则会造成死锁。
  5. 在必须获得多个锁时,应该始终以相同的顺序获得。

SEM_OVERVIEW(7)           Linux Programmer's Manual          SEM_OVERVIEW(7)

NAME

       sem_overview - overview of POSIX semaphores

DESCRIPTION

       POSIX semaphores allow processes and threads to synchronize their
       actions.

       A semaphore is an integer whose value is never allowed to fall below
       zero.  Two operations can be performed on semaphores: increment the
       semaphore value by one (sem_post(3)); and decrement the semaphore
       value by one (sem_wait(3)).  If the value of a semaphore is currently
       zero, then a sem_wait(3) operation will block until the value becomes
       greater than zero.

       POSIX semaphores come in two forms: named semaphores and unnamed
       semaphores.

       Named semaphores
              A named semaphore is identified by a name of the form
              /somename; that is, a null-terminated string of up to
              NAME_MAX-4 (i.e., 251) characters consisting of an initial
              slash, followed by one or more characters, none of which are
              slashes.  Two processes can operate on the same named
              semaphore by passing the same name to sem_open(3).

              The sem_open(3) function creates a new named semaphore or
              opens an existing named semaphore.  After the semaphore has
              been opened, it can be operated on using sem_post(3) and
              sem_wait(3).  When a process has finished using the semaphore,
              it can use sem_close(3) to close the semaphore.  When all
              processes have finished using the semaphore, it can be removed
              from the system using sem_unlink(3).

       Unnamed semaphores (memory-based semaphores)
              An unnamed semaphore does not have a name.  Instead the
              semaphore is placed in a region of memory that is shared
              between multiple threads (a thread-shared semaphore) or
              processes (a process-shared semaphore).  A thread-shared
              semaphore is placed in an area of memory shared between the
              threads of a process, for example, a global variable.  A
              process-shared semaphore must be placed in a shared memory
              region (e.g., a System V shared memory segment created using
              shmget(2), or a POSIX shared memory object built created using
              shm_open(3)).

              Before being used, an unnamed semaphore must be initialized
              using sem_init(3).  It can then be operated on using
              sem_post(3) and sem_wait(3).  When the semaphore is no longer
              required, and before the memory in which it is located is
              deallocated, the semaphore should be destroyed using
              sem_destroy(3).

       The remainder of this section describes some specific details of the
       Linux implementation of POSIX semaphores.

   Versions
       Prior to kernel 2.6, Linux supported only unnamed, thread-shared
       semaphores.  On a system with Linux 2.6 and a glibc that provides the
       NPTL threading implementation, a complete implementation of POSIX
       semaphores is provided.

   Persistence
       POSIX named semaphores have kernel persistence: if not removed by
       sem_unlink(3), a semaphore will exist until the system is shut down.

   Linking
       Programs using the POSIX semaphores API must be compiled with cc
       -pthread to link against the real-time library, librt.

   Accessing named semaphores via the filesystem
       On Linux, named semaphores are created in a virtual filesystem,
       normally mounted under /dev/shm, with names of the form sem.somename.
       (This is the reason that semaphore names are limited to NAME_MAX-4
       rather than NAME_MAX characters.)

       Since Linux 2.6.19, ACLs can be placed on files under this directory,
       to control object permissions on a per-user and per-group basis.

CONFORMING TO

       POSIX.1-2001.

NOTES

       System V semaphores (semget(2), semop(2), etc.) are an older
       semaphore API.  POSIX semaphores provide a simpler, and better
       designed interface than System V semaphores; on the other hand POSIX
       semaphores are less widely available (especially on older systems)
       than System V semaphores.

EXAMPLE

       An example of the use of various POSIX semaphore functions is shown
       in sem_wait(3).

SEE ALSO

       sem_close(3), sem_destroy(3), sem_getvalue(3), sem_init(3),
       sem_open(3), sem_post(3), sem_unlink(3), sem_wait(3), pthreads(7)

COLOPHON

       This page is part of release 3.65 of the Linux man-pages project.  A
       description of the project, and information about reporting bugs, can
       be found at http://www.kernel.org/doc/man-pages/.

Linux                            2012-05-13                  SEM_OVERVIEW(7)

套接口(Socket)

更为一般的进程间通信机制,可用于不同机器之间的进程间通信。 起初是由 UNIX 系统的 BSD 分支出来的,但现在一般可以移植到其它类 Unix 系统上: Linux 和 System V 的变种都支持套接字

文件 (File)

参考