进程间通信

引言

进程间通信(interprocess communication,简称 IPC) 指两个进程之间的通信。 系统中的每一个进程都有
各自的地址空间,并且相互独立、隔离, 每个进程都处于自己的地址空间中。 所以同一个进程的不同模块(譬如不同的函数)之间进行通信都是很简单的,譬如使用全局变量等。但是,两个不同的进程之间要进行通信通常是比较难的,因为这两个进程处于不同的地址空间中;通常情况下,大部分的程序是不要考虑进程间通信的,因为大家所接触绝大部分程序都是单进程程序(可以有多个线程),对于一些复杂、大型的应用程序,则会根据实际需要将其设计成多进程程序,譬如 GUI、服务区应用程序等 。

进程间通信通常有:管道,消息队列,信号量,共享内存,socket,streams。其中socket和streams支持两个不同主机间的进程通信

管道

把一个进程连接到另一个进程的数据流称为管道,管道被抽象成一个文件,这种文件叫管道文件,都是该文件不属于如何文件系统。管道分为匿名管道和命名管道

匿名管道

匿名管道的特点:也称为无名管道,是最常用的管道

  • 半双工,同一时间只能由一端传送给另一端
  • 只能在父子或者兄弟进程间使用
  • 管道的本质是文件,管道文件不属于任何文件系统

pipe():用于创建一个匿名管道

 #include <unistd.h>
 int pipe(int pipefd[2]);

返回值:成功返回0,失败返回-1,并且设置errno。

参数介绍:

  • fd为文件描述符数组,其中fd[0]表示读端,fd[1] 表示写端

父子进程通信过程解析

  1. 父进程创建管道,得到两个文件描述符指向管道的两端;
  2. 父进程fork出子进程,子进程也有两个文件描述符指向同一个管道。
  3. 父进程关闭fd[0](读端),子进程关闭fd[1](写端),因为管道只支持单向通信。(也可父写子读)

示例:子进程写数据,父进程读数据

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
	int fd[2];
	pid_t pid;
	char buf[128]={0};
	if(pipe(fd)==-1)	//创建管道
	{
		perror("pipe error");
		exit(-1);
	}
	
	pid = fork();		//创建子进程
	switch(pid)
	{
		case -1:
			perror("fork error");
			exit(-1);
		case 0:			//子进程写数据,父进程读数据
			printf("我是子进程\r\n");
			close(fd[0]);		//关闭读功能
			write(fd[1],"hello father",strlen("hello father"));
			_exit(0);
		default:
			break;
	}
    
    //以下是父进程执行的内容
	sleep(1);
	printf("我是父进程\r\n");
	close(fd[1]);		//关闭写功能
	read(fd[0],buf,128);
	printf("read from child: %s\r\n",buf);
	wait(NULL);
	exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
我是子进程
我是父进程
read from child: hello father

命名管道

不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。说白了就是命名管道会以文件存储在系统中。

命名管道特征:

  1. 可以进行不相干进程间的通信
  2. 命名管道是一个文件,对于文件的相关操作对其同样适用
  3. 对于管道文件,当前进程操作为只读时,则进行阻塞,直至有进程对其写入数据
  4. 对于管道文件,当前进程操作为只写时,则进行阻塞,直至有进程从管道中读取数据

mkfifo():用于创建一个命名管道

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

返回值:成功都返回0,失败都返回-1,并会设置errno。

参数介绍:

  • path:创建的命名管道的全路径名
  • mode:指定了文件的读写权限

注意

  • 命名管道和匿名管道的使用方法法基本是相同的。只是使用命名管道时,必须先调用open()将其打开。因为命名管道是一个存在于硬盘上的文件,而匿名管道是存在于内存中的特殊文件。
  • 调用open()打开命名管道的进程可能会被阻塞。
    • 但如果同时用读写方式( O_RDWR)打开,则一定不会导致阻塞
    • 如果以只读方式( O_RDONLY)打开,则调用open()函数的进程将会被阻塞直到有写方打开管道;
    • 同样以只写方式( O_WRONLY)打开也会阻塞直到有读方式打开管道。

示例:

//服务端,读数据的进程

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
    umask(0);//将权限清0
    if(mkfifo("./file",0666|S_IFIFO)==-1 && errno!=EEXIST){//创建管道
        perror("mkfifo");
        return 1;
    }

    int fd = open("./file",O_RDONLY);//打开管道
    if(fd < 0){
        perror("open");
        return 2;
    }

    char buf[1024];
    while(1){
        buf[0] = 0;
        printf("请等待。。。\n");
        ssize_t s = read(fd,buf,sizeof(buf)-1);

        if(s > 0){
            buf[s-1] = 0;//过滤\n
            printf("服务器:%s\n",buf);
        }else if(s == 0){//当客户端退出时,read返回0
            printf("客户端退出,自己退出\n");
            break;
        }
    }
    close(fd);
    return 0;
}

