Linux进程间通讯(IPC)


前言

进程间通信(Inter-Process Communication,简称IPC)是指在不同进程之间传播或交换信息的机制和技术。在现代操作系统中,同时运行着多个进程,它们可能需要相互协作、共享数据或进行通信来完成特定任务。进程间通信允许进程在同一台计算机上或不同计算机上进行交流和协作,从而提高系统的效率、可靠性、并发性和扩展性。以下是关于进程间通信的思维导图:


一、管道(Pipes)

1.命名管道(FIFO)

原理 

命名管道(FIFO,First In, First Out)是Linux系统中一种特殊的文件类型,用于实现进程间通信(IPC)。其实现原理可以归纳如下:

1. 文件系统中的表示

  • 文件形式存储:FIFO文件作为一种特殊的文件存放在文件系统中,它并不在磁盘上占用数据块来存储数据,而是仅仅用来标识内核中一条通道的存在。
  • 路径名关联:与匿名管道(pipe)不同,FIFO提供一个路径名与之关联,这使得任何能够访问该路径的进程都可以通过FIFO进行通信,而不仅仅是具有亲缘关系的进程。

2. 内核管理

  • 内核缓冲区:FIFO在内核空间中创建相应的内核缓冲区(buf),用于暂存数据。当数据被写入FIFO时,它实际上是被写入这个内核缓冲区;当数据被读取时,则是从这个缓冲区中取出。
  • 通信机制:当一个进程以读(r)的方式打开FIFO文件,而另一个进程以写(w)的方式打开该文件时,内核会在这两个进程之间建立一条管道,使得它们可以通过这条管道进行数据的传递。

3. 先进先出原则

  • 队列数据结构:FIFO的名称来源于其数据传递的先进先出(First In, First Out)原则,即最早写入的数据会最先被读出,从而保证了信息交流的顺序性。

4. 创建与操作

  • 创建:通过mkfifo命令或mkfifo函数可以创建FIFO文件。一旦创建,就可以像操作普通文件一样使用openreadwriteclose等系统调用来打开、读写、关闭FIFO。
  • 读写操作:写模式的进程向FIFO文件中写入数据,而读模式的进程从FIFO文件中读出数据。需要注意的是,如果FIFO为空且没有进程在读,则写入操作会阻塞,直到有进程打开FIFO进行读操作为止;同样,如果FIFO为满且没有进程在写,则读取操作也会阻塞。

5. 生命周期

  • 持久性:当进程对FIFO的使用结束后,FIFO文件仍然存在于文件系统中,除非对其进行删除操作,否则该FIFO文件不会自行消失。

创建命名管道

1.发送者(write.c)

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <fcntl.h>  
#include <sys/stat.h> 
#include <errno.h> 
  
int main(int argc , const char* argv[]) 
{  
    int fd;  
    char *myfifo = "/tmp/myfifo";  
  
    // 确保管道存在  
    if (mkfifo(myfifo, 0666) == -1) 
    {  
        if(errno != EEXIST)  // 检查错误类型是否是文件已存在
        {  
            perror("mkfifo");  
            exit(EXIT_FAILURE);  
        }  
    }  
  
    // 打开管道以写入  
    fd = open(myfifo, O_WRONLY);  
    if(fd == -1) 
    {  
        perror("open");  
        exit(EXIT_FAILURE);  
    }  
  
    // 写入数据到管道  
    char *message = "Hello from writer!";  
    write(fd, message, strlen(message) + 1);  
  
    close(fd); // 关闭管道文件
    
    unlink(myfifo); // 删除管道文件
    
    return 0;  
}

2.接收者(reader.c) 

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <fcntl.h>  
#include <string.h>  
  
int main(int argc , const char* argv[]) 
{  
    int fd;  
    char buffer[1024];  
    char *myfifo = "/tmp/myfifo";  
  
    // 打开管道以读取  
    fd = open(myfifo, O_RDONLY);  
    if(fd == -1) 
    {  
        perror("open");  
        exit(EXIT_FAILURE);  
    }  
  
    // 从管道读取数据  
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);  
    if(bytes_read == -1) 
    {  
        perror("read");  
        exit(EXIT_FAILURE);  
    }  
  
    buffer[bytes_read] = '\0'; // 确保字符串正确终止  
    printf("Received: %s\n", buffer);  
  
    close(fd);  
    return 0;  
}

编译和运行 

1.编译这两个程序:

gcc writer.c -o writer  
gcc reader.c -o reader

2.创建一个有名管道(或者确保mkfifo /tmp/myfifo已经运行)

mkfifo /tmp/myfifo

