UNIX环境高级编程_进程间通信_信号,管道,消息队列,共享内存,信号量

这篇笔记简要记录一下进程间通信这一大话题。我们知道每个进程都使用独立的4G的虚拟内存(每个虚拟内存都对应不同的物理内存),进程之间的虚拟内存不能互相访问,保证了进程空间的独立性。独立的进程之间如何通信,这就是本篇笔记要记录的内容。

1 进程间通信

进程间通信一共有如下几种方式,下面分别记录一下。
1 信号
2 管道通信
3 消息队列
4 共享内存
5 信号量

1.1 进程通信方式1:信号

信号是一种不精确的通信方式,只能通知某件事情发生了,而不能确定具体发生了什么事情。信号优点类似“”烽火狼烟”,只能确定敌人来了,而不能确定来了多少敌人,我方要以什么战术进攻。
在Linux下,在窗口进程下按下ctrl + c 就可以结束一个正在运行的进程,这就是窗口进程发送了一个SIGTERM信号给正在运行的进程,如果正在运行的进程没有捕获该信号做其他事情(比如忽略该信号),该进程就会被中断。
通过Kill -l 命令可以查看到Linux系统下的信号。
在这里插入图片描述
我们可以使用signal函数来捕获信号,在捕获函数中实现自己想要的操作。比如捕获SIGINT信号,代码如下:

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

void signal_fun1(int signo)
{
	printf("SIGNAL signo = %d\n",signo);
	if (signo == SIGINT)
	{
		printf("signo == SIGINT,%d\n",signo);
	}
	else if (signo == SIGQUIT)
	{
		printf("signo == SIGQUIT,%d\n",signo);
	}
}

int main()
{
	
	// signal(SIGINT,SIG_IGN);
	signal(SIGINT,signal_fun1);
	while(1);
	return 0;
}

在这里插入图片描述

1.2 进程通信方式2:管道通信(无名管道+有名管道)

管道通信是说,两个进程通过管道文件进行通信。管道文件分为两种,有名管道和无名管道。

1.2.1 无名管道

无名管道通信原理

管道就是一个管道文件,在内存中创建一个这样的管道文件,这样两个进程通信就可以通过OS提供的文件进行通信了。如下图所示,调用无名管道的API,在内核中开辟一个管道文件,然后A进程和B进程通过操作管道文件的描述符,向文件中读数据/写数据就可以实现进程的管道通信了。
在这里插入图片描述
无名管道之所以是“无名”是因为不返回明确的文件描述符,所以无法通过open这样的方式打开,所以称为无名。因此,无名管道只适用于父子进程间通信,因为无名管道没有文件描述符,只能通过父子进程继承的方式,让子进程获取到父进程创建的无名管道文件描述符,实现父子进程的通信。通信原理如下:
在这里插入图片描述

代码实现
  • 父子进程的单项通信
1.	#include <unistd.h>  
2.	#include <stdlib.h>  
3.	#include <stdio.h>  
4.	#include <strings.h>  
5.	  
6.	#define STR "parent process write hello"  
7.	void printf_err(char *str)  
8.	{  
9.	    perror(str);  
10.	    exit(-1);  
11.	}  
12.	  
13.	int main()  
14.	{  
15.	    int ret = 0;  
16.	    int pipefd[2] = {0}; // fd[0]:read   fd[1]:write  
17.	    ret =  pipe(pipefd);  // 在内核中开辟一个管道文件,并返回读写文件描述符
18.	    if (ret == -1)  
19.	    {
20.	        printf_err("pipe fail");  
21.	    }
22.	    printf("fd[0] = %d,fd[1]=%d\n",pipefd[0],pipefd[1]);  
23.	  
24.	    ret = fork();  // create child process  
25.	    if (ret > 0)  // parent process   
26.	    {  
27.	        close(pipefd[0]);   // 关闭没有使用的文件描述符  
28.	        while (1)  
29.	        {  
30.	            write(pipefd[1],STR,sizeof(STR));  
31.	            sleep(1);  
32.	        }  
33.	    }  
34.	    else if (ret == 0)  // child process   
35.	    {  
36.	        close(pipefd[1]);  
37.	  
38.	        char buf[30];  
39.	        bzero(buf,sizeof(buf));  
40.	  
41.	        while (1)  
42.	        {  
43.	            read(pipefd[0],buf,sizeof(buf));  
44.	            printf("%s\n",buf);  
45.	            sleep(1);  
46.	        }  
47.	  
48.	    }  
49.	      
50.	  
51.	    return 0;  
52.	}