//客户端,写数据的进程

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
int main()
{

    int fd = open("./file",O_WRONLY);//打开管道
    if(fd < 0){
        perror("open");
        return 1;
    }

    char buf[1024];
    while(1){
        printf("客户端:");
        fflush(stdout);
        ssize_t s = read(0,buf,sizeof(buf)-1);//向管道文件中写数据
        if(s > 0){
            buf[s] = 0;//以字符串的形式写
            write(fd,buf,strlen(buf));
        }
    }
    close(fd);
    return 0;
}

消息队列

是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

消息队列特点:

  1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
  3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取

头文件

#include <sys/msg.h>

ftok()

系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。

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

返回值:当成功执行的时候,一个key_t值将会被返回,否则 -1 被返回。

参数介绍:

  • pathname:传入一个路径(一般是当前路径“ . ”)。
  • proj_id:随便填写一个数(要做通信的话通信的另外一端要与这个数保持一致才能找到对应的icpID)。

创建或打开消息队列:

 int msgget(key_t key, int flag);

返回值:成功返回队列ID,失败返回-1。

参数介绍:

  • key:key是一个键值,由ftok获得。通信双方要想通信,key值要一样。
  • flag:标识函数的行为以及消息队列的权限
    • IPC_CREAT:创建消息队列
    • IPC_EXCL:检测消息队列是否存在
    • 位或权限位:消息队列位或权限位后可以设置消息队列的访问权限,格式和open函数的mode_t一样,但可执行权限未使用

添加消息

 int msgsnd(int msqid, const void *ptr, size_t size, int flag);

返回值:成功返回0,失败返回-1。

参数介绍:

  • msqid:消息对象ID。

  • ptr:要是的消息的结构体变量的地址

     // 消息结构
    struct msgbuf
    {
        long mtype;		//消息的类型
        char mtext[256];	//消息的内容
    };
    
  • size:消息正文的字节数(等于消息结构体的大小减去long类型的大小)

  • flag:0:msgsnd调用阻塞直到条件满足为止;IPC_NOWAIT:若消息没有立刻发送则调用该函数的进程会立刻返回

读取消息

int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);

返回值:成功返回消息数据的长度,失败返回-1

参数介绍:

  • msqid:消息队列的标识符,代表要从哪个消息队列中获取消息
  • ptr:存放消息的结构体地址
  • size:消息正文的字节数
  • type:感兴趣的消息类型,可以有以下几种类型
    • type=0:返回队列中第一个消息
    • type>0:返回队列中消息类型为type的消息
    • type<0:返回队列中消息类型小于等于msgtyp的绝对值的消息,如果这种消息有若干个,则取类型值最小的消息
  • flag:函数的控制属性
    • 0:msgrcv调用阻塞直到接收消息成功为止
    • MSG_NOERROR:若返回的消息字节数比nbytes字节数多,则消息就会截断到nbytes字节,且不通知消息发送进程;
    • IPC_NOWAIT:调用进程会立即返回,若没有收到消息则立即返回-1

消息队列的控制

对消息队列进行各种控制,如删除消息队列,修改消息队列的属性

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

返回值:成功返回0,失败返回-1。

参数介绍:

  • msqid:消息队列的标识符 c
  • md:函数功能的控制
    • IPC_RMID:删除由msqid标识的消息队列,将他从系统中删除并破坏相关的数据结
    • IPC_STAT:将msqid相关的数据结构中各个元素的当前值存入到由buf指向的结构中 IPC_SET
    • 将msqid相关的数据结构中的元素设置为由buf指向的结构中的对应值
  • buf:msqid_ds数据类型的地址,用来存放或更改消息队列的属性

示例

							/*进程A*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <string.h>

 // 消息结构
struct msgbuf
{
    long mtype;     //消息的类型
    char mtext[128];    //消息的内容
};

 int main()
 {
    key_t key;
    key = ftok(".",'z');	
    printf("key=%x\r\n",key);

    int msgid = msgget(key,IPC_CREAT|0777);
    if(-1 == msgid)
    {
        printf("msgget error\r\n");
        return -1;
    }

    //从队列中读取消息
    struct msgbuf readBuf;
    msgrcv(msgid,&readBuf,sizeof(readBuf.mtext),888,0);	//读取类型为888的数据
    printf("read from Que:%s\r\n",readBuf.mtext);

    //发送消息道队列
    struct msgbuf sendBuf = {888,"我是进程A"};	
    msgsnd(msgid,&sendBuf,strlen(sendBuf.mtext),0);	//发送类型为888的数据
     
     msgctl(msgid,IPC_RMID,NULL);	//删除消息队列
 }
 								/*进程B*/
 #include <stdio.h>
 #include <stdlib.h>
 #include <sys/msg.h>
 #include <string.h>

 struct msgbuf
{
    long mtype;     //消息的类型
    char mtext[128];    //消息的内容
};

