进程间通信

169 阅读26分钟

一.进程间通信的概念

前言:顾名思义,进程间通信就是不同进程进程之间传播或者交换信息。它的主要功能有:数据传输、资源共享、通知事件、进程控制。

进程间通信到底是什么?

实际上、进程间通信就是让不同的进程看见同一份资源。但是我们知道,各个进程之间具有独立性(数据层面上),因此想要实现该功能还是很困难的。

也是因为这个原因,进程间通信都是借用第三方数据--OS提供的一段内存,让通信进程通过该资源进行读写,由此实现通信,同时由于该资源可以由OS的不同模块提供,也诞生出了不同的进程间通信方式。

同一主机间进程的通信有多种方式:无名管道,有名管道,信号,共享内存,消息队列,信号量,存储映射。不同主机间通信有Socket,本文主要介绍同一主机通信,不同主机通信放在网络部分讲解。

二.无名管道——pipe

(一)什么是匿名管道

管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个“管道”。无名管道(匿名管道)就是只能用于父子进程通信的管道。

测试:统计当前云服务器上的登录用户个数。

image-20231221215808346

其中who和wc指令运行起来就是两个进程,"|"就是管道,who把数据写入管道,wc再从管道里读数据,至此完成数据传输。

(二)匿名通信的原理

如上图,就是匿名管道的原理,即让父子进程看到同一份资源,然后父子进程进行读写。

(三)匿名管道的特点

1.无名管道没有名字,只能在具有亲缘的进程中使用,如父子进程,兄弟进程。

2.无名管道不属于任何进程,有自己的大小,由操作系统管理,存放 在内存中(因为尽管采用文件方案,但是使用磁盘会降低IO效率)。

3.无名管道传输数据时是无格式的,需要管道两方约定好格式,比如多少字节算一个数据。

4.无名管道属于半双工通信,同一时刻只能单向流动。

5.无名管道的数据遵循先入先出的原则。

6.无名管道的数据只能从一端写入,一端写出

7.无名管道在内存中存在对应的缓冲区,不同操作系统中缓冲区的大小不相同。

8.从无名管道中读取数据是一次性操作,一旦数据被读走,数据就会从管道中被抛弃,释放空间以便写入更多的数据。

9.存在阻塞方式。

(四)创建匿名管道--pipe函数

#include <unistd.h>
//创建无名管道
int pipe(int pipefd[2]);
/**
* 当⼀个管道建⽴时,它会创建两个⽂件描述符 fd[0] 和 fd[1]。其中
* fd[0] 固定⽤于读管道,⽽ fd[1] 固定⽤于写管道。 
* ⼀般⽂件 I/O的函数都可以⽤来操作管道(lseek() 除外。)
* return 创建成功返回0,创建失败返回-1.
*/

接下来我们尝试创建无名管道,代码如下:

//子进程写,父进程读
//注意:管道只能单向通信,所以父子进程需要自己决定谁写谁读!!!
int main()
{
    int fd[2] = { 0 };
    if(pipe(fd) < 0)
    {
        perror("pipe");
        return 1;
    }

    pid_t id = fork();
    if(id == 0)
    {
        //子进程,关闭读端
        close(fd[0]);
        const char* msg = "hello father, I am child...";
        int count = 10;
        while(count--)
        {
            write(fd[1], msg, strlen(msg));
            sleep(1);
        }
        close(fd[1]);   //写完关闭文件
        exit(0);
    }
    //父进程
    close(fd[1]);   //父进程关闭写端
    char buff[64];
    while(1)
    {
        ssize_t s = read(fd[0], buff, sizeof(buff));
        if(s > 0)
        {
            buff[s] = '\0';
            printf("child send to father: %s\n", buff);
        }
        else if(s == 0)
        {
            printf("read file end\n");
            break;
        }
        else{
            printf("read error!\n");
            break;
        }  
    }
    close(fd[0]);
    waitpid(id, NULL, 0);
    return 0;
}

image-20231223154940477

(四)再认识管道