在这里插入图片描述
为了避免干扰,通常把没有使用的文件描述关闭。如果关闭全部的文件描述符,内核会发送一个SIGPIPE信号,这个信号的默认动作是终止,所以收到这个信号的进程或被终止。

  • 父子进程的双向通信
    在这里插入图片描述
    创建两个无名管道,父进程向管道1中写数据,子进程从管道1中读数据;父进程关闭管道1的读端,子进程关闭管道1的写段;对于管道2来说,子进程关闭读端,父进程关闭写端。
1.	int main()  
2.	{  
3.	    int ret = 0;  
4.	    int pipefd1[2] = {0}; // fd[0]:read   fd[1]:write  
5.	    int pipefd2[2] = {0};  
6.	  
7.	    ret =  pipe(pipefd1);  
8.	    if (ret == -1)  
9.	    {  
10.	        printf_err("pipe1 fail");  
11.	    }  
12.	    ret =  pipe(pipefd2);  
13.	    if (ret == -1)  
14.	    {  
15.	        printf_err("pipe2 fail");  
16.	    }  
17.	  
18.	    printf("fd[0] = %d,fd[1]=%d\n",pipefd1[0],pipefd1[1]);  
19.	    printf("fd[0] = %d,fd[1]=%d\n",pipefd2[0],pipefd2[1]);  
20.	  
21.	    ret = fork();  // create child process  
22.	    if (ret > 0)  // parent process   
23.	    {  
24.	        close(pipefd1[0]);  // 关闭fd1   
25.	        close(pipefd2[1]);  
26.	  
27.	        char buf[30] = {0};  
28.	        while (1)  
29.	        {  
30.	            write(pipefd1[1],"hello",5);  
31.	            sleep(1);  
32.	  
33.	            bzero(buf,sizeof(buf));  
34.	  
35.	            read(pipefd2[0],buf,sizeof(buf));  
36.	            printf("parent ,rec data:%s\n",buf);  
37.	        }  
38.	    }  
39.	    else if (ret == 0)  // child process   
40.	    {  
41.	        close(pipefd1[1]);  
42.	        close(pipefd2[0]);  
43.	  
44.	        char buf[30] = {0};  
45.	        while (1)  
46.	        {  
47.	            bzero(buf,sizeof(buf));  
48.	  
49.	            read(pipefd1[0],buf,sizeof(buf));  
50.	            printf("child ,rec data:%s\n",buf);  
51.	  
52.	            write(pipefd2[1],"world",5);  
53.	            sleep(1);     
54.	        }  
55.	    }  
56.	    return 0;  
57.	}  

在这里插入图片描述

无名管道缺点

无法用于非亲缘进程之间,因为非亲缘进程之间没办法继承管道的文件描述符;
无法实现多进程之间的网状通信,不能实现任意两个进程之间的通信,非亲缘关系的两个进程通信,可以用下面的有名管道。

1.2.2 有名管道

有名管道的原理

调用相应的有名管道API创建好“有名管道”后,会在相应的路径下面看到一个叫某某名字的 “有名管道文件”。不管是有名管道,还是无名管道,它们的本质其实都是一样的,它们都是内核所开辟的一段缓存空间,以文件的方式管理这段内存。
因为调用API时候,可以返回文件描述符,所以有名管道可以应用于两个非亲缘进程之间。

代码实现
  • 有名管道实现的单项通信
    代码1:
1.	int create_open_fifo(char *filename,int open_mode)  
2.	{  
3.	    int ret = -1;  
4.	    int fd = -1;  
5.	      
6.	    ret = mkfifo(filename,0664);  
7.	    if (ret == -1 && errno != EEXIST)  
8.	    {  
9.	        print_err("mkfifo fail\n");  
10.	    }  
11.	      
12.	    fd = open(filename,open_mode);  
13.	    if (fd == -1)  
14.	    {  
15.	        print_err("open fail\n");  
16.	    }  
17.	}  
18.	  
19.	  
20.	int main()  
21.	{  
22.	    // 1 create pipe   
23.	    int ret = 0;  
24.	    int fd1 = 0;  
25.	    int fd2 = 0;  
26.	    char buf[100] = {0};  
27.	    fd1 = create_open_fifo(FILENAME1,O_WRONLY);  
28.	    // fd2 = create_open_fifo(FILENAME2,O_RDWR);  
29.	      
30.	    while (1)   
31.	    {  
32.	        bzero(buf,sizeof(buf));  
33.	        scanf("%s",buf);  
34.	        write(fd1,buf,sizeof(buf));  
35.	    }  
36.	      
37.	    return 0;  
38.	} 

