【Linux】系统编程之进程间通信IPC(管道、FIFO、消息队列、共享存储、信号、信号量)

本文详细介绍了Linux下进程间通信的五种主要方式:管道(包括无名管道和命名管道FIFO)、消息队列、共享内存、信号以及信号量。管道是半双工的,适合父子进程间通信;FIFO是全双工的,可以在无关进程间通信;消息队列能传递大量信息且独立于进程;共享内存能快速传递大量数据;信号用于进程间的同步与互斥,常与信号量结合使用。每种通信方式都通过实例代码进行了演示,展示了其实现过程和使用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言
进程间通信IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

一、管道

管道,通常指无名管道 ,是 UNIX 系统IPC最古老的形式。

1、特点

 1、它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。

 2、它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

 3、它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

2、函数pipe

函数原型:

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

参数介绍:

参数fd返回两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入。

返回值:

若成功,返回0
若出错,返回-1

当一个管道被创建时,它会伴随着创建两个文件描述符,fd[0]为读而打开,fd[1]为写而打开;如图:
在这里插入图片描述
想关闭管道只要将两个文件描述符关闭即可。

3、管道使用过程

单个进程中的管道几乎没有任何用处。通常,进程会先调用pipe,接着调用fork,从而创建从父进程到子进程的IPC通道,反之亦然。如图:
在这里插入图片描述
fork之后做什么取决于我们想要的数据流的方向。对于从父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程关闭写端(fd[1])。如图:
在这里插入图片描述
对于一个从子进程到父进程的管道,父进程关闭fd[1],子进程关闭fd[0]。

当管道的一端被关闭后,下 列两条规则起作用:
(1) 当读(read)一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。(通常一个管道只有一个读进程和一个写进程。)
(2)如果写(write)一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno设置为EPIPE。
示例
代码:

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

int main()
{
        //int pipe(int pipefd[2]);
        int fd[2];
        pid_t pid;
        char *wbuf = "Apibro is very nice!";
        char *rbuf = NULL;
		rbuf = (char *)malloc(sizeof(char)*strlen(wbuf));

        if(pipe(fd) == -1)
        {
                printf("create pipe failed!\n");
        }

        pid = fork();

        if(pid < 0)
        {
                printf("create child process failed!\n");
        }
        else if(pid > 0)
        {
                sleep(3);
                printf("this is parent process!\n");
                close(fd[0]);
                write(fd[1],wbuf,strlen(wbuf));
                wait(NULL);
        }
        else
        {
                printf("this is child process!\n");
                close(fd[1]);
                read(fd[0],rbuf,strlen(wbuf));
                printf("read from parent:%s\n",rbuf);
                exit(0);
        }

        return 0;
}              

结果:
执行程序后,父进程睡3秒,子进程先跑,read时管道里没有东西,所以阻塞,当父进程醒来write东西到管道时,子进程read继续跑,从而读出数据。
在这里插入图片描述

二、FIFO

FIFO,也称为命名管道,它是一种文件类型。

1、特点

 1、FIFO可以在无关的进程之间交换数据,与无名管道不同。

 2、FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

2、函数mkfifo

函数原型:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

参数介绍:

pathname:要创建的管道(以当前路径开始)
mode:与open函数中的 mode 相同

O_RDWR(读写管道),O_WRONLY(只写管道),O_RDONLY(只读管道)
O_NONBLOCK:非阻塞
O_CREAT:如果该文件不存在,就创建一个新的文件,并用第三个参数为其设置权限
O_EXCL:如果使用O_CREAT时文件存在,那么可能返回错误信息。这一参数可测试文件是否存在

返回值:

若成功,返回0
若出错,返回-1

注意:如果文件已经存在即(EEXIST),这时候再重新创建文件的话也会是提示失败的。

mkfifo一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  • 1、若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
  • 2、若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该FIFO,其errno置ENXIO。

示例
代码:

//文件read_fifo.c
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
	char *readBuf = NULL;
	readBuf =(char *)malloc(sizeof(char)*128);

	//int mkfifo(const char *pathname, mode_t mode);
	if(mkfifo("./fifo",0600) == -1 && errno != EEXIST){//创建FIFO管道
		printf("mkfifo failed!\n");
                perror("why");
	}
	
	int fd = open("./fifo",O_RDONLY);
	printf("read-open fifo successed!\n");

	while(1){
        int n_read = read(fd,readBuf,128);
		sleep(1);
		if(n_read <= 0){break;}
		printf("read %d bytes from fifo,content:%s\n",n_read,readBuf);
        }

	close(fd);

	return 0;
}
//文件write_fifo.c
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main()
{
	int cnt = 0;
	char *writeBuf = "Apibro is very nice!";

	int fd = open("./fifo",O_WRONLY);
	printf("write-open fifo successed!\n");

	while(1){
		int n_write = write(fd,writeBuf,strlen(writeBuf));
		printf("write %d bytes to fifo,content:%s\n",n_write,writeBuf);
		sleep(1);
		cnt++;
		if(cnt == 3){break;}
	}

	close(fd);

	return 0;
}