int main()
{
    key_t key;
    key = ftok(".",'z');
    printf("key=%x\r\n",key);

    int msgid = msgget(key,IPC_CREAT|0777);
    if(-1 == msgid)
    {
        printf("msgget error\r\n");
        return -1;
    }

    //发送消息道队列
    struct msgbuf sendBuf = {888,"我是进程B"};
    msgsnd(msgid,&sendBuf,strlen(sendBuf.mtext),0);	//发送类型为888的数据

    //从队列中读取消息
    struct msgbuf readBuf;
    msgrcv(msgid,&readBuf,sizeof(readBuf.mtext),888,0);	//读取类型为888的数据
    printf("read from Que:%s\r\n",readBuf.mtext);
    
    msgctl(msgid,IPC_RMID,NULL);	//删除消息队列
}

共享内存

共享内存就是映射一段能被其它进程所访问的内存, 这段共享内存由一个进程创建, 但其它的多个进程
都可以访问, 使得多个进程可以访问同一块内存空间。共享内存是最快的 IPC 方式, 它是针对其它进程间
通信方式运行效率低而专门设计的, 它往往与其它通信机制, 譬如结合信号量来使用, 以实现进程间的同步
和通信

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

共享内存特点:

  1. 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
  2. 因为多个进程可以同时操作,所以需要进行同步。
  3. 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

共享内存操作步骤

  1. 创建共享内存/打开共享内存
  2. 连接:连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。
  3. 读写数据:
  4. 分离:并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
  5. 销毁:从系统中删除该共享内存。

头文件

#include <sys/shm.h>

创建或获取——shmget()

创建或获取一个共享内存。

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

返回值:成功返回共享内存ID(shmid),失败返回-1。

参数介绍:

  • key:key是一个键值,由ftok获得。用于唯一标识一块共享内存
  • size:共享内存大小。而如果引用一个已存在的共享内存,则将 size 指定为0 。
  • shmflg:该参数用于确定共享内存属性。标志位 | 内存权限
    • IPC_CREAT;IPC_EXCL
    • 值得注意PC_EXCL无法单独使用

连接——shmat()

创建共享内存后还不能直接使用,需要找到内存地址后才能使用,即连接。

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

返回值:连接成功返回共享内存在进程中的起始地址,失败返回-1。

参数介绍:

  • shmid:共享内存ID,即shmget的创建成功的返回值
  • shmaddr:用于确定将共享内存挂在进程虚拟地址哪个位置,一般填nullptr即可代表让内核自己确定位置。
  • shmflg:用于确定挂接方式,一般填0

分离——shmdt()

当使用共享内存完毕后,需要分离挂接的共享内存。

注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。

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

返回值:分离成功返回0,失败返回-1。

参数介绍:

  • shmaddr:为共享内存在进程中地址位置,一般填nullptr

控制——shmctl()

该接口本身用于控制共享内存,可用于销毁。

 #include <sys/ipc.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

返回值:成功返回0,失败返回-1。

参数介绍:

  • shmid:共享内存ID,即shmget的创建成功的返回值
  • cmd:函数功能的控制
    • IPC_RMID:销毁由shqid标识的共享内存
    • ···
  • buf:传nullptr。

ipcs:该指令为系统指令。使用时可以查看当前全部共享内存。ipcs -m

ipcrm:通过指定共享内存shmid,进行删除。ipcrm -m [shmid]

示例

							/*进程A*/
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>
int main()
{
    int shmid;
    key_t key;
    char *shmaddr;
    key = ftok(".",1);
    shmid = shmget(key,1024,IPC_CREAT|0666);  //创建共享内存
    if(-1==shmid)
    {
        printf("shmget error\r\n");
        return -1;
    }

    shmaddr = shmat(shmid,0,0);   //连接映射
    if(-1 == shmaddr)
    {
        printf("shmat error\r\n");
    }
    strcpy(shmaddr,"hello world");  //往共享内存映射在进程空间中的内存中写数据

    sleep(5);       //等待进程B读取完
    shmdt(shmaddr);     //分离
    shmctl(shmid,IPC_RMID,0);  //销毁
    printf("共享内存已销毁\r\n");
}
							/*进程B*/
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main()
{
    int shmid;
    key_t key;
    char *shmaddr;
    key = ftok(".",1);
    shmid = shmget(key,1024,0);  //打开共享内存
    if(-1 == shmid)
    {
        printf("shmget error\r\n");
        return -1;
    }

    shmaddr = shmat(shmid,0,0);   //连接映射
    if(-1 == shmaddr)
    {
        printf("shmat error\r\n");
    }
    
    printf("read data:%s\r\n",shmaddr);  //从共享内存映射在进程空间中的内存中读数据并打印

    shmdt(shmaddr);     //分离
    // shmctl(shmid,PC_RMID,nullptr);  //销毁,另一个进程A已经销毁
    printf("共享内存已销毁\r\n");
}

信号