通过上面的代码,我们已经大致认识了管道,其中管道还有一些隐含的特点,接下来我们讲解一下:

  • 1.管道内部自带同步和互斥机制:一次只允许一个进程使用的资源叫做临界资源,管道在任一时刻都只允许一个进程对它操作,所以管道是临界资源,这样就可以对管道内的数据进行保护。互斥指的是同一时刻只能有一个进程使用临界资源,同步是两个及以上的进程在运行种协同步调,按照预定的顺序运行。

  • 2.管道的生命周期跟随进程:管道本质上是通过文件通信,当所有打开该文件的进程退出以后,该文件(管道)会被释放,即生命周期随进程。

  • 3.管道提供流式服务:即数据的写入读取都是任意的,没有明确的分割,该服务称为流式服务,对应的则为数据报服务。

  • 4.管道的大小:在2.6.11之前的Linux版本中,管道的大小和系统页面类型,往后的版本管道最大为65536字节。

之前我们介绍过管道是半双工通信,在此再补充下数据在线路上的传送方式:

  • 1.单工通信:单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。

  • 2.半双工通信:传输是双向的,但是不能同时传输,比如子进程读,父进程就不能读,父进程只能写

  • 3.全双工通信:传输是双向的,可以同时传输,相当于两个半双工通信的结合,类似下图。

(六)匿名管道读写进程的4种情况

1.只读不写

(1)写端不关闭,也不写,管道中没有数据,此时读端进程去读数据就会阻塞。未来有可能有数据递达,故阻塞等待,此时让出cpu。

(2)写端不关闭,也不写,管道中有残留数据,此时读端进程去读数据,直到read数据读完之后,返回实际读到的字节数,之后汇总阻塞。

2.写端关闭,读端一直读

与情况1类似,读进程去读读端管道的内容,读取全部最后返回0.

3.只写不读

所有读端不关闭,但是不读。

(1)管道未满,write会写入,直至写满,最后返回写入的字节数。

(2)管道已满,写端wtire会阻塞(因为要等待读端读取数据,无名管道本身容纳数据有限)。

4.读端关闭,写端一直写

所有读端关闭,写端进程则会收到信号(即异常终止),然后退出,也可以使用SIGPIPE捕捉信号(因为数据如果不被读取,就没有写的必要,写端进程故退出)。

简单总结下:对于读端,只要管道中有数据,就会一直读,最后read返回读取到的实际字节数,管道中没有数据,就会阻塞等待,因为未来可能有数据递达。对于写段,管道未满,write就会一直写入,最后返回实际写入的字节数,管道满了,write就会收到信号退出。

感兴趣的朋友可以试验下上述4种情况。

设置为非阻塞的方法

// 获取原来的flags
int flags = fcntl(fd[0], F_GETFL);
// 设置新的flags
flag |= O_NONBLOCK;
// flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);

三.命名管道——FIFO

(一)认识命名管道

命名管道(FIFO)不同于无名管道的地方在于它提供了一个路径与之关联,以FIFO的文件形式存在于文件系统中,这样即使于FIFO的创建不存在亲缘关系的进程,只要可以访问该路径就能够通过FIFO相互通信,因此,通过FIFO不相关的进程也可以交换数据。

与无名管道(pipe)的异同点:

1.FIFO在文件系统中是作为一个特殊的文件存在的,但FIFO中的内容也是放在内存中的。

2.当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。

3.FIFO有名字,不相关的进程可以通过打开命名管道通信。

(二)创建命名管道的方法

1.通过命名创建命名管道

image-20231223183635365

我们可以看到已生成一个fifo文件,此时就可以通过该文件进行两个进程间的通信,以下一个终端往fifo写数据,另一个读:

while :; do echo "hello fifo"; sleep 1; done > fifo
cat < fifo

以上也说明了,当读端关闭,写端就没有写的价值了,写端就会被OS用信号杀死,这里的进程是终端,故我们可以看到最后终端消失了。

当然也有别的创建fifo文件的方法,不过不常用,这里就不多于介绍了。