结果:

  • 运行read_fifo后,读端在while循环里每隔一秒读一次,此时读端open是以O_RDONLY只读的方式打开。这时候没有指定
    O_NONBLOCK即不阻塞操作,所以当没有写端写入数据时,这时候读端跑到read时会被执行阻塞直到有东西写入;
  • 运行write_fifo后,写端的意思是每隔一秒写一次,直到写三次后退出循环,这时候的open是以只写的方式O_WRONLY打开,这时候没有指定
    O_NONBLOCK即不阻塞操作,所以当没有读端读数据时,这时候写端运行到write时会被执行阻塞直到读端读取数据后继续执行。
  • 最终看到的效果是运行read_fifo会阻塞,接着运行write_fifo会出现,一写一读交替运行三次后结束程序。

在这里插入图片描述

三、消息队列

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

1、特点

 1、消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。

 2、消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

 3、消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

注意:
消息队列是linux内核地址空间中的内部链表,通过Linux内核在各个进程之间传递内容,消息顺序地发送到消息队列中,并且以几种不同的方式从队列中获取,每个消息队列可以用IPC标识符唯一的进行标识,内核中的消息队列是通过IPC的标识符来区别的,不同的消息队列之间是相互独立的,每个消息队列中的消息又构成一个独立的链表。
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型地数据结构。我们可以通过发送消息来避免命名管道地同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
在这里插入图片描述

2、标识符和键

(1)概念

 1、每个内核中的IPC(InterProcess Communication)结构都用一个非负整数的标识符加以引用。这个标识符的作用和文件描述符类似,但与文件描述符不同的是,标识符每次创建然后又被删除时它的值都会加一,直到达到一个整数的最大值然后从0开始继续循环。
 2、标识符是是IPC对象内部的命名,对外不可见,我们在做进程通信时需要创建一个键(key_t),这个键和标识符是绑定的,我们可以使用这个键进行进程间通信;
 3、linux下信号量、消息队列和共享存储器在使用时都必须制定一个键值;

(2)函数ftok

函数原型:

#include <sys/types.h>
#include <sys/ipc.h>

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

参数介绍:

pathname:是你指定的文件名(路径),要求文件必须存在,一般使用当前目录
proj_id:是子序号,虽然是int类型,但是只使用8bits(1-255),可以是数字也可以是字符

返回值:

若成功,返回键key_t值
若出错,返回-1

3、函数msgget、msgsnd、msgrcv和msgctl

(1)函数msgget

作用:创建或打开一个消息队列。
函数原型:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

参数介绍:

key:消息队列的键值,多个进程可以通过他访问同一个消息队列,其中有个特殊值IPC_PRIVATE;它多用于创建当前进程的私有消息队列
msgflg:权限标志位。⽤法和创建⽂件时使⽤的mode模式标志是⼀样的,IPC_CREAT//不存在创建,存在就打开,IPC_CREAT|EXCL//不存在就创建,存在出错

返回值:

若成功,返回消息队列ID(一个非负整数)
若失败,返回-1

(2)函数msgsnd

作用:添加消息,把消息添加到已打开的消息队列末尾,即发送消息
函数原型:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数介绍:

msqid:消息队列的队列ID,表示往哪个消息队列发数据
msgp:指向消息结构的指针,指针指向准备发送的消息(即准备发送的消息的内容)
消息结构msgbuf参考

struct msgbuf {
		long mtype;       /* message type, must be > 0 */
		char mtext[1];    /* message data */
}; 
参数:
mtype:它必须以⼀个long int⻓整数开始,接收者函数将利⽤这个⻓整数确定消息的类型
mtext:保存消息内容的数组或指针,它必须小于系统规定的上限值

msgsz:消息正文的字节数,即消息的大小,也就是mtext的大小,用strlen()计算
msgflg0表示msgsng调用阻塞直到发送成功为止,IPC_NOWAIT若消息无法立即发送(如当前消息队列已满),函数立即返回EAGAIN错误

返回值:

若成功,返回0
若出错,返回-1

(3)函数msgrcv

作用:读取消息,把消息从消息队列中取走
函数原型:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

参数介绍:

msqid:消息队列的队列ID,表示从哪个消息队列拿数据
msgp:消息缓冲区,指向准备接收的消息
msgsz:消息正文的字节数,即消息的大小,这个大小不含保存消息类型的那个long int⻓整型,即用sizeof()计算
msgtyp:可以实现接收优先级的简单形式,即接收消息的类型,即发消息结构体中的mtype

msgtyp ==0,返回队列中的第一个消息; 
msgtyp > 0,返回队列中消息类型为 type 的第一个消息;
msgtyp <0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。

msgflg
IPC_NOWAIT若消息无法立即发送(如当前消息队列已满),函数立即返回EAGAIN错误
0表示msgsng调用阻塞直到接收一条相应类型的消息为止
MSG_NOERROR若返回的消息比msgsz字节多,消息就会截短到msgsz字节,且不通知消息发送进程

返回值:

若成功,返回消息数据部分的长度
若出错,返回-1

(4)函数msgctl

作用:控制消息队列
函数原型:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

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

参数介绍:

msqid:消息队列的队列ID
cmd:一般用IPC_RMID,表示删除除消息队列

IPC_STAT读取消息队列的数据结构msqid_ds,并将其存储在buf指定的地址中
IPC_SET设置消息队列的数据结构msqid_ds中的ipc_perm元素的值,这个值取自buf参数
IPC_RMID删除消息队列