信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够
打断程序当前执行的正常流程, 其实是在软件层次上对中断机制的一种模拟。 大多数情况下,是无法预测信
号达到的准确时间,所以,信号提供了一种处理异步事件的方法

信号的目的是用来通信的,用于通知接收信号的进程有某种事件发生,所以可用于进程间通信;除了用于进程间通信之外,进程还可以发送信号给进程本身。

信号的分类

linux系统下可以从两个角度对信号进行分类,从可靠性方面将信号分为可靠信号与不可靠信号,从实时性方面将信号分为实时信号与非实时信号。

可靠信号

可靠信号支持排队,不会丢失。编号34~64,可靠信号没有具体对应的名字,而是使用了 SIGRTMIN+N 或 SIGRTMAXN 的方式来表示

不可靠信号

编号 1~31 所对应的是不可靠信号

信号的处理方式有三种:忽略、捕捉和默认动作

进程对信号的处理

signal()函数

signal()函数是 Linux 系统下设置信号处理方式最简单的接口, 可将信号的处理方式设置为捕获信号、 忽略信号以及系统默认操作

#include <signal.h>
typedef void (*sig_t)(int);		//函数指针
sig_t signal(int signum, sig_t handler);

返回值:返回值: 此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR,并会设置 errno。

参数介绍:

  • signum: 此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名

  • handler: sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处
    理函数; SIG_IGN 或 SIG_DFL, SIG_IGN 表示此进程需要忽略该信号, SIG_DFL 则表示设置为系统默认操作。

  • sig_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数
    上,此时就可通过此参数来判断当前触发的是哪个信号。

示例

signal()函数将 SIGINT(2) 信号绑定到了一个用户自定的处理函数sig_handler(int sig)上, 当进程收到 SIGINT 信号后会执行该函数然后运行 printf 打印语句 。

当运行程序之后,程序会卡在 for 死循环处,此时在终端按下中断符 CTRL + C,系统便会给前台进程组中的每一个进程发送SIGINT 信号,我们测试程序便会收到该信号。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
	printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
    sig_t ret = NULL;
    ret = signal(SIGINT, (sig_t)sig_handler);		//设置信号处理方式
    if (SIG_ERR == ret)
    {
        perror("signal error");
        exit(-1);
    }
    /* 死循环 */
    while(1);
    exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig 
^CReceived signal: 2
^CReceived signal: 2
^CReceived signal: 2

注意

  1. 进程创建 当一个进程调用 fork()创建子进程时,其子进程将会继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕获函数的地址在子进程中是有意义的。
  2. 当一个应用程序刚启动的时候(或者程序中没有调用 signal()函数) , 通常情况下, 进程对所有信号的
    处理方式都设置为系统默认操作。

sigaction()函数

sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时
的行为施以更加精准的控制。

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

返回值:成功返回 0;失败将返回-1,并设置 errno。

参数介绍:

  • signum: 需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
  • act: 指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式,如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
  • oldact:指向一个 struct sigaction 数据结构。如oldact 不为 NULL, 将信号之前的处理方式等信息通过参数 oldact 返回出来;为NULL表示不获取之前信号信息。

**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);
};
  • sa_handler:指定信号处理函数,与 signal()函数的 handler 参数相同。
  • sa_sigaction:也用于指定信号处理函数,这是一个替代的信号处理函数,可以通过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取(自行查找资料); sa_handler 和sa_sigaction 是互斥的,不能同时设置, 对于标准信号来说, 使用 sa_handler 就可以了,可通过SA_SIGINFO 标志进行选择
  • sa_mask:信号掩码可以避免一些信号之间的竞争状态(也称为竞态)。
  • sa_flags:SA_SIGINFO如果设置了该标志,则表示使用 sa_sigaction 作为信号处理函数、而不是 sa_handler,设置为0表示使用sa_handler。
  • sa_restorer:该成员已过时,不要再使用了。

示例:与signal()函数示例的功能相同

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
	printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
    int ret;
    struct sigaction sig = {0};		//初始化结构体
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;				//值为0,调用sa_handler保存的函数
    ret = sigaction(SIGINT, &sig, NULL);
    if (-1 == ret) {
    perror("sigaction error");
    exit(-1);
    }
    /* 死循环 */
    while(1);
    exit(0);
    }
}  

向进程发送信号

与 kill 命令相类似, Linux 系统提供了 kill()系统调用,一个进程可通过 kill()向另一个进程发送信号;
除了 kill()系统调用之外, Linux 系统还提供了系统调用 killpg()以及库函数 raise(),也可用于实现发送信号
的功能 。

kill()函数

可将信号发送给指定的进程或进程组中的每一个进程

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

返回值:成功返回 0;失败将返回-1,并设置 errno。