2.通过函数创建命名管道

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
/**	参数说明:
返回值:成功返回0,失败返回-1.
pathname:当以路径的形式给出,则会将fifo文件创建在该目录下,以文件名的方式给出,则在当前路径下创建该文件
mode:表示文件权限,参考open函数,设置的权限一般受掩码影响,可以先把掩码设置为0
**/

接下来实验一下:

//创建fifo文件
#define FILE_NAME "myfifo"

int main()
{
    umask(0);   //设置文件掩码为0
    if(mkfifo(FILE_NAME, 0666) < 0)
    {
        perror("mkfifo");
        return 1;
    }

    //创建成功
    return 0;
}

image-20231223191750713

(三)命名管道读写端的几种情况

1)一个为只读的文件打开一个管道的进程会阻塞直到另一个进程为只写打开该管道。

2)一个为只写的文件打开一个管道的进程会阻塞直到另一个进程为只读打开该管道。

3) 读管道:

与无名管道类似,管道中有数据,read会返回实际读到的字节数。管道中无数据,如果写端全关闭,则read返回0,相当于直接读到文件末尾;写端不关闭,则read阻塞等待。

4)写管道:

与无名管道类似,读端全部关闭则进程异常终止,也可以使用捕捉SIGPIPE信号终止进程。读端没关闭,如果管道已经满了,写端阻塞,管道未满,write写入数据,直至写满,并返回实际写入的字节数。

4.共享存储映射(共享内存)

消息队列,信号量,

存储映射和共享内存是一个东西吗????

内存映射函数呢????????????

简介

存储映射I/O(Memory-mapped I/O)使一个磁盘文件与存储空间中的一个缓冲区相映射,于是相当于从缓冲区中去读数据,就相当于读文件中的相应字节。同样的,数据存入缓冲区,则相应的字节就自动写入文。这样就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作,进程就可以直接通过读写内存来操作文件。

共享内存可以说是最好用的进程间通信方式,也是最快的IPC形式,因为进程可以直接读写内存,而不需要任何的数据拷贝。

!!!!!!!!!!!!画一张共享内存的图!!!!!!!!!!!!!!

共享内存创建和释放的步骤

共享内存的创建:

  1. 操作系统申请一块共享内存空间
  2. 操作系统通过页表将这块共享内存挂接到虚拟地址空间上

共享内存的释放:

  1. 取消共享内存和虚拟地址空间的映射
  2. 释放共享内存

创建共享内存

我们创建共享内存需要用到shmget函数 它的函数原型如下

  int shmget(key_t key, size_t size, int shmflg);
1

返回值

  • 如果创建成功 shmget函数会返回一个整型的内存标识符
  • 如果创建失败 shmget函数会返回-1

参数

  1. key_t key 待创建的共享内存在系统中的唯一标识
  2. size_t size 待创建的共享内存的大小
  3. int shmflg 创建共享内存的方式

参数详解

  1. key_t key

我们在前面说过 它是系统对于共享内存的唯一标识

那么我们怎么去创建这么一个唯一标识呢?

在linux中我们使用ftok函数去创建它 它的函数原型是

  key_t ftok(const char *pathname, int proj_id);
1

ftok函数的作用就是将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值 这个值被称为IPC键值

在使用shmget函数获取共享内存时 这个key值会被填充进维护共享内存的数据结构当中

需要注意的是 pathname所指定的文件必须存在且可存取

其实这个函数的底层就是一种算法 通过这个算法将路径的字符串和id转化为一个key_t类型的key值

所以说只要我们传入的路径和id是一样的最后生成的key值就是一样的

  1. size_t size

按照需要创建共享内存的大小 注意不要太大也不要太小

  1. int shmflg

这个格式我们见过很多次了 在文件操作中我们也是用一个整型来表示文件的打开方式

实际上我们是用其中比特位来判断打开哪些模式

常见的打开方式如下