buf:描述消息队列的msq_ds结构类型变量,一般写NULL

返回值:

若成功,返回0
若出错,返回-1

示例
代码:

//文件msgsnd.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct msgbuf {
	long mtype;
	char mtext[128];
};

int main()
{
	struct msgbuf rcvBuf;

	//key_t ftok(const char *pathname, int proj_id);键生成
	key_t key;
	key = ftok(".",23);
	if(key == -1){
		printf("ftok error!");
		exit(1);
	}
	printf("Message Queue - Client key is: %d.\n",key);

	//int msgget(key_t key, int msgflg);创建或打开消息队列
	int msqid = msgget(key,IPC_CREAT|0777);
	if(msqid == -1){
		printf("msgget error!");
		exit(1);
	}

	//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);添加发送消息
	struct msgbuf sndBuf = {11,"this message comes from the queue!"};
	int ret = msgsnd(msqid,&sndBuf,strlen(sndBuf.mtext),0);
	printf("message sent successfully!  ret = %d\n",ret);

	printf("======这里消息发送成功,下面等待对方回复======\n");

	//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);读取消息
	int ret2 = msgrcv(msqid,&rcvBuf,sizeof(rcvBuf.mtext),13,0);
	printf("Reading message queue reply content is: %s ret2 = %d\n",rcvBuf.mtext,ret2);

	//int msgctl(int msqid, int cmd, struct msqid_ds *buf);控制消息队列
	msgctl(msqid,IPC_RMID,NULL);

	return 0;
}
//文件msgrcv.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct msgbuf {
	long mtype;
	char mtext[128];
};

int main()
{
	struct msgbuf rcvBuf;

	//key_t ftok(const char *pathname, int proj_id);键生成
	key_t key;
	key = ftok(".",23);
	if(key == -1){
		printf("ftok error!");
		exit(1);
	}
	printf("Message Queue - Server key is: %d.\n",key);

	//int msgget(key_t key, int msgflg);创建或打开消息队列
	int msqid = msgget(key,IPC_CREAT|0777);
	if(msqid == -1){
		printf("msgget error!");
		exit(1);
	}

	//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);读取消息
	int ret2 = msgrcv(msqid,&rcvBuf,sizeof(rcvBuf.mtext),11,0);
	printf("Reading message queue content is: %s ret = %d\n",rcvBuf.mtext,ret2);

	printf("======这里已经读取消息结束,下面准备回复消息======\n");

	//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);添加发送消息
	struct msgbuf sndBuf = {13,"think you,I read the message!"};
	int ret = msgsnd(msqid,&sndBuf,strlen(sndBuf.mtext),0);
	printf("message sent successfully!  ret = %d\n",ret);

	//int msgctl(int msqid, int cmd, struct msqid_ds *buf);控制消息队列
	msgctl(msqid,IPC_RMID,NULL);

	return 0;
}

结果:
在这里插入图片描述

四、共享内存

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
首先说一下共享内存实现进程间通信的基本原理,共享内存实际是操作系统在实际物理内存中开辟的一段内存。
共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。
在这里插入图片描述
一个进程往该共享内存空间写入内容时,另一个进程也可以访问到这个共享内存空间,读取到一个进程写入的内容,相反也可,这就实现了两个进程间的通信。
要实现进程间通信需要两个进程能够看到同一块空间,系统开辟的共享内存就是两进程看到的同一资源。
注意:共享内存可以实现两个不相关的进程间的通信,而且共享内存是进程间通信方式中最快的。

1、特点

 1、共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

 2、因为多个进程可以同时操作,所以需要进行同步。

 3、信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

2、函数shmget、shmat、shmdt和shmctl

(1)函数shmget

作用:创建共享内存
函数原型:

#include <sys/ipc.h>
#include <sys/shm.h>

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

参数介绍:

key:共享内存的键值,多个进程可以通过它访问同一个共享内存,其中有个特殊值IPC_PRIVATE;它多用于创建当前进程的私有共享内存
size:共享内存区大小,以page为单位,大小为4096的整数倍
shmflg:权限标志,常用两个IPC_CREATIPC_EXCL,一般后面还加一个权限,相当于文件的权限(可用8进制表示法)

返回值:

若成功,返回共享内存ID(一个非负整数)
若失败,返回-1

(2)函数shmat

作用:映射共享内存,把这段创建的共享内存映射到调用该函数进程的进程地址空间中
函数原型:

#include <sys/types.h>
#include <sys/shm.h>

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

参数介绍:

shmid:要映射的共享内存区标识符,即共享内存ID(shmget的返回值)
shmaddr:将共享内存映射到指定地址(为0表示系统自动分配地址并把该段共享内存映射到调用进程的地址空间)
shmflgSHM_RDONLY表示共享内存只读;默认0表示共享内存可读写

返回值:

若成功,返回指向共享存储段的指针,即返回映射到进程地址空间共享区的开始地址
若失败,返回-1

(3)函数shmdt

作用:撤销映射,释放进程地址空间,与free有点相似
函数原型:

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

参数介绍:

shmaddr:共享内存映射到进程地址空间的地址,shmat的返回值

返回值:

若成功,返回0
若出错,返回-1