3.首先启动接收者(reader):

./reader

4.然后,在另一个终端或窗口中启动发送者(writer):

./writer

你应该在启动reader的终端中看到“Received: Hello from writer!”的消息。

2.匿名管道 

主要特点

  1. 半双工通信:匿名管道是半双工的,意味着在同一时间内,数据只能在一个方向上流动。这意味着它不能像全双工管道那样同时支持双向通信。
  2. 单向数据流:数据只能从管道的一端写入,从另一端读出。写入管道的数据遵循先进先出的原则,即先写入的数据先被读出。
  3. 基于内存:匿名管道不是普通的文件,不属于任何文件系统,而是仅存在于内存中。这使得匿名管道具有较快的通信速度,因为数据直接在内存中进行传输,减少了磁盘I/O操作。
  4. 固定大小的缓冲区:匿名管道在内存中对应一个缓冲区,不同系统的缓冲区大小可能不同。这个缓冲区采用环形队列的机制来管理数据,以实现生产者和消费者之间的数据交换。
  5. 阻塞与非阻塞:默认情况下,从管道中读数据是阻塞操作。如果管道中没有数据可读,读操作将等待直到有数据到来或管道被关闭。然而,通过特定的设置或调用,可以实现非阻塞的读操作。类似地,当管道满时,写操作也会阻塞,直到管道中有足够的空间来容纳新的数据。
  6. 无格式的数据传输:匿名管道传输的数据是无格式的,这要求通信双方必须事先约定好数据的格式,如数据的长度、类型等。否则,接收方可能无法正确解析发送方发送的数据。
  7. 生命周期与进程绑定:匿名管道的生命周期与进程紧密相关。当所有指向管道的文件描述符都被关闭时,管道的生命周期也随之结束。这意味着匿名管道只能用于具有公共祖先的进程之间(如父进程与子进程)的通信。
  8. 面向字节流:匿名管道通信是面向字节流的,即数据以字节为单位进行传输。这使得匿名管道可以传输各种类型的数据,包括文本、二进制文件等。
  9. 同步与互斥:匿名管道本身具有一定的同步与互斥效果。由于管道的缓冲区大小有限,当缓冲区满时,写操作会阻塞,直到缓冲区中有空间可写;当缓冲区空时,读操作会阻塞,直到缓冲区中有数据可读。这种机制有助于实现进程间的同步与互斥。
  10. 局限性:匿名管道只能用于具有血缘关系的进程之间的通信(如父子进程),这限制了它的使用范围。对于需要跨网络或在不同用户空间下运行的进程之间的通信,需要使用其他机制(如套接字、命名管道等)。

 创建匿名管道

以下是一个基于C语言的简单示例,该示例展示了如何使用匿名管道在父进程和子进程之间进行通信。在这个例子中,父进程将向管道中写入数据,而子进程将从管道中读取这些数据。 

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <string.h>  
#include <sys/types.h>  
#include <sys/wait.h>  
  
int main() {  
    int pipefd[2]; // 管道文件描述符数组  
    pid_t pid;  
    char buf[1024];  
    int nread;  
  
    // 创建管道  
    if (pipe(pipefd) == -1) {  
        perror("pipe");  
        exit(EXIT_FAILURE);  
    }  
  
    // 创建子进程  
    pid = fork();  
    if (pid == -1) {  
        perror("fork");  
        exit(EXIT_FAILURE);  
    }  
  
    if (pid == 0) {  
        // 子进程  
        close(pipefd[1]); // 关闭写端  
  
        // 读取父进程发送的数据  
        memset(buf, 0, sizeof(buf));  
        nread = read(pipefd[0], buf, sizeof(buf) - 1);  
        if (nread == -1) {  
            perror("read");  
            exit(EXIT_FAILURE);  
        }  
        printf("Child received: %s\n", buf);  
  
        close(pipefd[0]); // 关闭读端  
        exit(EXIT_SUCCESS);  
    } else {  
        // 父进程  
        close(pipefd[0]); // 关闭读端  
  
        // 写入数据到管道  
        const char *msg = "Hello from parent!";  
        write(pipefd[1], msg, strlen(msg) + 1);  
  
        close(pipefd[1]); // 关闭写端  
  
        // 等待子进程结束  
        wait(NULL);  
    }  
  
    return 0;  
}

程序说明

  1. 管道创建:使用pipe()函数创建一个管道,该函数返回两个文件描述符,pipefd[0]用于读取数据,pipefd[1]用于写入数据。