组合作用
IPC_CREAT如果内核中不存在键值与key相等的共享内存 则新建一个共享内存并返回该共享内存的句柄 如果存在这样的共享内存 则直接返回该共享内存的句柄
IPC_CREAT l IPC_EXCL如果内核中不存在键值与key相等的共享内存 则新建一个共享内存并返回该共享内存的句柄 如果存在这样的共享内存 则出错返回

句柄: 标定某种资源能力的东西叫做句柄

  • 如果我们使用IPC_CREAT创建共享内存 我们一定能得到一个共享内存 但是不一定是新的
  • 如果我们使用IPC_CREAT | IPC_EXCL创建共享内存 我们如果得到了一个共享内存 它一定是最新的

查看共享内存

我们可以使用ipcs指令查看有关进程间通信设施的信息

当我们单独使用ipcs指令的时候会出现出消息队列、共享内存以及信号量相关的信息 如果只想知道其中的一个信息的话 我们可以在后面携带选项

  • -q:列出消息队列相关信息

  • -m:列出共享内存相关信息

  • -s:列出信号量相关信息

    共享内存的释放

    如果再次运行下sever程序 我们就会发现这样子的错误:FILE EXIT!

    这是因为共享内存的生命周期是随内核的 就算进程结束了共享内存也不会直接消失

    想要释放共享内存只有两种方式

    • 关机重启

    • 主动使用函数释放

    使用命令释放共享内存

    我们可以使用ipcrm -m shmid指令释放指定id的共享内存资源

我们一般使用shmctl函数来控制共享内存 它的函数原型如下

  int shmctl(int shmid, int cmd, struct shmid_ds *buf);
1

返回值:

  • 如果调用成功 返回0
  • 如果调用失败 返回-1

参数:

  • 第一个参数shmid 表示用户层面标识共享内存的句柄
  • 第二个参数cmd 表示具体的控制命令
  • 第三个参数buf 用于获取或设置所控制共享内存的数据结构

其中关于第二个参数 常用的命令有以下几个

选项作用
IPC_STAT获取共享内存的当前关联值 此时参数buf作为输出型参数
IPC_SET在进程有足够权限的前提下 将共享内存的当前关联值设置为buf所指的数据结构中的值
IPC_RMID删除共享内存段

共享内存的关联

我们一般使用shmat函数来进行共享内存和虚拟地址空间的关联 它的函数原型如下

  void *shmat(int shmid, const void *shmaddr, int shmflg);
1

返回值:

  • shmat调用成功 返回共享内存映射到进程地址空间中的起始地址
  • shmat调用失败 返回(void*)-1

参数:

  • 第一个参数shmid 表示待关联共享内存的用户级标识符
  • 第二个参数shmaddr 指定共享内存映射到进程地址空间的某一地址 通常设置为NULL 表示让内核自己决定一个合适的地址位置
  • 第三个参数shmflg 表示关联共享内存时设置的某些属性

其中它的第三个参数的选项如下表所示

选项作用
SHM_RDONLY关联共享内存后只进行读取操作
SHM_RND若shmaddr不为NULL 则关联地址自动向下调整为SHMLBA的整数倍 公式:shmaddr-(shmaddr%SHMLBA)
0默认为读写权限

5.信号

简介

信号是Linux进程间通信最古老的方式;信号是软件中断,它是在软件层次上对中断机制的模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。

信号的特点

1)通信方式比较简单

2)不能携带大量信息

3)满足某个特定条件才能发送

一个完整的信号周期

1)信号的产生

2)信号在进程中的注册,信号在进程中的注销

3)执行信号处理函数

信号编号

1)不存在编号为0的信号,也不存在3233信号。其中131号被成为常规信号,也叫普通信号或者标准信号。34~64称之为实时信号,与驱动编程和硬件相关。名字上区别不大,而前32个名字各不相同。

2)比较重要的一些,需要记住的几个信号

a.SIGINT:当用户按下了Ctrl + C组合键,用户中断向正在运行中的由该终端启动的程序发出此信号,终止进程。

b.SIGQUIT:当用户按下Ctrl + 组合键,用户终端向正在运行中的由该终端启动的称取发出某些信号,终止进程。