注意:shmat要与shmdt一起使用才起作用

(4)函数shmctl

作用:控制操作共享内存
函数原型:

#include <sys/ipc.h>
#include <sys/shm.h>

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

参数介绍:

shmid:共享内存ID(shmget的返回值)
cmd:一般用IPC_RMID,表示释放共享内存
buf:指向一个共享内存的数据结构,struct shmid_ds,一般写NULL

返回值:

若成功,返回0
若出错,返回-1

示例
代码:

//文件shmw.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
	int shmid;
	char *shmaddr;
	key_t key;

	if((key = ftok(".",1)) == -1){
		perror("ftok error");
		exit(1);
	}
						
	//int shmget(key_t key, size_t size, int shmflg);创建共享内存
	shmid = shmget(key,1024*4,IPC_CREAT|0666);
	if(shmid == -1){
		perror("shmget error");
		exit(-1);
	}

	//void *shmat(int shmid, const void *shmaddr, int shmflg);映射共享内存
	shmaddr = shmat(shmid,0,0);
	printf("shmat ok!\n");

	strcpy(shmaddr,"Apibro is very nice!");//input data to space(shmaddr)

	sleep(5);
	//int shmdt(const void *shmaddr);撤销映射
	shmdt(shmaddr);

	//int shmctl(int shmid, int cmd, struct shmid_ds *buf);控制操作共享内存
	shmctl(shmid,IPC_RMID,NULL);
	printf("quit!\n");

	return 0;
}
//文件shmr.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
	int shmid;
	char *shmaddr;
	key_t key;

	if((key = ftok(".",1)) == -1){
		perror("ftok error");
		exit(1);
	}
						
	//int shmget(key_t key, size_t size, int shmflg);创建共享内存
	shmid = shmget(key,1024*4,0);
	if(shmid == -1){
		perror("shmget error");
		exit(-1);
	}

	//void *shmat(int shmid, const void *shmaddr, int shmflg);映射共享内存
	shmaddr = shmat(shmid,0,0);
	printf("shmat ok!\n");

	printf("data: %s\n",shmaddr);

	sleep(5);
	//int shmdt(const void *shmaddr);撤销映射
	shmdt(shmaddr);

	printf("quit!\n");

	return 0;
}

结果:
写端写完,等待5s时间给读端读走data后,撤销映射quit,读端读完撤销映射,释放共享内存quit
在这里插入图片描述

五、信号

信号signal)是事件发生时对进程的通知机制,有时也叫软件中断,信号可以让一个正运行进程被另一个运行进程异步进程中断,转而处理某突发事件。

对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。

注意:信号的产生和处理都是由内核完成 。
简单说一下使内核为进程产生信号的事件有:
1 终端发特殊字符,如CTRL+C 对前台进程发送中断信号
2 硬件发生异常,如被0除等异常机器语言指令
3 系统状态变化:如alarm定时器到期引起SIGALRM信号,进程某个子进程退出
4 运行kill 命令/函数

信号使用的目的:
1.让进程知道发生了一件特定的事情。
2.强迫进程执行自己的信号处理程序。

特点:
简单,但不能携带大量的信息,满足特定条件才发送信号,其优先级比较高。

1、信号概述

(1)信号的名字和编号

每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。
信号定义在signal.h头文件中,信号名都定义为正整数。
具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。
在这里插入图片描述
列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

(2)信号的处理

信号的处理有三种方法,分别是:忽略、捕捉和默认动作

  • 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是
    SIGKILLSIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景。
  • 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
  • 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。在此,我就不详细展开了,需要查看的,可以自行查看。也可以参考《UNIX 环境高级编程(第三部)》的 P251—P256中间对于每个信号有详细的说明。

信号的默认动作:
Term:终止
lgn:忽略
Core:终止并生成一个Core文件(为了调试错误)
Stop:暂停当前进程
Cont:继续执行当前被暂停的进程

信号的几种状态: 产生,未处理(未决),递达(被处理)

注意:SIGKILLSIGSTOP信号不能被捕捉,阻塞或忽略,只能执行默认动作

当然,对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段。

2、信号处理函数的注册(接收)

(1)入门版-函数signal

函数原型:

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

根据函数原型可以看出由两部分组成,一个是真实处理信号的函数,另一个是注册函数了。
对于sighandler_t signal(int signum, sighandler_t handler);函数来说,signum 显然是信号的编号,handler 是中断函数的指针。
同样,typedef void (*sighandler_t)(int);中断函数的原型中,有一个参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号。
在这里插入图片描述

参数介绍:

signum:是注册的信号的编号
handler:中断函数的指针(函数名)

返回值:

若成功,返回以前的信号处理配置
若失败,返回SIG_ERR

(2)高级版-函数sigaction

作用:检查或修改与指定信号相关联的处理动作
函数原型:

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

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

参数介绍:

signum:是注册的信号的编号 (除SIGKILLSIGSTOP
act:如果不为空说明需要对该信号有新的配置,指定对特定信号的处理
oldact:如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复

//回调函数句柄sa_handler、sa_sigaction只能任选其一

void(*sa_handler)(int);
//信号处理函数,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作,
//如果 sa_flags 中存在SA_SIGINFO 标志,
//那么 sa_sigaction 将作为 signum 信号的处理函数,否则用 sa_handler 
	
void(*sa_sigaction)(int, siginfo_t *, void *);
//信号处理函数,能够接受额外数据和 sigqueue 配合使用

sigset_t sa_mask;
//阻塞关键字的信号集,可以在调用捕捉函数之前把信号添加到信号阻塞字,
//信号捕捉函数返回之前恢复为原先的值

int sa_flags;
//影响信号的行为,SA_SIGINFO 表示能够接受数据

注意:

  • struct sigaction结构体中的 sa_mask成员,设置在其的信号集中的信号,会在捕捉函数调用前设置为阻塞,并在捕捉函数返回时恢复默认原有设置。这样的目的是,在调用信号处理函数时,就可以阻塞默写信号了。在信号处理函数被调用时,操作系统会建立新的信号阻塞字,包括正在被递送的信号。因此,可以保证在处理一个给定信号时,如果这个种信号再次发生,那么他会被阻塞到对之前一个信号的处理结束为止。

  • sigaction 的时效性:当对某一个信号设置了指定的动作的时候,那么,直到再次显式调用 sigaction并改变动作之前都会一直有效。

  • 关于结构体中的 flag 属性的详细配置,在此不做详细的说明了,只说明其中一点。如果设置为 SA_SIGINFO 属性时,说明了信号处理程序带有附加信息,也就是会调用sa_sigaction 这个函数指针所指向的信号处理函数。否则,系统会默认使用 sa_handler 所指向的信号处理函数。在此,还要特别说明一下,sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于union,所以只能设置其中的一个,不能两个都同时设置。

  • 关于void (*sa_sigaction)(int, siginfo_t *, void *);处理函数来说还需要有一些说明。void* 是接收到信号所携带的额外数据;而struct siginfo这个结构体主要适用于记录接收信号的一些相关信息。

siginfo_t {
	int      si_signo;     /* Signal number */
    int      si_errno;     /* An errno value */
    int      si_code;      /* Signal code */
    int      si_trapno;    /* Trap number that caused
                              hardware-generated signal
                              (unused on most architectures) */
    pid_t    si_pid;       /* Sending process ID */
    uid_t    si_uid;       /* Real user ID of sending process */
    int      si_status;    /* Exit value or signal */
    clock_t  si_utime;     /* User time consumed */
    clock_t  si_stime;     /* System time consumed */
    sigval_t si_value;     /* Signal value */
    int      si_int;       /* POSIX.1b signal */
    void    *si_ptr;       /* POSIX.1b signal */
    int      si_overrun;   /* Timer overrun count;
                              POSIX.1b timers */
    int      si_timerid;   /* Timer ID; POSIX.1b timers */
    void    *si_addr;      /* Memory location which caused fault */
    long     si_band;      /* Band event (was int in
                              glibc 2.3.2 and earlier) */
    int      si_fd;        /* File descriptor */
    short    si_addr_lsb;  /* Least significant bit of address
                              (since Linux 2.6.32) */
    void    *si_lower;     /* Lower bound when address violation
                              occurred (since Linux 3.19) */
    void    *si_upper;     /* Upper bound when address violation
                              occurred (since Linux 3.19) */
    int      si_pkey;      /* Protection key on PTE that caused
                              fault (since Linux 4.6) */
    void    *si_call_addr; /* Address of system call instruction
                              (since Linux 3.5) */
    int      si_syscall;   /* Number of attempted system call
                              (since Linux 3.5) */
    unsigned int si_arch;  /* Architecture of attempted system call
                              (since Linux 3.5) */
}

其中的成员很多,si_signosi_code 是必须实现的两个成员,可以通过这个结构体获取到信号的相关信息。
关于发送过来的数据是存在两个地方的,sigval_t si_value这个成员中有保存了发送过来的信息;同时,在si_int或者si_ptr成员中也保存了对应的数据。

返回值:

若成功,返回0
若出错,返回-1

3、信号发送函数

(1)入门版-函数kill

作用:将信号发送给进程或进程组
函数原型:

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

参数:

pid:正数,要发送信号的进程号
sig:信号

返回值:

若成功,返回0
若出错,返回-1

(2)高级版-函数sigqueue

函数原型:

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

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

参数介绍:

pid:正数,要发送信号的进程号
sig:信号
value:信号携带的数据

返回值:

若成功,返回0
若出错,返回-1

注意:

  • 使用 sigaction 函数安装信号处理程序时,制定了 SA_SIGINFO 的标志。
  • sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的时 sa_handler成员,那么将无法获取额外携带的数据。
  • sigqueue 函数只能把信号发送给单个进程,可以使用 value 参数向信号处理程序传递整数值或者指针值。
  • sigqueue 函数不但可以发送额外的数据,还可以让信号进行排队(操作系统必须实现了POSIX.1的实时扩展),对于设置了阻塞的信号,使用 sigqueue发送多个同一信号,在解除阻塞时,接受者会接收到发送的信号队列中的信号,而不是直接收到一次。但是,信号不能无限的排队,信号排队的最大值受到SIGQUEUE_MAX的限制,达到最大限制后,sigqueue 会失败,errno 会被设置为 EAGAIN

示例:入门版signal和kill
代码:

//文件signaldemo1.c(接收)
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

//typedef void (*sighandler_t)(int);
//sighandler_t signal(int signum, sighandler_t handler);

void handler(int signum)
{
        switch(signum)
        {
                case 2:
                        printf("signum is :%d\n",signum);
                        break;
                case 9:
                        printf("signum is :%d\n",signum);
                        break;
                case 10:
                        printf("signum is :%d\n",signum);
                        break;
        }
        printf("never quit!\n");
}

int main()
{
	printf("the pid is:%d\n",getpid());
        signal(SIGINT,handler);
        signal(SIGKILL,handler);
        signal(SIGUSR1,handler);

	while(1);
	return 0;
}

结果:
执行signaldemo1.c编译的pro程序,主函数注册三个信号,并修改它,让其不退出程序,./pro运行程序后,ctrl+c程序不退出,kill -10 76317进程不退出,kill -9 76317进程退出。
在这里插入图片描述
在这里插入图片描述

简单的总结一下,我们通过 signal 函数注册一个信号处理函数,分别注册了两个信号(SIGINT 和 SIGUSER1);随后主程序就一直“长眠”了。 通过 kill 命令发送信号之前,我们需要先查看到接收者,通过 ps命令查看了之前所写的程序的 PID,通过 kill 函数来发送。 对于已注册的信号,使用 kill发送都可以正常接收到,但是如果发送了未注册的信号,则会使得应用程序终止进程。
那么,已经可以设置信号处理函数了,信号的处理还有两种状态,分别是默认处理和忽略,这两种设置很简单,只需要将 handler 设置为SIG_IGN(忽略信号)或 SIG_DFL(默认动作)即可,但记住上面说过的SIGKILL和SIGSTOP是不能被忽略,捕捉的,只能执行默认动作

在此还有两个问题需要说明一下:
1.当执行一个程序时,所有信号的状态都是系统默认或者忽略状态的。除非是 调用exec进
程忽略了某些信号。exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不会改变 。
2.当一个进程调动了 fork 函数,那么子进程会继承父进程的信号处理方式。

//文件signaldemo1Con.c(发送)
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc,char **argv)
{
	int signum;
	pid_t pid;
	char cmd[128] = {0};

	/*
	函数原型:int atoi(const char *str)
	参数:str-要转换为整数的字符串
	返回值:该函数返回转换后的长整数,如果没有执行有效的转换,则返回零
	*/
	signum = atoi(argv[1]);
	pid = atoi(argv[2]);
	
	printf("pid = %d,signum = %d\n",pid,signum);
	
	//int kill(pid_t pid, int sig);
	//kill(pid,signum);	
	sprintf(cmd,"kill -%d %d",signum,pid);
	system(cmd);

	printf("sending signal succeeded!\n");
	
	return 0;
}