参数介绍:

  • pid:用于指定接收此信号的进程 pid;除此之外,参数 pid 也可设置为 0 或-1 以及小于-1 等不同值,稍后给说明
    • pid为正:则信号 sig 将发送到 pid 指定的进程
    • pid等于0:则将 sig 发送到当前进程的进程组中的每个进程。
    • pid等于-1:则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外。
    • pid 小于-1,则将 sig 发送到 ID 为-pid 的进程组中的每个进程。
  • sig: 指定需要发送的信号,设置为 0表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在。 如果向一个不存在的进程发送信号, kill()将会返回-1, errno 将被设置ESRCH,表示进程不存在

注意:进程发送信号给另外一个进程需要权限

  • 超级用户root 进程可以将信号发送给任何进程
  • 非超级用户(普通用户)进程来说,其基本规则是发送者进程的实际用户 ID 或有效用户 ID 必须等于接收者进程的实际用户 ID 或有效用户 ID

示例: sigaction()函数的示例代码用于接收该信号

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    int pid;
    /* 判断传参个数 */
    if (2 > argc)
    exit(-1);
    /* 将传入的字符串转为整形数字 */
    pid = atoi(argv[1]);
    printf("pid: %d\n", pid);
    /* 向 pid 指定的进程发送信号 */
    if (-1 == kill(pid, SIGINT)) 
    {
        perror("kill error");
        exit(-1);
    }
    exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./testApp &	#接收信号进程挂后台运行,进程号为8879
[1] 8879
ten@ten-virtual-machine:~/H616/demo$ ./testApp2 8879	#给8879进程发信号
pid: 8879
Received signal: 2										#接收进程的打印

raise()函数

用于进程向自身发送信号,raise(sig)等价于kill(getpid(), sig);

#include <signal.h>
int raise(int sig);

返回值: 成功返回 0;失败将返回非零值。

参数介绍:sig 为需要发送的信号。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
//信号处理函数
static void sig_handler(int sig)
{
	printf("Received signal: %d\n", sig);
}

int main(int argc, char *argv[])
{
	int ret;
    struct sigaction sig = {0};
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    ret = sigaction(SIGINT, &sig, NULL);
    if (-1 == ret) 
    {
        perror("sigaction error");
        exit(-1);
    }
    while(1)
    {
        /* 向自身发送 SIGINT 信号 */
        if (0 != raise(SIGINT)) 
        {
            printf("raise error\n");
            exit(-1);
    	}
    	sleep(3); // 每隔 3 秒发送一次
    }
    exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig 
Received signal: 2
Received signal: 2
Received signal: 2

alarm()和 pause()函数

alarm()函数

使用 alarm()函数可以设置一个定时器(闹钟) ,当定时器定时时间到时,内核会向进程发送 SIGALRM
信号 。补充:SIGALRM 信号的系统默认操作是终止进程 。

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

返回值: 如果在调用 alarm()时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0。

seconds: 设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟。

注意: alarm 闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在 SIGALRM 信号处理函数中再次调用 alarm()函数设置定时器。

示例

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    puts("Alarm timeout");
    exit(0);
}
int main(int argc, char *argv[])
{
    int second;
    struct sigaction sig = {0};
    
    /* 检验传参个数 */
    if (2 > argc)
    	exit(-1);
    
    /* 为 SIGALRM 信号绑定处理函数 */
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGALRM, &sig, NULL))
    {
        perror("sigaction error");
        exit(-1);
	}
    /* 启动 alarm 定时器 */
    second = atoi(argv[1]);
    printf("定时时长: %d 秒\n", second);
    alarm(second);
    /* 循环 */
    while(1)
    {
    	sleep(1);
    }
    exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig 5
定时时长: 5 秒
Alarm timeout

pause()函数

pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信
号处理函数并从其返回时, pause()才返回,在这种情况下, pause()返回-1,并且将 errno 设置为 EINTR。

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

示例:通过 alarm()和 pause()函数模拟 sleep 功能。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

static void sig_handler(int sig)
{  
	puts("Alarm timeout");
}

int main(int argc, char *argv[])
{
	int second;
    struct sigaction sig = {0};
    
    /* 检验传参个数 */
    if (2 > argc)
    	exit(-1);
    	
    /* 为 SIGALRM 信号绑定处理函数 */
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGALRM, &sig, NULL)) 
    {
        perror("sigaction error");
        exit(-1);
    }
    /* 启动 alarm 定时器 */
    second = atoi(argv[1]);
    printf("定时时长: %d 秒\n", second);
    alarm(second);
    
    /* 进入休眠状态 */
    pause();
    puts("休眠结束");
    exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig 3
定时时长: 3 秒
Alarm timeout
休眠结束

信号集

一个能表示多个信号(一组信号)的数据类型---信号集(signal set),很多系统调用都
使用到了信号集这种数据类型来作为参数传递,譬如 sigaction()函数、 sigprocmask()函数、 sigpending()函数
等。

信号集其实就是 sigset_t 类型数据结构

# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
	unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;