c.SIGSEGV:指示进程进行了无效内存访问(段错误),终止进程并产生core文件。

d.SIGPIPE Broken pipe:向一个没有读端的管道写数据,终止进程

e.SIGCHLD :子进程结束时,父进程会收到这个信号,并忽略这个信号。

信号四要素

1)编号:man 7 signal 查看文档了解信号编号

2)名称

3)事件

4)默认处理动作:

a. Term:终止进程

b.lgn:忽略信号(默认即时对该种信号忽略操作)

c.Core:终止进程,生产Core文件(查验死亡原因,用于gdb调试)

d.Stop:停止(暂停)进程

e.Cont:继续运行进程

注意:9号信号SIGKILL和19号SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作,甚至不能将其设置为阻塞。

信号的状态

1)信号的产生

a.当用户按下某些终端键时,将产生信号。

b.硬件异常将产生信号。

c.软件异常将产生信号

d.调用系统函数(如:kill、raise、abort)将发送信号

e.运行kill/killall命令将发送信号

2)未决状态:没有被处理。

3)递达状态:信号被处理了。

阻塞信号集和未决信号集

1)阻塞信号集(信号没处理)

将某些信号加入集合,对它们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(处理发生在解除屏蔽后)。

2)未决信号集合(即信号处理了,处理状态是不作为)

信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。

信号产生函数

sighandler_t signal(int signum, sighandler_t handler);
//修改进程对信号的默认处理动作

int signum 它代表了我们要处理的信号
sighandler_t handler 它代表了我们要替换的信号处理函数

1)kill函数(并不一定杀死进程,表示为给进程传递某个信号)

#include <sys/types.h>
#include <signal.h>
/**
* 给指定进程发送指定信号(不⼀定杀死).
* @param pid 取值有四种情况:
* pid > 0: 将信号传送给进程 ID 为pid的进程.
* pid = 0 : 将信号传送给当前进程所在进程组中的所有进程.
* pid = -1 : 将信号传送给系统内所有的进程.
* pid < -1 : 将信号传给指定进程组的所有进程,这个进程组号等于 pid 的
绝对值.
* @param sig 信号的编号,这⾥可以填数字编号,也可以填信号的宏定义.
* 可以通过命令 kill - l("l" 为字⺟)进⾏相应查看.
* 不推荐直接使⽤数字,应使⽤宏名,因为不同操作系统信号编号可能不同,但名
称⼀致.
* @return 成功: 0; 失败: -1.
* 普通⽤户基本规则是:发送者实际或有效⽤户ID == 接收者实际或有效⽤户ID.
*/int kill(pid_t pid, int sig);

2)raise函数

#include <signal.h>
/**
* 给当前进程发送指定信号(⾃⼰给⾃⼰发),等价于 kill(getpid(), sig).
* @param sig 信号编号.
* @return 成功: 0; 失败: ⾮0值.
*/
int raise(int sig)

raise和kill函数的区别:raise只能给当前进程发送信号,但是kill可以给任意进程发送,甚至给多个进程发送。

3)abort函数

#include <stdlib.h>
/**
* 给⾃⼰发送异常终⽌信号 6) SIGABRT,并产⽣core⽂件,等价于kill(getpid(),
SIGABRT).
*/
void abort(void)

4)alarm函数(闹钟)

#include <unistd.h>
/**
* 设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收
到该信号,默认动作终⽌。每个进程都有且只有唯⼀的⼀个定时器;
* 取消定时器alarm(0),返回旧闹钟余下秒数.
* @param seconds 指定的时间,以秒为单位.
* @return 返回0或剩余的秒数.
* /
unsigned int alarm(unsigned int seconds);

5)settiimer函数(定时器)

#include <sys/time.h>
struct itimerval {
 struct timerval it_interval; // 闹钟触发周期
 struct timerval it_value; // 闹钟触发时间
};
struct timeval {
 long tv_sec; // 秒
 long tv_usec; // 微秒
}
/**
* 设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时.
* @param which 指定定时⽅式:
* (1) ⾃然定时:ITIMER_REAL ! 14)SIGALRM计算⾃然时间;
* (2) 虚拟空间计时(⽤户空间):ITIMER_VIRTUAL ! 26)SIGVTALRM 
只计算进程占⽤cpu的时间;
* (3) 虚拟空间计时(⽤户空间):ITIMER_VIRTUAL ! 26)SIGVTALRM 
只计算进程占⽤cpu的时间.
* @param new_value 负责设定timeout时间.
* @param old_value 存放旧的timeout值,⼀般指定为NULL.
* @return 成功: 0; 失败: -1.
*/
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