signaldemo1.c(接收)和signaldemo1Con.c(发送)配合结果:
这里向一个进程发送信号是用kill函数来发送,或者是用system来对进程发送信号
在这里插入图片描述
在这里插入图片描述
示例:高级版sigaction和sigqueue
代码:

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

void handler(int signum, siginfo_t *info, void *context)
{
	printf("the signum is:%d\n",signum);

	if(context != NULL)
	{
                printf("get the data is:%d\n",info->si_int);
                printf("get the data is:%d\n",info->si_value.sival_int);
                printf("from the pid = %d\n",info->si_pid);
        }
}

int main()
{
	struct sigaction act;
	printf("my pid = %d\n",getpid());

	act.sa_sigaction = handler;
	act.sa_flags = SA_SIGINFO;//can get message

	sigaction(SIGUSR1,&act,NULL);
	
	while(1);
	return 0;
}
//signaldemo2snd.c
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
//int sigqueue(pid_t pid, int sig, const union sigval value);

int main(int argc,char **argv)
{
	int signum;
	pid_t pid;

	signum = atoi(argv[1]);
    pid = atoi(argv[2]);

	union sigval value;
    value.sival_int = 100;

	sigqueue(pid,signum,value);

	printf("my pid = %d\n",getpid());
    printf("Sending done!\n");

	return 0;
}

结果:
在这里插入图片描述
在这里插入图片描述

六、信号量

信号量semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

1、信号量的相关概念

信号量:主要用于同步与互斥。为了防止出现因多个进程访问临界资源而引发的一系列问题,信号量可以提供这样一种访问机制,在任一时刻只能有一个执行线程访问代码的临界区域,也就是说信号量是用来协调进程对临界资源的访问。

信号量的操作:信号量是一种特殊的变量,对信号量的访问必须是原子操作,信号量的操作只有两种:P操作(-1,申请资源)和V操作(+1,释放资源)。最简单的信号量只有两种取值0和1,称这样的信号量为二元信号量。可以取值为正整数N的信号量称为多元信号量,它允许多个线程并发的访问资源。

临界资源:能被多个进程共享,但一次只能允许一个进程使用的资源称为临界资源。

临界区:涉及到临界资源的部分代码,称为临界区。

互斥:亦称间接制约关系,在一个进程的访问周期内,另一个进程就不能进行访问,必须进行等待。当占用临界资源的进程退出临界区后,另一个进程才允许去访问此临界资源。

  • 例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时,
    系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态。