使用这个结构体可以表示一组信号,将多个信号添加到该数据结构中, 当然 Linux 系统封装了用于操作
sigset_t 信号集的 API,譬如 sigemptyset()、 sigfillset()、 sigaddset()、 sigdelset()、 sigismember()。

初始化信号集

sigemptyset():初始化信号集,使其不包含任何信号

sigfillset():初始化信号集,使其包含所有信号(包括所有实时信号)。

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

返回值: 成功返回 0;失败将返回-1,并设置 errno 。

set: 指向需要进行初始化的信号集变量。

示例

//初始化为空信号集
sigset_t sig_set;
sigemptyset(&sig_set);
//初始化信号集,使其包含所有信号:
sigset_t sig_set;
sigfillset(&sig_set);

向信号集中添加/删除信号

sigaddset():向信号集中添加信号

sigdelset():向信号集中移除信号

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

返回值: 成功返回 0;失败将返回-1,并设置 errno。

set: 指向信号集。

signum: 需要添加/删除的信号。

示例:

//添加信号
sigset_t sig_set;
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
//移除信号
sigset_t sig_set;
sigfillset(&sig_set);
sigdelset(&sig_set, SIGINT);

测试信号是否在信号集中

sigismember():可以测试某一个信号是否在指定的信号集中。

#include <signal.h>
int sigismember(const sigset_t *set, int signum);

返回值: 如果信号 signum 在信号集 set 中,则返回 1;如果不在信号集 set 中,则返回 0;失败则返回-
1,并设置 errno。

set: 指定信号集

signum: 需要进行测试的信号。

示例:判断 SIGINT 信号是否在 sig_set 信号集中 。

sigset_t sig_set;
......
if (1 == sigismember(&sig_set, SIGINT))
puts("信号集中包含 SIGINT 信号");
else if (!sigismember(&sig_set, SIGINT))
puts("信号集中不包含 SIGINT 信号");

获取信号描述信息

在 Linux 下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述。这些字
符串位于 sys_siglist 数组中, sys_siglist 数组是一个 char *类型的数组,数组中的每一个元素存放的是一个字
符串指针,指向一个信号描述信息。譬如,可以使用 sys_siglist[SIGINT]来获取对 SIGINT 信号的描述。

Tips:使用 sys_siglist 数组需要包含<signal.h>头文件

补充:在某些较新的系统或者不同的编译环境下,sys_siglist 可能不能直接可用。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    printf("SIGINT 描述信息: %s\n", sys_siglist[SIGINT]);
    printf("SIGQUIT 描述信息: %s\n", sys_siglist[SIGQUIT]);
    printf("SIGBUS 描述信息: %s\n", sys_siglist[SIGBUS]);
    exit(0);
}

strsignal()函数 :

用来获取信号的描述字符串,推荐使用这种方案。

#include <string.h>
char *strsignal(int sig);  

返回值:sig信号描述信息字符串的指针 。函数会对参数 sig 进行检查,若传入的 sig 无效,则会返回"Unknown signal"信息 。

sig:指定信号

示例:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    printf("SIGINT 描述信息: %s\n", strsignal(SIGINT));
    printf("SIGQUIT 描述信息: %s\n", strsignal(SIGQUIT));
    printf("SIGBUS 描述信息: %s\n", strsignal(SIGBUS));
    printf("编号为 1000 的描述信息: %s\n", strsignal(1000));
    exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
SIGINT 描述信息: Interrupt
SIGQUIT 描述信息: Quit
SIGBUS 描述信息: Bus error
编号为 1000 的描述信息: Unknown signal 1000

psignal()函数

可以在标准错误(stderr)上输出信号描述信息

补充:stderr不存在缓冲区,也就是说stderr的输出内容会直接打印在屏幕上

#include <signal.h>
void psignal(int sig, const char *s);

sig:指定信号

s:调用者需要添加的一些输出信息

示例:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    psignal(SIGINT, "SIGINT 信号描述信息");
    psignal(SIGQUIT, "SIGQUIT 信号描述信息");
    psignal(SIGBUS, "SIGBUS 信号描述信息");
    exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
SIGINT 信号描述信息: Interrupt
SIGQUIT 信号描述信息: Quit
SIGBUS 信号描述信息: Bus error

信号掩码

内核为每一个进程维护了一个信号掩码(其实就是一个信号集) ,即一组信号。当进程接收到一个属于
信号掩码中定义的信号时,内核会将该信号进行阻塞、导致无法传递给进程进行处理,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理 。:只是将该信号阻塞,并不是删除该信号了(可以理解为把信号堵在管道里了,当从信号掩码中移除信号时,该信号又就能出来了)。

向信号掩码中添加一个信号,通常有如下几种方式:

  • 当应用程序调用 signal()或 sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加
    到信号掩码中, 这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;
    当然对于 sigaction()而言,是否会如此,需要根据 sigaction()函数是否设置了 SA_NODEFER 标志
    而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。
  • 使用 sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该
    组信号自动添加到信号掩码中, 当信号处理函数结束返回后,再将这组信号从信号掩码中移除; 通
    过 sa_mask 参数进行设置。
  • 除了以上两种方式之外,还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加/
    移除信号。