创建完管道之后处理文件描述符 

  1. 进程创建:通过fork()创建一个子进程。fork()的返回值在父进程中是子进程的PID,在子进程中是0,如果失败则返回-1。

  1. 文件描述符操作

    • 在子进程中,关闭管道的写端(pipefd[1]),然后读取数据。
    • 在父进程中,关闭管道的读端(pipefd[0]),然后写入数据。

  1. 数据交换:父进程向管道写入字符串"Hello from parent!",子进程从管道读取这个字符串并打印出来。

  2. 关闭文件描述符:在数据交换完成后,关闭相应的文件描述符以避免资源泄露。

  3. 等待子进程:父进程使用wait(NULL)等待子进程结束,以确保所有资源都被正确释放

3.匿名管道与FIFO的区别 

  • 匿名管道:仅存在于内存中,通常用于父子进程或兄弟进程之间的通信。它通过pipe()系统调用创建,并返回两个文件描述符,一个用于读,一个用于写。匿名管道是半双工的,即数据只能单向流动。
  • 命名管道(FIFO):具有持久性,以文件的形式存在于文件系统中,允许无亲缘关系的进程之间进行通信。它使用mkfifo()mknod()函数创建,创建后会在文件系统中生成一个特殊类型的文件,可以通过文件I/O操作来读取和写入数据。命名管道遵循先进先出(FIFO)的原则。

二、XSI进程间通讯

1.共享内存

共享内存是一种允许多个进程访问同一块内存区域的方式。这种方式通常比较高效,因为数据不需要在进程间复制,但需要处理进程间的同步和互斥问题。共享内存通过映射同一块物理内存到不同进程的地址空间来实现。 

原型

#include <sys/shm.h>
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr); 
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

 编程模型

进程A代码如下(示例): 

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int shmid;
char *shm;
//收到通知并读取数据
void sighander(int num)
{
	printf("接收到信号: %s\n", shm);  
	if(strcmp("quit", shm) == 0)
	{
		printf("通讯结束\n");	

		//取消映射
		if(shmdt(shm))
		{
			perror("shmdt");	
		}
		usleep(1000); //等待对方取消映射
		//删除共存内存
		if(shmctl(shmid, IPC_RMID, NULL))
		{
			perror("shmctl");
		}
		exit(0);
	}
}
int main(int argc , const char* argv[])
{
	//注册信号处理函数
	signal(SIGRTMIN, sighander);
	//创建共享内存
	int shmid = shmget(ftok(".", 110), 4096, IPC_CREAT | 0664);
	if(shmid < 0)
	{
		perror("shmget");	
		return -1;
	}

	//映射共享内存
	shm = shmat(shmid, NULL, 0);
	if(shm == (void *)-1)
	{
		perror("shmat");	
		shmctl(shmid, IPC_RMID, NULL);
		return -1;
	}
	
	pid_t pid = 0;
	printf("当前进程ID:%d\n", getpid());
	printf("输入要通讯进程ID:");
	scanf("%u", &pid);

	//写数据并通知其他进程
	while(1)
	{
		printf(">>>");	
		scanf("%s", shm);
		kill(pid, SIGRTMIN); // 通知其他进程
		if(strcmp("quit", shm) == 0)
		{
			printf("通讯结束\n");	
			break;
		}
	}
	
	//取消映射
	if(shmdt(shm))
	{
		perror("shmdt");	
		return -1;
	}
	usleep(1000);


	//删除共享内存
	if(shmctl(shmid, IPC_RMID, NULL))
	{
		perror("shmctl");	
		return -1;
	}
	return 0; 
}

 进程B代码如下(示例):

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <sys/ipc.h>  
#include <sys/shm.h>  
#include <signal.h>  
#include <unistd.h>  
  
#define SHM_SIZE 4096  
 
char *shm;  
  
// 信号处理函数  
void sighander(int num)  
{  
    printf("接收到信号: %s\n", shm);  
	if(strcmp("quit", shm) == 0)
	{
		printf("通讯结束\n");	

		//取消映射
		if(shmdt(shm))
		{
			perror("shmdt");	
		}
		usleep(1000); //等待对方取消映射
		exit(0);
	}
}  
  