代码2:

1.	int create_open_fifo(char *filename,int open_mode)  
2.	{  
3.	    int ret = -1;  
4.	    int fd = -1;  
5.	      
6.	    ret = mkfifo(filename,0664);  
7.	    if (ret == -1 && errno != EEXIST)  
8.	    {  
9.	        print_err("mkfifo fail\n");  
10.	    }  
11.	      
12.	    fd = open(filename,open_mode);  
13.	    if (fd == -1)  
14.	    {  
15.	        print_err("open fail\n");  
16.	    }  
17.	}  
18.	  
19.	  
20.	int main()  
21.	{  
22.	    // 1 create pipe   
23.	    int ret = 0;  
24.	    int fd1 = 0;  
25.	    int fd2 = 0;  
26.	    char buf[100] = {0};  
27.	    fd1 = create_open_fifo(FILENAME1,O_RDONLY);  
28.	    // fd2 = create_open_fifo(FILENAME2,O_RDWR);  
29.	      
30.	    while (1)   
31.	    {  
32.	        read(fd1,buf,sizeof(buf));  
33.	        printf("%s\n",buf);  
34.	        //sleep(1);  
35.	    }  
36.	      
37.	    return 0;  
38.	}  

在这里插入图片描述
在这里插入图片描述

  • 有名管道实现双向通信
  • 原理如下:
    在这里插入图片描述
    代码1:
1.	void print_err(char *str)  
2.	{  
3.	    perror(str);  
4.	    exit(-1);  
5.	}  
6.	  
7.	int create_open_fifo(char *filename,int open_mode)  
8.	{  
9.	    int ret = -1;  
10.	    int fd = -1;  
11.	      
12.	    ret = mkfifo(filename,0664);  
13.	    if (ret == -1 && errno != EEXIST)  
14.	    {  
15.	        print_err("mkfifo fail\n");  
16.	    }  
17.	      
18.	    fd = open(filename,open_mode);  
19.	    if (fd == -1)  
20.	    {  
21.	        print_err("open fail\n");  
22.	    }  
23.	}  
24.	  
25.	void signal_fun(int signo)  
26.	{  
27.	    remove(FILENAME1);  
28.	    
29.	    exit(-1);  
30.	}  
31.	  
32.	int main()  
33.	{  
34.	    // 1 create pipe   
35.	    int ret = 0;  
36.	    int fd1 = 0;  
37.	    int fd2 = 0;  
38.	    char buf[100] = {0};  
39.	    fd1 = create_open_fifo(FILENAME1,O_RDWR);  
40.	    fd2 = create_open_fifo(FILENAME2,O_RDWR);  
41.	      
42.	    ret = fork();  
43.	    if (ret > 0)  
44.	    {  
45.	        signal(SIGINT,signal_fun);  
46.	        while (1)   
47.	        {  
48.	            bzero(buf,sizeof(buf));  
49.	            scanf("%s",buf);  
50.	            write(fd1,buf,sizeof(buf));  
51.	        }  
52.	      
53.	    }  
54.	    else if (ret == 0)  
55.	    {  
56.	        while (1)   
57.	        {  
58.	            bzero(buf,sizeof(buf));  
59.	            read(fd2,buf,sizeof(buf));  
60.	            printf("rec:%s\n",buf);  
61.	        }  
62.	    }  
63.	    return 0;  
64.	}  

代码2:

1.	int main()  
2.	{  
3.	    // 1 create pipe   
4.	    int ret = 0;  
5.	    int fd1 = 0;  
6.	    int fd2 = 0;  
7.	    char buf[100] = {0};  
8.	    fd1 = create_open_fifo(FILENAME1,O_RDWR);  
9.	    fd2 = create_open_fifo(FILENAME2,O_RDWR);  
10.	      
11.	    ret = fork();  
12.	    if (ret > 0)  
13.	    {  
14.	        while (1)   
15.	        {  
16.	            bzero(buf,sizeof(buf));  
17.	            read(fd1,buf,sizeof(buf));  
18.	            printf("rec:%s\n",buf);  
19.	        }  
20.	      
21.	    }  
22.	    else if (ret == 0)  
23.	    {  
24.	        while (1)   
25.	        {  
26.	            bzero(buf,sizeof(buf));  
27.	            scanf("%s",buf);  
28.	            write(fd2,buf,sizeof(buf));  
29.	        }  
30.	    }  
31.	      
32.	    return 0;  
33.	}  

在这里插入图片描述
在这里插入图片描述

有名管道适用场景