同步:亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的同步就是源于它们之间的相互合作。所谓同步其实就是两个进程间的制约关系。

  • 例如,输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才唤醒进程A。

原子性:对于进程的访问,只有两种状态,要么访问完了,要么不访问。当一个进程在访问某种资源的时候,即便该进程切出去,另一个进程也不能进行访问。

总结:
(1)信号量本质上是一个能被两个进程或多个进程都能看到的计数器。这个计数器用来表示临界资源的多少,计数器不以传输数据为目的,以保护临界资源为目的。
(2)信号量是进程间通信的一种,本身是一种临界资源,所以必须也要保护信号量的安全性。
(3)信号量的PV操作必须是原子操作。P操作:-1,表示申请资源成功;V操作:+1,表示释放资源成功。
(4)两个进程共享一个二元信号量sem,它的过程是这样的:其中一个进程执行了P操作(sem-1),它将得到信号量,并可以进入临界区,此时sem=0。另一个进程请求临界资源(P操作)的时候,他将被阻止进入临界区,并挂起等待,当第一个进程离开临界区并执行了V操作(sem+1)释放了资源的时候,第二个进程才可以恢复执行。

2、特点

 1、信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

 2、信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

 3、每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

 4、支持信号量组。

3、信号量函数

(1)函数semget

作用:创建信号量或获得在系统已存在的信号量
函数原型:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

参数介绍:

key:信号量的键值,多个进程可以通过它访问同一个信号量,其中有个特殊值IPC_PRIVATE;它多用于创建当前进程的私有信号量
nsems:需要创建的信号量数目,通常取为1
semflg:权限标志,常用两个IPC_CREATIPC_EXCL,一般后面还加一个权限,相当于文件的权限(可用8进制表示法)

返回值:

若成功,返回信号量ID (一个非负整数)
若出错,返回-1

注意:当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0

(2)函数semctl

作用:控制信号量的相关信息
函数原型:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
										/*union semun arg*/

参数介绍:

semid:信号量ID(semget的返回值)
semnum:信号量编号,只有使用信号量集时才会被用到。通常取值为0,就是使用单个信号量(也是第一个信号量)
cmd:指定对信号量的各种操作,当使用单个信号量(而不是信号量集)时,常用的操作有以下几种

IPC_STAT:获得该信号量(或者信号量集合)的semid_ds结构,并存放在由第4个参数arg结构变量的buf域指向的semid_ds结构中。semid_ds是在系统中描述信号量的数据结构。
SETVAL:将信号量值设置为arg 的 val值。 
GETVAL:返回信号量的当前值。
IPC_RMID:从系统中删除信号量(或者信号量集)

arg:是union semnn结构,可能在某些系统中不给出该结构的定义,此时必须由程序员自已定义

返回值:

若成功,IPC_STAT、SETVAL、IPC_RMID返回0,GETVAL返回信号量的当前值
若出错,返回-1

注意:在semctl函数中的命令有多种,这里就说两个常用的:
SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。这个值由第四个参数决定。第四参数是一个自定义的共同体,如下:

// 用于信号等操作的共同体。
union semun {
	int              val;    /* Value for SETVAL */
	struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
	unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  /* Buffer for IPC_INFO 
				    (Linux-specific) */
};

IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

(3)函数semop

作用:对信号量组进行操作,改变信号量的值
函数原型:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

参数介绍:

semid:信号量ID(semget的返回值)
sops:指向一个信号量结构体的指针
nsops:信号量的个数,操作数组sops中的操作个数(元素数目),通常取值为1(一个操作)

sops指向的信号量结构体:

struct sembuf 
{
    short sem_num; // 信号量编号,0~sem_nums-1,使用单个信号量时,通常取0
    short sem_op;  /* 信号量值在一次操作中的改变量,一般只会用到两个值:
                   -1:P操作,等待信号变得可用;
                   +1:V操作,发出信号量已经变得可用*/
    short sem_flg; /*通常设置为SEM_UNDO
    				IPC_NOWAIT:表示队列满不等待,非阻塞,返回EAGAIN错误。
    				IPC_UNDO: 使操作系统跟踪信号,并在进程没有释放该信号量而终止时,系统自动释放信号量*/
}

返回值:

若成功,返回0
若出错,返回-1

示例:初始信号量
代码:

//sem.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <unistd.h>

//       int semget(key_t key, int nsems, int semflg);
//       int semctl(int semid, int semnum, int cmd, ...);
//       int semop(int semid, struct sembuf *sops, size_t nsops);

union semun {
	int              val;    /* Value for SETVAL */
	struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
	unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  /* Buffer for IPC_INFO 
				    (Linux-specific) */
};

int main()
{
	int semid;
	pid_t pid;
	key_t key;
	key = ftok(".",1);

	semid = semget(key,1,IPC_CREAT|0666);//创建或获取信号量
	if(semid == -1){
		printf("semget error!\n");
	}

	union semun initsem;
	initsem.val = 1;

	semctl(semid,0,SETVAL,initsem);//初始化信号量
		//操作第1个信号量,SETVAL设置信号量的val值为initsem	

	pid = fork();
	if(pid > 0)
	{
		printf("this is the parent process!\n");
	}
	else if(pid == 0)
	{
		printf("this is the child process\n");
	}
	else
	{
		printf("fork error!\n");
	}

	return 0;
}