// itimerval.it_value: 设定第⼀次执⾏function所延迟的秒数
// itimerval.it_interval: 设定以后每⼏秒执⾏function

8)自定义信号集函数

#include <signal.h>

int sigemptyset(sigset_t* set);//将set集合置空
int sigfillset(sigset_t* set);//将所有信号加入set集合
int sigaddset(sigset_t* set, int signo);//将signo信号加入set集合
int sigdelset(sigset_t* set, int signo);//将signo信号移除set集合
int sigismember(const sigset_t* set, int signo);//判断信号是否存在

除了sigismember外,其余操作函数中的set均为传出参数,sigset_t类型的本质是位图。

9)阻塞信号集

a.信号阻塞集也称信号屏蔽集,信号掩码.

b.信号阻塞集用来描述哪些信号递达到该进程的时候被阻塞。

c.sigprocmask函数

#include<signal.h>
/**检查或者修改阻塞集,根据how指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由set指定而原先的信号阻塞集合由oldset保存。

how信号阻塞集合的修改方法有三种情况:
(1)SIG_BLOCK:向信号阻塞集合中添加set信号集,新的信号掩码是set和旧信号掩码的并集,相当于 mask = mask | set;
(2)SIG_UNBLOCK: 从信号阻塞集合中删除set信号集,从当前信息掩码中取出set中的信号,相当于mask = mask &~ set;
(3)SIG_SETMASK:将信号阻塞集合设为set信号集,相当于原理信号阻塞集的内容清空,然后按照set中的信号重新设置信号阻塞集,相当于mask = set。

set:要操作的信号集地址,若set为NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到oldset中。
oldset:保存原先信号的阻塞地址。
返回值:成功返回0,失败返回-1,失败时错误代码只可能是EINVAL,表示参数how设置错误,不合法。**/

int sigprocmask(int how, const sigset_t set*, sigset_t oldset);
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<signal.h>

//自定义信号集函数--sigset, 和sigprocmask
int main()
{
    sigset_t iset;
    sigemptyset(&iset); //初始化iset
    sigaddset(&iset, 2);    //把2,9号信号加入iset
    sigaddset(&iset, 9);    
    sigprocmask(SIG_SETMASK, &iset, NULL);  //修改该进程的信号屏蔽字同时也接受之前的屏蔽字
	
    
    while(1)
    {
        printf("hello, I am running, ID is: %d\n", getpid());
        sleep(1);
    }

    return 0;

}

d.sigpending函数

#include<signal.h>
/**
读取当前进程的未决信号集。
set:未决信号集。
返回值:成功返回0,失败返回-1
**/
int sigpending(sigset_t* set);

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<signal.h>

//sigpending
void printPending(sigset_t *pending)
{
    int i = 0;
    for(i = 1; i < 32; i++)
    {
        if(sigismember(pending, i))
            printf("1 ");
        else    
            printf("0 ");
    }
    printf("\n");
}


//目的:阻塞2号信号,同时获取位图,
int main()
{
    sigset_t set, oset;
    sigemptyset(&set);
    sigemptyset(&oset); //清空数据集

    sigaddset(&set, 2); //加入2号信号
    sigprocmask(SIG_SETMASK, &set, &oset); //阻塞2号信号

    sigset_t pending;
    sigemptyset(&pending);
	printf("the pid is:%d\n", getpid());

    while(1)
    {
        sigpending(&pending);
        printPending(&pending);
        sleep(1);
    }
    return 0;

}
void printPending(sigset_t *pending)
{
    int i = 0;
    for(i = 1; i < 32; i++)
    {
        if(sigismember(pending, i))
            printf("1 ");
        else 
            printf("0 ");
    }

    printf(" ");
}