当两个非亲缘进程通信时候,不管是亲缘还是非亲缘,都可以使用有名管道来通信。

1.3 消息队列

Linux继承了System V 中的实现的消息队列,共享内存和信号量。

消息队列通信原理

消息队列的本质就是在内核中创建的用于存放消息的队列,只不过队列通过链表实现。由于是存放消息的,所以我们就把这个链表称为消息队列,这个队列不必非要按照先进先出的方式。通信的进程通过共享操作同一个消息队列,就能实现进程间通信。
在这里插入图片描述
上图中, 消息编号:识别消息用;消息正文:真正的信息内容

消息队列的实现步骤

**第一步:**使用msgget函数创建新的消息队列、或者获取已存在的某个消息队列,并返回唯一标识消息队列的标识符(msqID),后续收发消息就是使用这个标识符来实现的。
**第二步:**收发消息
发送消息:使用msgsnd函数,利用消息队列标识符发送某编号的消息
接收消息:使用msgrcv函数,利用消息队列标识符接收某编号的消息
**第三步:**使用msgctl函数,利用消息队列标识符删除消息队列

代码实现

  • 创建消息队列
1.	int create_get_msgque()  
2.	{  
3.	    int msgid = -1;  
4.	    int fd = -1;  
5.	    key_t key = 0;  
6.	    //   
7.	    fd = open(PATH,O_RDWR|O_CREAT,0664);  
8.	    if (fd == -1)  
9.	    {  
10.	        print_err("open fail\n");  
11.	    }  
12.	  
13.	    //   
14.	    key = ftok(PATH,'b');  
15.	    if (key == -1)  
16.	    {  
17.	        print_err("ftok fail");  
18.	    }  
19.	  
20.	    //   
21.	    msgid = msgget(key,0664|IPC_CREAT);  
22.	    if (-1 == msgid)  
23.	    {  
24.	        print_err("ftok fail");  
25.	    }  
26.	  
27.	    return msgid;  
28.	}  
29.	  
30.	int main()  
31.	{  
32.	    int ret = -1;  
33.	    long recv_msgtype = 0;  
34.	    int msgid = -1;  
35.	      
36.	    msgid = create_get_msgque();  
37.	      
38.	    ret = fork();  
39.	    if (ret > 0)  
40.	    {  
41.	  
42.	    }  
43.	    else if (ret == 0)  
44.	    {  
45.	          
46.	    }  
47.	  
48.	      
49.	    return 0;  
50.	}  

使用ipcs验证消息队列是否被创建成功
使用ipcs命令即可查看,可跟接的选项有:

  • a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
  • m:只显示共享内存的信息
  • q: 只显示消息队列的信息
  • s: 只显示信号量的信息
    在这里插入图片描述
    在这里插入图片描述
    适用消息队列方式实现进程间通信:
1.	int create_get_msgque()  
2.	{  
3.	    int msgid = -1;  
4.	    int fd = -1;  
5.	    key_t key = 0;  
6.	    //   
7.	    fd = open(PATH,O_RDWR|O_CREAT,0664);  
8.	    if (fd == -1)  
9.	    {  
10.	        print_err("open fail\n");  
11.	    }  
12.	  
13.	    //   
14.	    key = ftok(PATH,'b');  // 获取唯一key值,用于创建消息队列
15.	    if (key == -1)  
16.	    {  
17.	        print_err("ftok fail");  
18.	    }  
19.	  
20.	    //   
21.	    msgid = msgget(key,0664|IPC_CREAT);  
22.	    if (-1 == msgid)  
23.	    {  
24.	        print_err("ftok fail");  
25.	    }  
26.	  
27.	    return msgid;  
28.	}  
29.	  
30.	int main(int argc,char **argv)  
31.	{  
32.	    int ret = -1;  
33.	    long recv_msgtype = 0;  
34.	    int msgid = -1;  
35.	    struct msgbuf msg_buf = {0};  
36.	      
37.	    if (argc != 2)  
38.	    {  
39.	        printf("err");  
40.	        exit(-1);  
41.	    }  
42.	      
43.	    recv_msgtype = atol(argv[1]);  
44.	      
45.	    msgid = create_get_msgque();  
46.	      
47.	      
48.	    ret = fork();  
49.	    if (ret > 0)  
50.	    {  
51.	  
52.	        while (1)  
53.	        {  
54.	            printf("input message:");  
55.	            scanf("%s",msg_buf.mtext);  
56.	              
57.	            printf("input snd_msgtype:\n");  
58.	            scanf("%ld",&msg_buf.mtype);  
59.	            msgsnd(msgid,&msg_buf,1024,0); // int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);  
60.	        }  
61.	    }  
62.	    else if (ret == 0)  
63.	    {  
64.	        while (1)  
65.	        {  
66.	            // bzero(&msg_buf,sizeof(msg_buf));  
67.	            ret = msgrcv(msgid, &msg_buf, 1024, recv_msgtype, 0);  
68.	            if (ret > 0)  
69.	            {  
70.	                printf("%s\n",msg_buf.mtext);  
71.	            }  
72.	        }  
73.	    }  
74.	  
75.	      
76.	    return 0;  
77.	}  