结果:
大部分情况下,父进程先运行,子进程后运行

示例:PV操作
代码:

//sem1.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <unistd.h>

//       int semget(key_t key, int nsems, int semflg);
//       int semctl(int semid, int semnum, int cmd, ...);
//       int semop(int semid, struct sembuf *sops, size_t nsops);

union semun {
	int              val;    /* Value for SETVAL */
	struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
	unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  /* Buffer for IPC_INFO 
				    (Linux-specific) */
};

void sem_p(int semid)
{
	struct sembuf sem_b;

        sem_b.sem_num = 0;
        sem_b.sem_op = -1;
        sem_b.sem_flg = 0;

        semop(semid,&sem_b,1);

        printf("pGetKey ok!\n");
}

void sem_v(int semid)
{
        struct sembuf sem_b;

        sem_b.sem_num = 0;
        sem_b.sem_op = 1;
        sem_b.sem_flg = 0;

        semop(semid,&sem_b,1);

        printf("vPutBackKey ok!\n");
}

int main()
{
	int semid;
	pid_t pid;
	key_t key;
	key = ftok(".",1);

	semid = semget(key,1,IPC_CREAT|0666);//创建或获取信号量
	if(semid == -1){
		printf("semget error!\n");
	}

	union semun initsem;
	initsem.val = 0;

	semctl(semid,0,SETVAL,initsem);//初始化信号量
		//0操作第1个信号量,SETVAL设置信号量的val值为initsem	

	pid = fork();
	if(pid > 0)
	{
		sem_p(semid);
		printf("this is the parent process!\n");
		sem_v(semid);

		semctl(semid,0,IPC_RMID);
	}
	else if(pid == 0)
	{
		printf("this is the child process\n");
		sem_v(semid);
	}
	else
	{
		printf("fork error!\n");
	}

	return 0;
}

结果:
这时候锁的数量为0,所以父进程在执行拿锁的时候阻塞在那里,让子进程放锁回去后才继续执行。
在这里插入图片描述
示例:PV操作封装
代码:

//sem00.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/sem.h>
#include <unistd.h>

// 联合体,用于semctl初始化
union semun
{
    int              val; /*for SETVAL*/
    struct semid_ds *buf;
    unsigned short  *array;
};
 
// 初始化信号量
int init_sem(int sem_id, int value)
{
    union semun sem_arg;
    sem_arg.val = value;
    if(semctl(sem_id, 0, SETVAL, sem_arg) == -1)
    {
        perror("Init Semaphore Error");
        return -1;
    }
    return 0;
}
 
// P操作:
//  若信号量值为1,获取资源并将信号量值-1 
//  若信号量值为0,进程挂起等待
int sem_p(int sem_id)
{
    struct sembuf sbuf;
    sbuf.sem_num = 0; /*序号*/
    sbuf.sem_op = -1; /*P操作*/
    sbuf.sem_flg = SEM_UNDO;
 
    if(semop(sem_id, &sbuf, 1) == -1)
    {
        perror("P operation Error");
        return -1;
    }
    return 0;
}
 
// V操作:
//  释放资源并将信号量值+1
//  如果有进程正在挂起等待,则唤醒它们
int sem_v(int sem_id)
{
    struct sembuf sbuf;
    sbuf.sem_num = 0; /*序号*/
    sbuf.sem_op = 1;  /*V操作*/
    sbuf.sem_flg = SEM_UNDO;
 
    if(semop(sem_id, &sbuf, 1) == -1)
    {
        perror("V operation Error");
        return -1;
    }
    return 0;
}
 
// 删除信号量集
int del_sem(int sem_id)
{
    union semun sem_arg;
    if(semctl(sem_id, 0, IPC_RMID, sem_arg) == -1)
    {
        perror("Delete Semaphore Error");
        return -1;
    }
    return 0;
}
 
 
int main()
{
    int sem_id;  // 信号量集ID
    key_t key;  
    pid_t pid;
 
    // 获取key值
    if((key = ftok(".", 'z')) < 0)
    {
        perror("ftok error");
        exit(1);
    }
 
    // 创建信号量集,其中只有一个信号量
    if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
    {
        perror("semget error");
        exit(1);
    }
 
    // 初始化:初值设为0资源被占用
    init_sem(sem_id, 0);
 
    if((pid = fork()) == -1)
        perror("Fork Error");
    else if(pid == 0) /*子进程*/ 
    {
        sleep(2);
        printf("Process child: pid=%d\n", getpid());
        sem_v(sem_id);  /*释放资源*/
    }
    else  /*父进程*/
    {
        sem_p(sem_id);   /*等待资源*/
        printf("Process father: pid=%d\n", getpid());
        sem_v(sem_id);   /*释放资源*/
        del_sem(sem_id); /*删除信号量集*/
    }
    return 0;
}

参考:
1、【Unix编程】进程间通信(IPC)
2、Linux 信号(signal)
3、Linux系统编程之进程间的通信(信号量)
4、typedef void (*sighandler_t)(int);类型说明
5、UNIX环境编程(第3版)

最后谢谢阅读,笔者乃小白,如有错误之处还请指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Apibro

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值