sigprocmask():向信号掩码中添加/移除信号

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

返回值:返回值: 成功返回 0;失败将返回-1,并设置 errno 。

参数介绍:

  • how: 指定了调用函数时的一些行为。
    • SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信
      号掩码设置为当前值与 set 的并集。
    • SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除
    • SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集
  • set:指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为
    NULL,则表示无需对当前信号掩码作出改动。
  • oldset: 如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码。

示例:由于将SIGINT信号加入了信号掩码,不会立马执行信号处理函数打印”执行信号处理函数...“,而是睡眠2秒后在”"休眠结束“后打印”执行信号处理函数...“

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

static void sig_handler(int sig)
{
	printf("执行信号处理函数...\n");
}

int main(void)
{
    struct sigaction sig = {0};
    sigset_t sig_set;
    /* 注册信号处理函数 */
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGINT, &sig, NULL))
    {
        exit(-1);
    }
    /* 信号集初始化 */
    sigemptyset(&sig_set);
    sigaddset(&sig_set, SIGINT);
    /* 向信号掩码中添加信号 */
    if (-1 == sigprocmask(SIG_BLOCK, &sig_set, NULL))
    {
        exit(-1);
    }
    /* 向自己发送信号 */
    raise(SIGINT);
    /* 休眠 2 秒 */
    sleep(2);
    printf("休眠结束\n");
    /* 从信号掩码中移除添加的信号 */
    if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
    {
        exit(-1);
    }
    exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
休眠结束
执行信号处理函数...

阻塞等待信号sigsuspend()

将恢复信号掩码和 pause()挂起进程这两个动作封装成一个原子操作 。补充:原子操作就是两个操作是在一起执行的,不会被打断。

#include <signal.h>
int sigsuspend(const sigset_t *mask);

返回值: sigsuspend()始终返回-1,并设置 errno 来指示错误(通常为 EINTR) ,表示被信号所中断,如
果调用失败,将 errno 设置为 EFAULT。

mask: 参数 mask 指向一个信号集。

示例

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
	printf("执行信号处理函数...\n");
}
int main(void)
{
    struct sigaction sig = {0};
    sigset_t new_mask, old_mask, wait_mask;
    /* 信号集初始化 */
    sigemptyset(&new_mask);
    sigaddset(&new_mask, SIGINT);
    sigemptyset(&wait_mask);
    /* 注册信号处理函数 */
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGINT, &sig, NULL))
    {
    	 exit(-1);
    }
   
    /* 向信号掩码中添加信号 */
    if (-1 == sigprocmask(SIG_BLOCK, &new_mask, &old_mask))
    {
     	exit(-1);
    }
   
    /* 执行保护代码段 */
    puts("执行保护代码段");
    /******************/
    /* 挂起、等待信号唤醒 */
    if (-1 != sigsuspend(&wait_mask))		//信号掩码被替wait_mask替换,wait_mask为空
    {
    	 exit(-1);
    }
   
    /* 恢复信号掩码 */
    if (-1 == sigprocmask(SIG_SETMASK, &old_mask, NULL))
    {
        exit(-1);
    }
    exit(0);
}

在上述代码中,我们希望执行受保护代码段时不被 SIGINT 中断信号打断,所以在执行保护代码段之前
将 SIGINT 信号添加到进程的信号掩码中,执行完受保护的代码段之后,调用 sigsuspend()挂起进程,等待
被信号唤醒,被唤醒之后再解除 SIGINT 信号的阻塞状态。

实时性信号

Linux 内核定义了 31 个不同的实时信号,信号编号范围为 34~64,使用 SIGRTMIN 表示编号最小的实
时信号,使用 SIGRTMAX 表示编号最大的实时信号,其它信号编号可使用这两个宏加上一个整数或减去一
个整数。

sigpending()函数

当接收到的信号是在信号掩码中时,会被阻塞,此时信号被添加到进程的等待信号集(等待被处理,处于等待状态的信号)中 。

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

返回值: 成功返回 0;失败将返回-1,并设置 errno。

set: 处于等待状态的信号会存放在参数 set 所指向的信号集中 。

示例

补充:sigismember()此函数用于检查一个指定的信号是否在给定的信号集中,也就是检查该信号是否被阻塞

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

int main()
{
    /* 定义信号集 */
    sigset_t sig_set;
    /* 将信号集初始化为空 */
    sigemptyset(&sig_set);
    /* 获取当前处于等待状态的信号 */
    sigpending(&sig_set);
    /* 判断 SIGINT 信号是否处于等待状态 */
    if (1 == sigismember(&sig_set, SIGINT))
    	puts("SIGINT 信号处于等待状态");
    else if (!sigismember(&sig_set, SIGINT))
    	puts("SIGINT 信号未处于等待状态");
}