int main(int argc, const char *argv[])  
{  
	/*if(argc < 2) 
    {  
        fprintf(stderr, "Usage: %s <shm_key>\n", argv[0]);  
        exit(EXIT_FAILURE);  
    }  
  
    key_t shm_key = atoi(argv[1]); // 从命令行参数获取共享内存键  */
    
	// 注册信号处理函数  
    signal(SIGRTMIN, sighander); // 监听来自进程A的信号
    
    printf("当前进程ID:%d\n", getpid());
  	
  	key_t shm_key = ftok(".", 110);
    // 获取共享内存  
    int shmid = shmget(shm_key, SHM_SIZE, IPC_CREAT);  
    if(shmid < 0) 
    {  
        perror("shmget");  
        exit(EXIT_FAILURE);  
    }  
  
    // 映射共享内存  
    shm = shmat(shmid, NULL, 0);  
    if(shm == (char *)-1) 
    {  
        perror("shmat");  
        shmctl(shmid, IPC_RMID, NULL); // 如果映射失败,则删除共享内存  
        exit(EXIT_FAILURE);  
    }  
    
    pid_t pid = 0;
    printf("输入要通讯进程ID:");
	scanf("%u", &pid); 
  
    // 主循环(这里我们简单地等待信号,不执行持续读写)  
    while(1) 
    {   
    	printf(">>>");	
		scanf("%s", shm);
		kill(pid, SIGRTMIN);
		if(strcmp("quit", shm) == 0)
		{
			printf("通讯结束\n");	
			break;
		}
    }  
  
    // 取消映射  
    if(shmdt(shm) == -1) 
    {  
        perror("shmdt");  
        exit(EXIT_FAILURE);  
    }    
    return 0;   
}

编译运行 

 1.在终端输入如下命令进行编译

gcc processA.c -o a
gcc processB.c -o b

2.运行进程A

./a

运行结果,得到当前进程的进程号 

 3.在新的终端中运行进程B

./b

 

4.在进程A的运行终端输入进程B的进程号后回车

5.在进程B的运行终端输入进程A的进程号后回车

2.消息队列

 消息队列是消息的链表,存放在内核中并由消息队列标识符标识。它克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点,支持异步通信、多对多通信和数据持久化等。

原型

int msgget(key_t key, int msgflg);
    功能:创建\获取消息队列
    key:IPC键值
    msgflg:
        IPC_CREAT  消息队列已存在则获取,否则创建
        IPC_EXCL   消息队列已存在则返回错误
        注意:如果创建需要提供权限
    返回值:IPC标识符,失败-1

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    功能:向消息队列发送消息包
    msqid:IPC标识符
    msgp:要发送的消息包的首地址
        struct msgbuf {
           long mtype;      //  消息类型
           char mtext[n];   //  数据
           ...
        };
    msgsz:数据的字节数,不包含消息类型
    msgflg:
        阻塞发送一般给0
        IPC_NOWAIT 当消息队列满,不等待立即返回
    返回值:成功返回0,失败返回-1

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
    功能:从消息队列中接收对应消息包的数据
    msqid:IPC标识符
    msgp:存储消息包的内存首地址
    msgsz:存储数据的内存字节数(尽量大些)
    msgtyp:消息类型(按照类型获取,不按照顺序)
        >0 读取消息类型=msgtyp的消息
        =0 读取消息队列中第一条消息
        <0 读取消息类型小于abs(msgtyp)的消息,如果有多个则读值最小的
    msgflg
        IPC_NOWAIT 消息队列都不符合时不阻塞,立即返回
        MSG_EXCEPT 如果msgtyp>0,则读取第一条不等于msgtyp的消息
        MSG_NOERROR 如果不包含此标志,如果实际发送过来的数据字节数>接收的字节数,则返回失败,如果包含此标志,那么就只读取接收的字节数,一定会成功
    返回值:成功读取到数据的字节数

int msgctl(int msqid,int cmd,struct msqid_ds *buf);
    功能:获取\修改消息队列的属性、删除队列
    msqid:IPC标识符
    cmd:
        IPC_STAT   获取消息队列属性 buf输出型参数
        IPC_SET    设置消息队列属性 buf输入型参数
        IPC_RMID   删除消息队列     NULL

 编程模型

进程A的代码如下(示例): 

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include "message.h"

int main(int argc , const char* argv[])
{
	// 创建消息队列
	int msgid = msgget(ftok(".", 120), IPC_CREAT | 0664);		
	if(msgid < 0)
	{
		perror("msgget");	
	}

	Msg msg = {};

	while(1)
	{
		msg.type = 1;
		printf(">>>");
		scanf("%s", msg.data);

		// 发送消息类型为1的消息
		if(msgsnd(msgid, &msg, strlen(msg.data), IPC_NOWAIT) == -1)
		{
			perror("msgsnd");	
			return -1;
		}

		if(strcmp("quit", msg.data) == 0)
		{
			break;
		}

		// 获取消息类型为2的消息
		if(msgrcv(msgid, &msg, MSGMAX, 2, 0) <= 0)
		{
			perror("msgrcv");	
			break;
		}

		printf("recv:%s\n", msg.data);
		if(strcmp("quit", msg.data) == 0) break;
	}

	printf("通讯结束!\n");
	usleep(1000);
	
	// 删除消息队列
	if(msgctl(msgid, IPC_RMID, NULL))
	{
		perror("msgctl");	
		return -1;
	}

	return 0; 
}

 进程B的代码如下(示例):

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include "message.h"