在这里插入图片描述
在这里插入图片描述

1.4 共享内存

共享内存实现原理

共享内存就是OS在物理内存中开辟一大段缓存空间。管道、消息队列调用read、write、msgsnd、msgrcv等API来读写操作。而进程间使用共享内存通信时,进程是直接使用地址来共享读写的,避免了中间的调用过程。对于小数据量的通信来说,使用管道和消息队列这种使用API读写的通信方式很合适,但是如果进程涉及到超大量的数据通信时,使用“共享内存”这种直接使用地址操作的通信方式,效率会更高。
在这里插入图片描述
【共享内存方式就是让两个进程共享一块物理内存,这个物理内存和该进程映射到的原本的物理内存的关系:共享的物理内存只是从进程的虚拟空间中拿出来一块,映射到了新的物理内存上】

共享内存实现步骤

第一步:进程调用shmget函数创建新的或获取已有共享内存,shm是share memory的缩写。
第二步:进程调用shmat函数,将物理内存映射到自己的进程空间,实现虚拟地址和真实物理地址建立一一对应的映射关系。建立映射后,就可以直接使用虚拟地址来读写共享的内存空间了。
第三步:shmdt函数,取消映射;
第四步:调用shmctl函数释放开辟的那片物理内存空间;

1.5 信号量

信号量实现原理

信号量其实是OS创建的一个共享变量,进程在进行操作之前,会先检查这个变量的值,来达到加锁的目的。这变量的值就是一个标记,通过这个标记就可以知道可不可以操作,以实现互斥或同步。
当多个进程/线程进行共享操作时,使用信号量进行资源保护,以防止出现相互干扰的情况。为了避免出现相互干扰的问题,就需要加入资源保护的措施,保护的目的就是,保证每个进程在没有把数据读、写完整之前,其它进程不能进行读、写操作。
资源保护分为两种,一种是互斥,另一种是同步。互斥时候,不考虑先后顺序;而同步时候,要考虑加锁的先后顺序。

代码实现

通过同步让三个亲缘进程按照顺序打印出111111、222222、333333。
在这里插入图片描述

int main(void)
{
	int semid = 0;
	int i = 0;
	int ret = 0;
	int semnum_buf[1] = {0};
	
	semid = create_sem(NSEMS);
	
	// init sem 0 1 2 
	for (i = 0; i < NSEMS;i++)
	{
		if (i == 0)
		{		// void init_sem(int semid,int semnum,int val)
			init_sem(semid,i,1); // 第二步,初始化信号量,初值设置为1表示互斥
		}
		else
		{
			init_sem(semid,i,0);
		}

	}
	
	ret = fork();
	if (ret > 0)
	{
		ret = fork();
		if (ret > 0)	// parent process
		{
			while (1)
			{
				semnum_buf[0] = 2;
				p_sem(semid,semnum_buf,1); //p_sem(int semid,int semnum_buf[],int nsops);
				
				printf("333\n");
				sleep(1);
				
				semnum_buf[0] = 0;
				v_sem(semid,semnum_buf,1); //p_sem(int semid,int semnum_buf[],int nsops);
				
			}
		}
		else if (ret == 0)
		{
			while (1)
			{
				semnum_buf[0] = 1;
				p_sem(semid,semnum_buf,1); //p_sem(int semid,int semnum_buf[],int nsops);
				
				printf("222\n");
				sleep(1);
				
				semnum_buf[0] = 2;
				v_sem(semid,semnum_buf,1); //p_sem(int semid,int semnum_buf[],int nsops);
				
			}
		
		}
		

	}
	else if (ret == 0)
	{
		while (1)
		{
			semnum_buf[0] = 0;
			p_sem(semid,semnum_buf,1); //p_sem(int semid,int semnum_buf[],int nsops);
			
			printf("111\n");
			sleep(1);
			
			semnum_buf[0] = 1;
			v_sem(semid,semnum_buf,1);
		}
	
	}
	
	return 0;
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值