发送实时信号

如果同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传递一次(仅当做发生了一次) ,这是标准信号的缺点之一。

实时信号较之于标准信号,其优势如下 :

  • 实时信号的信号范围有所扩大,可应用于应用程序自定义的目的,而标准信号仅提供了两个信号可
    用于应用程序自定义使用: SIGUSR1 和 SIGUSR2 。
  • 内核对于实时信号所采取的是队列化管理。如果将某一实时信号多次发送给另一个进程,那么将会
    多次传递此信号。
  • 当发送一个实时信号时,可为信号指定伴随数据(一整形数据或者指针值),供接收信号的进程在
    它的信号处理函数中获取。
  • 信号传递顺序得到保障。信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。

应用程序当中使用实时信号,需要有以下的两点要求:

  • 发送进程使用 sigqueue()系统调用发送信号及伴随数据

  • sa_handler接收进程使用sigaction函数为信号建立处理函数,并且使用sa_sigaction,而不是sa_handler。可以使用sa_handler,但是获取不到伴随数据了。


sigqueue()

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

返回值: 成功将返回 0;失败将返回-1,并设置 errno。

参数介绍:

  • pid: 指定接收信号的进程对应的 pid,将信号发送给该进程。

  • sig: 指定需要发送的信号。与 kill()函数一样,也可将参数 sig 设置为 0,用于检查参数 pid 所指定的进
    程是否存在。

  • value: 参数 value 指定了信号的伴随数据, union sigval 数据类型。 union sigval 数据类型(共用体) 如下所示:

    typedef union sigval
    {
        int sival_int;
        void *sival_ptr;
    } sigval_t;
    

    示例

    					/*发送进程使用 sigqueue()系统调用向另一个进程发送实时信号*/
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    int main(int argc, char *argv[])
    {
        union sigval sig_val;
        int pid;
        int sig;
        /* 判断传参个数 */
        if (3 > argc)
        {
            exit(-1);
        }
        /* 获取用户传递的参数 */
        pid = atoi(argv[1]);	//接收进程pid
        sig = atoi(argv[2]);	//发送的信号
        printf("pid: %d\nsignal: %d\n", pid, sig);
        /* 发送信号 */
        sig_val.sival_int = 10; //伴随数据
        if (-1 == sigqueue(pid, sig, sig_val)) 
        {
            perror("sigqueue error");
            exit(-1);
        }
        puts("信号发送成功!");
        exit(0);
    }
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    static void sig_handler(int sig, siginfo_t *info, void *context)
    {
        sigval_t sig_val = info->si_value;
        printf("接收到实时信号: %d\n", sig);
        printf("伴随数据为: %d\n", sig_val.sival_int);
    }
    
    int main(int argc, char *argv[])
    {
        struct sigaction sig = {0};
        int num;
        /* 判断传参个数 */
        if (2 > argc)
        exit(-1);
        /* 获取用户传递的参数 */
        num = atoi(argv[1]);		//接收的信号
        /* 为实时信号绑定处理函数 */
        sig.sa_sigaction = sig_handler;
        sig.sa_flags = SA_SIGINFO;
        if (-1 == sigaction(num, &sig, NULL)) 
        {
            perror("sigaction error");
            exit(-1);
        }
        /* 死循环 */
        for ( ; ; )
        {
       		sleep(1);
        }
        exit(0);
    }
    

    先运行接收进程,并且设置接收的信号为34。然后查看接收程序的pid,使用发送进程向该pid发送34信号

    ten@ten-virtual-machine:~/H616/demo$ ./testApp 34
    接收到实时信号: 34
    伴随数据为: 10
    
    
    ten@ten-virtual-machine:~/H616/demo$ ./testApp2 4218 34
    pid: 4218
    signal: 34
    信号发送成功!
    

异常退出 abort()函数

进程异常退出使用 abort()终止进程运行,会生成核心转储文件,可用于判断程序调用 abort()时的程序状态

当调用 abort()函数之后,内核会向进程发送 SIGABRT 信号, SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;

#include <stdlib.h>
void abort(void);

示例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
	printf("接收到信号: %d\n", sig);
}

int main(int argc, char *argv[])
{
    struct sigaction sig = {0};
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGABRT, &sig, NULL))
    {
        perror("sigaction error");
        exit(-1);
    }
    sleep(2);
    abort(); // 调用 abort
    for ( ; ; )
    sleep(1);
    exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
接收到信号: 6
已中止 (核心已转储)

从打印信息可知,即使在我们的程序当中捕获了 SIGABRT 信号,但是程序依然会无情的终止,无论阻
塞或忽略 SIGABRT 信号, abort()调用均不收到影响,总会成功终止进程 。

posted @ 2024-05-04 15:53  tenzzZ  阅读(144)  评论(0编辑  收藏  举报