int main(int argc , const char* argv[])
{
	// 创建消息队列
	int msgid = msgget(ftok(".", 120), 0);		
	if(msgid < 0)
	{
		perror("msgget");	
	}

	Msg msg = {};

	while(1)
	{
		// 获取消息类型为1的消息 接收失败阻塞
		if(msgrcv(msgid, &msg, MSGMAX, 1, 0) == 0)
		{
			perror("msgrcv");	
			break;
		}
		printf("recv:%s\n", msg.data);
		if(strcmp("quit", msg.data) == 0) break;
		
		msg.type = 2;
		printf(">>>");
		scanf("%s", msg.data);

		// 发送消息类型为2的消息 发送失败不阻塞直接返回
		if(msgsnd(msgid, &msg, strlen(msg.data), IPC_NOWAIT) == -1)
		{
			perror("msgsnd");	
			return -1;
		}

		if(strcmp("quit", msg.data) == 0)
		{
			break;
		}
	}

	printf("通讯结束!\n");
	usleep(1000);

	return 0; 
}

发送信息的结构体类型 (定义在mssage.h中)

#ifndef MESSAGE_H
#define MESSAGE_H

#define MSGMAX 256

typedef struct Msg
{
	long type;      //  消息类型
    char data[MSGMAX];   //  数据 	
} Msg;

#endif// MESSAGE_H

 编译运行

与上述共享内存模型测试相同 ,分别编译生成可执行文件,任何先运行A进程再运行B进程。

3.信号量

 信号量是一个计数器,用于控制多个进程对共享资源的访问。它主要用于进程间同步和互斥,避免死锁,限制资源访问数量,以及实现进程间通知。

基本特点:由内核管理的一个"全局变量",用于记录共享资源的数量,限制进程对共享资源的访问使用
    信号量是一种数据操作锁,本身是不具备数据交互功能,而是通过控制其他的通信资源从而配合实现进程间通信
    1、如果信号量的值大于0,说明可以使用资源,使用时需要信号量-1,然后再使用
    2、如果信号量的值等于0,说明没有资源可使用,此时进程进入休眠,直到信号量的值大于0,进程会被唤醒,执行步骤1
    3、当资源使用完毕,把信号量的值+1,正在休眠的进程就会被唤醒 

原型 

int semget(key_t key, int nsems, int semflg);
    功能:创建\获取信号量
    key:IPC键值
    nsems:信号量的数量 一般写1
    semflg:IPC_CREAT  信号量已存在则获取,否则创建
            IPC_EXCL   信号量已存在则返回错误
            注意:如果创建需要提供权限
    返回值:IPC标识符 失败-1

int semctl(int semid, int semnum, int cmd, ...);
    功能:删除、控制信号量
    semid:IPC标识符
    semnum:要操作的第几个信号量,从0开始,下标
    cmd:
        IPC_STAT   获取信号量属性 buf输出型参数
        IPC_SET    设置信号量属性 buf输入型参数
        IPC_RMID   删除信号量     NULL
        SETVAL     设置某个信号量的值
        SETALL     设置所有信号量的值
        GETVAL     获取某个信号量的值
        GETALL     获取所有信号量的值
        GETNCNT    获取等待拿资源的进程数量

int semop(int semid,struct sembuf *sops,size_t nsops);
    功能:对信号量进行加减操作
    semid:IPC标识符
    sembuf{
        unsigned short sem_num;  // 信号量的下标
        short          sem_op;   //
                1 信号量+1
                -1 信号量-1 如果不能减,则默认阻塞
        short          sem_flg;  //
                IPC_NOWAIT 不阻塞
                SEM_UNDO 如果进程终止没有手动还资源,系统会自动还
    }
    nsops:表示sops指向多少个结构体数量 一般写1

有关POXSI信号量的内容参考下面这篇文章:

http://t.csdnimg.cn/t9LWW


三、网络进程间通讯——(socket套接字) 

有关网络进程通讯参考博主的另外几篇文章 

http://t.csdnimg.cn/LVbMS socket介绍,使用socket在同一计算机中的进程间通信

http://t.csdnimg.cn/McAEq  基于TCP协议的网络通信模型

 http://t.csdnimg.cn/LlSe1 基于UDP通信协议的网络通信编程模型 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值