void handler(int signo)
{
    printf("handler signo:%d\n", signo);
}

int main()
{
    signal(2, handler);

    sigset_t set;
    sigset_t oset;
    sigemptyset(&set);
    sigemptyset(&oset);

    sigaddset(&set, 2); //
    sigprocmask(SIG_SETMASK, &set, &oset); //阻塞2号信号

    sigset_t pending;
    sigemptyset(&pending);

    int count = 0;
    while(1)
    {
        sigpending(&pending); //获取pending
        printPending(&pending);
        sleep(1);
        count++;
        if(count == 20)
        {
            sigprocmask(SIG_SETMASK, &oset, NULL); //恢复曾经屏蔽的信号
            printf("恢复信号屏蔽字\n");
        }
    }

    return 0;
}

10)信号捕捉

首先注意下:SIGKILL和SIGSTOP不能改变信号的处理方式,因为他们向用户提供了一种使进程终止的可靠办法

a.sigaction函数

#include<signal.h>
/**
检测或修改指定信号的设置,也可以同时执行这两种操作
signum:要操作的信号
act:要设置的对信号的新处理方式(传入参数)
oldact:原来对信号的处理方式(传出参数)
如果act指针非空,则要改变指定信号的处理方式(设置)
如果oldact指针非空,则系统将此前指定信号的处理方式存入oldact
返回值:成功返回0,失败返回-1
**/
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);

b.sa_handler、sa_sifaction:信号处理函数指针

和signal()里的函数指针用法一样,应根据情况给sa_sigaction、sa_handler两者之一赋值

其取值如下:

1)SIG_IGN:忽略该信号

2)SIG_DEL执行默认动作

3)处理函数名:自定义信号处理函数

c.sa_mask:信号阻塞集,在信号处理函数执行过程种,临时屏蔽指定的信号。

d.用于指定信号处理的行为,通常设置为0,表示使用默认属性,它可以是以下值得“按位或“组合:

1)A_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)

2)SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会受到SIGCHLD信号

3)SA_NOCLDWAIT:使父进程在他的子进程退出时不会受到SIGCHLD信号,这是子进程退出也不会成为僵尸进程。

4)SA_NODEFER:使对信号的屏蔽无效,集在信号处理函数执行期间仍能发出这个信号

5)SA_RESETHAND:信号处理之后重新设置为默认的处理方式

6)SA_SIGINFO:使用sa_sigaaction成员而不是sa-handler作为信号处理函数

11)struct sigaction结构体

struct sigaction{
void(*sa_handler)(int);//旧的信号处理函数指针
void(*sa_sigaction)(int, siginfo_t *, void *);//新的信号函数处理指针
sigset_t sa_mask;//信号阻塞集
int sa_flags;//信号处理的方式
void(*sa_restorer)(void); //已弃用
};

12)信号处理函数

/**
signum:信号的编号
info:记录信号发送进程信息的结构体
context:可以赋给指向ucontext_t类型对象的一个指针,以引用在传递信号时被中断的接受进程或线程的上下文。
**/

void(*sa_sigaction)(int signum, siginfo_t* info, void* context);

13)不可重入函数、可重入函数

当一个函数被设计成以下样子:不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。

这种情况在多线程、多进程的情况下很容易出现,这样的函数是不安全的,我们也叫做不可重入函数。

a.不可重入函数:

  • 函数体内使用了静态的数据结构

  • 函数体内调用了malloc()或者free()函数

  • 函数体内调用了标准I/O函数。

14)SIGHLD信号

  • 子进程终止时

  • 子进程收到SIGSTOP信号停止时。

  • 子进程在停止态,接受到SIGCONT后唤醒时

6.守护进程