【Linux C | 进程】Linux 进程间通信的10种方式(1)

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭介绍 🍭
😎金句分享😎:🍭你不能选择最好的,但最好的会来选择你------泰戈尔🍭

本文未经允许,不得转发!!!

目录

  • 🎄一、管道(无名管道)
    • [✨1.1 管道介绍](#✨1.1 管道介绍)
    • [✨1.2 例子](#✨1.2 例子)
  • 🎄二、命名管道FIFO
    • [✨2.1 命名管道FIFO介绍](#✨2.1 命名管道FIFO介绍)
    • [✨2.2 例子](#✨2.2 例子)
  • [🎄三、消息队列(System V IPC)](#🎄三、消息队列(System V IPC))
    • [✨3.1 消息队列(System V IPC)介绍](#✨3.1 消息队列(System V IPC)介绍)
    • [✨3.2 例子](#✨3.2 例子)
  • [🎄四、信号量(System V IPC)](#🎄四、信号量(System V IPC))
    • [✨4.1 消息队列(System V IPC)介绍](#✨4.1 消息队列(System V IPC)介绍)
    • [✨4.2 例子](#✨4.2 例子)
  • [🎄五、共享内存(System V IPC)](#🎄五、共享内存(System V IPC))
    • [✨5.1 共享内存(System V IPC)介绍](#✨5.1 共享内存(System V IPC)介绍)
    • [✨5.2 例子](#✨5.2 例子)
  • 🎄六、总结

下表是进程间通信的十种方式

🎄一、管道(无名管道)

✨1.1 管道介绍

管道是半双工的通信方式,数据只能单向流动,管道的作用是在有亲缘关系的进程之间传递消息。所谓亲缘关系是指,只要调用进程使用pipe函数, 打开的管道文件就会在fork之后, 被各个后代进程所共享。

这个无名管道可以理解为:没有实体文件与之关联, 靠的是世代相传的文件描述符来进行数据的读写。

无名管道可以使用函数pipe来创建,函数原型如下:

c 复制代码
#include <unistd.h>
int pipe(int pipefd[2]);

✨1.2 例子

看使用例子,父进程调用了pipe函数创建了管道文件,fork之后的子进程可以直接使用管道文件:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>  

#define PIPE_INPUT		0
#define PIPE_OUTPUT		1

int main()
{
	int pipe_fds[2];
	pipe(pipe_fds); // 创建无名管道
	
	pid_t pid = fork();
    if(pid == 0) {// 子进程
		printf("子进程[%d]开始执行, 关闭输入管道,写数据到输出管道\n", getpid()); 
		close(pipe_fds[PIPE_INPUT]);// 关闭输入管道
		write(pipe_fds[PIPE_OUTPUT], "test data", strlen("test data"));// 写入管道
        exit(0);
    }
	else if(pid > 0)
	{
		sleep(2); //延时一会,让子进程先运行
		printf("父进程[%d]开始执行, 关闭输出管道,读取管道数据\n", getpid()); 
		close(pipe_fds[PIPE_OUTPUT]);// 关闭输出管道
		char buf[256] = {0,};
		int readSize = read(pipe_fds[PIPE_INPUT], buf, sizeof(buf));
		printf("父进程[%d]从管道读取到%d个字节的数据[%s]\n", getpid(), readSize, buf); 
        exit(0);
	}
	else
	{
		printf("Error in fork\n"); 
        exit(1); 
	}
	
	return 0;
}

🎄二、命名管道FIFO

✨2.1 命名管道FIFO介绍

上面的无名管道没有与实体文件关联,靠的是世代相传的文件描述符来进行数据交换。命名管道就是为了解决无名管道的这个问题而引入的。 FIFO与管道类似, 最大的差别就是有实体文件与之关联。 由于存在实体文件, 不相关的、没有亲缘关系的进程也可以通过使用FIFO来实现进程之间的通信。

创建命名管道的3种方式:

  • 1、调用C语言接口函数mkfifo创建:mkfifo("my_fifo", 0666);

    c 复制代码
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
  • 2、使用mkfifo命令创建:mkfifo -m 0666 my_fifo

  • 3、使用mknod命令创建:mknod -m 0666 my_fifo p

一旦FIFO文件创建好了, 就可以把它用于进程间的通信了。 一般的文件操作函数如open、 read、 write、 close、 unlink等都可以用在FIFO文件上。

✨2.2 例子

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	if(0 == access("./my_fifo",F_OK))
	{
		system("rm ./my_fifo");
	}
	/*创建管道文件, 下次运行需要先删除my_fifo文件,否则mkfifo报错*/
    if(mkfifo("my_fifo", 0666) < 0)
    {
        perror("mkfifo");
        return -1;
    }
	
	pid_t pid = fork();
    if(pid == 0) {// 子进程
		printf("子进程[%d]开始执行, 打开my_fifo文件,循环往里写数据\n", getpid());
		int fd = open("my_fifo", O_WRONLY);
		if(fd < 0)
		{
			return -1;
		}
		int i = 9;
		while(i>=0)
		{
			printf("子进程[%d]写入数据:%d\n", getpid(), i);
			char buf[256] = {0,};
			sprintf(buf,"%d",i);
			write(fd, buf, strlen(buf));
			i--;
			sleep(1);
		}
		close(fd);
		printf("子进程[%d]退出\n", getpid());
        return 0;
    }
	else if(pid > 0)// 父进程
	{
		sleep(5); //延时一会,让子进程先运行
		printf("父进程[%d]开始执行, 打开my_fifo文件,读取数据\n", getpid()); 
		int fd = open("my_fifo", O_RDONLY);
		if(fd < 0)
		{
			return -1;
		}
		char buf[256] = {0,};
		int readSize = 0;
		while((readSize = read(fd, buf, sizeof(buf)) ) > 0)
		{
			printf("父进程[%d]读取到%d个字节数据:[%s]\n", getpid(),readSize, buf);
			memset(buf, 0, sizeof(buf));
		}
		close(fd);
		printf("父进程[%d]退出\n", getpid());
        return 0;
	}
	else
	{
		printf("Error in fork\n"); 
        exit(1); 
	}
	
	return 0;
}

运行结果:

🎄三、消息队列(System V IPC)

✨3.1 消息队列(System V IPC)介绍

有三种被称为XSI IPC的进程间通信,消息队列,信号量,共享内存。XSI IPC函数是基于System V的IPC函数。这里介绍的消息队列就属于其中一种,后面还有介绍其余两种,消息队列比较少用了,是一种逐渐被淘汰的通信方式,为了完整性,这里还是介绍一下,感兴趣的可以继续了解。

前面的管道通信,如果从管道中读取到100个字节,你无法确认这100个字节是单次写入的100字节, 还是分10次每次10字节写入的, 你也无法知晓这100个字节是几个消息。System V消息队列就不存在这种问题,因为它是基于消息通信的。无需从字节流解析完整的消息,而且每个消息有type字段作为消息类型。

消息队列编程步骤:

  • 1、生成 key,System V IPC的标识ID都是通过key来获取的,key的生成方式有三种:
    ①随机选择一个整数值作为key值,这个值必须不和其他key重复,例如:#define MSG_KEY 10086
    ②使用IPC_PRIVATE,例如:id = msgget(IPC_PRIVATE,S_IRUSR | S_IWUSR);
    ③使用ftok函数, 根据文件名生成一个key,例如:key_t key = ftok(".", 100);
  • 2、使用msgget()创建/获取消息队列,返回值为队列标识符。
    服务端创建:int msgid = msgget(key, 0666|IPC_CREAT);
    客户端获取:int msgid = msgget(key, 0);
  • 3、写入/取出消息;
    服务端写入:msgsnd(msgid, &msg, sizeof(msg.buf), 0);
    客户端获取:msgrcv(msgid, &msg, sizeof(msg)-sizeof(long), 0, 0);
  • 4、msgctl删除消息队列
    msgctl(msgid, IPC_RMID, NULL);

✨3.2 例子

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

typedef struct _MSG_TYPE
{
    long mtype;//消息类型
    char buf[256];//有效数据
}MSG_TYPE;

int main()
{
	// 1 生成key
    key_t key = ftok(".", 100);

	// 2 创建子进程
	pid_t pid = fork();
    if(pid == 0) {// 子进程
		printf("子进程[%d]开始执行, 创建消息队列,循环往里写数据\n", getpid());
		// 创建消息队列
		int msgid = msgget(key, 0666|IPC_CREAT);
		if(msgid == -1)
		{
			perror("msgget failed");
			exit(1);
		}
		// 发送数据
		int i = 9;
		MSG_TYPE msg;
		while(i>=0)
		{
			memset(&msg, 0, sizeof(msg));
			msg.mtype = i;
			sprintf(msg.buf, "hello-%d", i);
			msgsnd(msgid, &msg, sizeof(msg.buf), 0);//阻塞
			printf("子进程[%d]写入数据:hello-%d\n", getpid(), i);
			i--;
			sleep(1);
		}
		
		// 删除队列
		if(msgctl(msgid, IPC_RMID, NULL) == -1)
		{
			perror("msgctl failed");
			exit(3);
		}
		printf("子进程[%d]退出\n", getpid());
        return 0;
    }
	else if(pid > 0)// 父进程
	{
		sleep(3); //延时一会,让子进程先运行
		printf("父进程[%d]开始执行, 获取消息队列,读取数据\n", getpid()); 
		int msgid = msgget(key, 0); 
		if(msgid == -1)
		{
			perror("msgget failed");
			exit(1);
		}
		MSG_TYPE msg;
		while(1)
		{
			memset(&msg, 0, sizeof(msg));
			int res = msgrcv(msgid, &msg, sizeof(msg)-sizeof(long), 0, 0);//阻塞
			printf("res=%d, 消息:%s, 类型:%ld\n", res, msg.buf, msg.mtype);
			if(res == -1)
			{
				perror("msgrcv failed");
				break;
			}
		}
		
		// 删除队列
		if(msgctl(msgid, IPC_RMID, NULL) == -1)
		{
			perror("msgctl failed");
			exit(3);
		}
		printf("父进程[%d]退出\n", getpid());
        return 0;
	}
	else
	{
		printf("Error in fork\n"); 
        exit(1); 
	}
	
	return 0;
}

运行结果:

🎄四、信号量(System V IPC)

✨4.1 消息队列(System V IPC)介绍

信号量的作用是为了同步多个进程的操作。一般来说, 信号量是和某种预先定义的资源相关联的。

信号量是一个计数器,控制访问共享资源的最大并行进程总数。可以通过下面这个故事来了解信号量。

一套豪宅里有8个一模一样的卫生间和8把通用的钥匙。最初有8把钥匙放在钥匙存放处。 当同时使用卫生间的人数小于或等于8时, 大家都可以拿到一把钥匙, 各自使用各自的卫生间。 但是到第9个人和第10个人要使用卫生间时, 发现已经没有钥匙了, 所以他们就不得不等待了。

使用最广泛的信号量是二值信号量(binary semaphore), 对于这种信号量而言, 它只有两种合法值: 0和1, 对应一个可用的资源。 若当前有资源可用, 则与之对应的二值信号量的值为1; 若资源已被占用, 则与之对应的二值信号量的值为0。 当进程申请资源时, 如果当前信号量的值为0, 那么进程会陷入阻塞, 直到有其他进程释放资源, 将信号量的值加1才能被唤醒。

资源个数超过1个的信号量称为计数信号量(counting semaphore),例如,有个8个资源,最大同时允许8个进程使用。

信号量编程步骤:

  • 1、生成 key,System V IPC的标识ID都是通过key来获取的,key的生成方式有三种。参考上一节消息队列编程步骤;
  • 2、使用int semget(key_t key, int nsems, int semflg);创建/获取信号量集,返回值为信号量集标识符。
    第二个参数nsems表示信号量集中信号量的个数。如果并非创建信号量, 仅仅是访问已经存在的信号量集, 可以将nsems指定为0。
    semflg支持多种标志位。 目前支持IPC_CREAT和IPC_EXCL标志位
  • 3、设置信号量的初始值 int semctl(int semid, int semnum, int cmd,/* union semun arg*/);
  • 4、正常使用,实现信号量的++ --的原子性int semop(int semid, struct sembuf *sops, unsigned nsops);
  • 5、semctl删除消息信号量
    semctl(semid, 0, IPC_RMID);

✨4.2 例子

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

// 生成key
#define SEM_KEY		10086

int main()
{
	// 2 创建子进程
	pid_t pid = fork();
    if(pid == 0) {// 子进程
		printf("子进程[%d]开始执行, 创建信号量,使用资源\n", getpid());
		// 创建信号量集
		int semid = semget(SEM_KEY, 1, IPC_CREAT|0666);
		if(semid == -1)
		{
			perror("semget failed");
			exit(1);
		}
		
		// 设置第0个信号量的资源数量为1
		if(semctl(semid, 0, SETVAL, 1) == -1)
		{
			perror("semctl setval failed");
			exit(1);
		}
		
		// 使用资源,数量 -1
		struct sembuf op;
		op.sem_num = 0;//对下标为0的信号量操作
		op.sem_op = -1;//对信号量-1
		op.sem_flg = 0;//无法完成时阻塞等待
		semop(semid, &op, 1);
		printf("子进程[%d]访问共享资源\n", getpid());
		sleep(20);
		printf("子进程[%d]完成共享资源的访问\n",getpid());
		
		// 释放资源,数量 +1
		op.sem_op = 1;
		semop(semid, &op, 1);
		
        return 0;
    }
	else if(pid > 0)// 父进程
	{
		sleep(3); //延时一会,让子进程先运行
		printf("父进程[%d]开始执行, 获取信号量,准备使用资源\n", getpid()); 
		int semid = semget(SEM_KEY, 0, 0);
		if(semid == -1)
		{
			perror("semget failed");
			exit(1);
		}
		
		// 使用资源,数量 -1
		struct sembuf op;
		op.sem_num = 0;//对下标为0的信号量操作
		op.sem_op = -1;//对信号量-1
		op.sem_flg = 0;//无法完成时阻塞等待
		semop(semid, &op, 1);
		printf("父进程[%d]访问共享资源\n", getpid());
		sleep(3);
		printf("父进程[%d]完成共享资源的访问\n",getpid());
		
		// 释放资源,数量 +1
		op.sem_op = 1;
		semop(semid, &op, 1);
		
		// 删除信号量
		if(semctl(semid, 0, IPC_RMID) == -1)
		{
			perror("semctl failed");
			exit(3);
		}
		printf("父进程[%d]退出\n", getpid());
        return 0;
	}
	else
	{
		printf("Error in fork\n"); 
        exit(1); 
	}
	
	return 0;
}

🎄五、共享内存(System V IPC)

✨5.1 共享内存(System V IPC)介绍

共享内存是所有IPC手段中最快的一种。 它之所以快是因为共享内存一旦映射到进程的地址空间,进程之间数据的传递就不须要涉及内核了。

建立共享内存之后, 进程从此就像操作普通进程的地址空间一样操作这块共享内存, 一个进程可以将信息写入这片内存区域, 而另一个进程也可以看到共享内存里面的信息, 从而达到通信的目的。

允许多个进程同时操作共享内存, 就不得不防范竞争条件的出现。因此, 共享内存这种进程间通信的手段通常不会单独出现, 总是和信号量、 文件锁等同步的手段配合使用。

信号量编程步骤:

  • 1、生成 key,System V IPC的标识ID都是通过key来获取的,key的生成方式有三种。参考上一节消息队列编程步骤;
  • 2、使用int shmget(key_t key, size_t size, int shmflg);创建/获取共享内存段,返回值为共享内存的标识符。
    其中第二个参数size必须是正整数, 表示要创建的共享内存的大小。
    第三个参数支持IPC_CREAT和IPC_EXCL标志位。 如果没有设置IPC_CREAT标志位, 那么第二个参数size对共享内存段并无实际意义, 但是必须小于或等于共享内存的大小, 否则会有EINVAL错误。
  • 3、映射共享内存,得到虚拟地址。void *shmat(int shmid, const void *shmaddr, int shmflg);
    其中, 第二个参数是用来指定将共享内存放到虚拟地址空间的什么位置的。 大部分的普通青年都会将第二个参数设置为NULL, 表示用户并不在意, 一切交由内核做主。
    shmat如果调用成功, 则返回进程虚拟地址空间内的一个地址。就可以像使用malloc分配的空间一样使用共享内存。
  • 4、读写共享内存数据。
  • 5、解除映射。int shmdt(const void *shmaddr);
  • 6、销毁共享内存。shmctl(shmid, IPC_RMID, NULL) ;

✨5.2 例子

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

// 生成key
#define SHM_KEY		10010

int main()
{
	// 2 创建子进程
	pid_t pid = fork();
    if(pid == 0) {// 子进程
		printf("子进程[%d]开始执行, 创建共享内存段,使用创建共享内存\n", getpid());
		// 2.1 创建共享内存段
		int shmid = shmget(SHM_KEY, 8, IPC_CREAT|0666);
		if(shmid == -1)
		{
			perror("semget failed");
			exit(1);
		}
		
		// 2.2 映射共享内存,得到虚拟地址
		void *p = shmat(shmid, 0, 0);
		 if((void *)-1 == p)
		{
			perror("shmat failed");
			exit(2);
		}
		
		// 2.3 读写共享内存
		int *pi = p;
		*pi = 0xaaaaaaaa;
		*(pi+1) = 0x55555555;
		printf("子进程[%d]写入%x, %x\n", getpid(), *pi, *(pi+1));
		
		// 2.4 解除映射
		if(shmdt(p) == -1)
		{
			perror("shmdt failed");
			exit(3);
		}
		printf("子进程[%d]解除映射, 结束进程\n\n", getpid());
        return 0;
    }
	else if(pid > 0)// 父进程
	{
		sleep(3); //延时一会,让子进程先运行
		printf("父进程[%d]开始执行, 获取共享内存段,准备使用资源\n", getpid()); 
		// 3.1 获取共享内存段
		int shmid = shmget(SHM_KEY, 0, 0);
		if(shmid == -1)
		{
			perror("shmget failed");
			exit(1);
		}
		
		// 3.2 映射共享内存,得到虚拟地址
		void *p = shmat(shmid, 0, 0);
		if((void *)-1 == p)
		{
			perror("shmat failed");
			exit(2);
		}
		
		// 3.3 读写共享内存
		int x = *((int *)p);
		int y = *((int *)p + 1);
		printf("父进程[%d]读取数据:x=%#x y=%#x\n",getpid(), x, y);
		
		// 3.4 解除映射
		if(shmdt(p) == -1)
		{
			perror("shmdt failed");
			exit(3);
		}
		printf("父进程[%d]解除映射\n", getpid());
		
		// 3.5 销毁共享内存
		if(shmctl(shmid, IPC_RMID, NULL) == -1)
		{
			perror("shmctl");
			exit(4);
		}
		printf("父进程[%d]销毁共享内存, 结束进程\n", getpid());
        return 0;
	}
	else
	{
		printf("Error in fork\n"); 
        exit(1); 
	}
	
	return 0;
}

允许结果:

🎄六、总结

Linux 进程间通信有10种方式,本文先介绍了5种:无名管道、命名管道、XSI消息队列、XSI信号量、XSI共享内存,下篇文章将会介绍剩下的5个方式:POSIX消息队列、POSIX信号量、POSIX共享内存、信号、网络通信。

如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

相关推荐
Lovyk1 小时前
Linux 正则表达式
linux·运维
Fireworkitte2 小时前
Ubuntu、CentOS、AlmaLinux 9.5的 rc.local实现 开机启动
linux·ubuntu·centos
sword devil9002 小时前
ubuntu常见问题汇总
linux·ubuntu
ac.char2 小时前
在CentOS系统中查询已删除但仍占用磁盘空间的文件
linux·运维·centos
淮北也生橘124 小时前
Linux的ALSA音频框架学习笔记
linux·笔记·学习
华强笔记7 小时前
Linux内存管理系统性总结
linux·运维·网络
十五年专注C++开发8 小时前
CMake进阶: CMake Modules---简化CMake配置的利器
linux·c++·windows·cmake·自动化构建
phoenix09818 小时前
ansible部署lnmp-allinone
linux·运维·ansible
winds~9 小时前
【git】 撤销revert一次commit中的某几个文件
linux·c++
iY_n9 小时前
Linux网络基础
linux·网络·arm开发