在日常遇到的.so,.elf 格式的二进制文件,常常可以看到 Linux 进程通信的使用,接下来的部分,我将介绍 Linux 通信的各种实现方式以及相应的代码示例

什么是进程?

进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。可以认为进程是一个程序的一次执行过程。

什么是进程通信?

进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。

什么时候会用到进程通信?

  • 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。

  • 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

  • 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

  • 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

# POSIX 信号量

linux sem 信号量是一种特殊的变量,访问具有原子性, 用于解决进程或线程间共享资源引发的同步问题。

用户态进程对 sem 信号量可以有以下两种操作:

  • 等待信号量
    当信号量值为 0 时,程序等待;当信号量值大于 0 时,信号量减 1,程序继续运行。
  • 发送信号量
    将信号量值加 1

通过对信号量的控制,从而实现共享资源的顺序访问。

使用信号量需要包含头文件 semaphore.h

# 有名信号量函数

有名信号量是通过 sem_open 创建或打开信号量,用 sem_close 关闭信号量,用 sem_unlink 销毁信号量。

# sem_open

创建并初始化或者打开一个已有的有名信号量

/*
@param name: 信号量文件的名字。该名字可以在最前面加 "/",在之后不能再加 "/",但是无论加不加 "/",
指定的文件都是 /dev/shm/sem.xxx。比如传入的 name 为 "mysem",那么指定的是 /dev/shm/sem.mysem
@param oflag: 打开的标志,如果需要打开已经存在的信号量文件,则传入 O_RDWR 即可,不用再传入后两个参数。如果需要创建信号量,则传入 O_CREAT,如果指定的文件不存在,则创建文件,由后两个参数指定文件权限
和信号量的初值。如果指定的文件存在,则打开信号量文件,并忽略后两个参数。如果传入的是 O_CREAT|O_EXCL
那么会检查指定的文件是否存在,如果文件已经存在,那么 sem_open 会返回 - 1。
@param mode: 一般传入 0666 (可读可写)
@param value: 信号量初值
@return: 成功时函数返回有名信号量的地址,失败时返回 SEM_FAILED,其值为 ((void *) 0)。
*/
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);

# sem_close

关闭信号量

该函数应该与 sem_open 成对调用

/*
@param sem: 信号量地址
@return: 成功返回 0,失败返回 - 1
*/
int sem_close(sem_t *sem);

删除信号量文件

POSIX 中的信号量是随内核持续的,如果信号量不 sem_unlink 的话,该命名信号量会常驻在 kernel 之中,即使进程结束了也会存在,而 sem_open 创建信号量时,如果该 named semaphore 存在内核中,设置的初始化参数是无效的

/*
@param name: 信号量文件的名称
@return: 成功返回 0,失败返回 - 1
*/
int sem_unlink(const char *name);

# 无名信号量函数

无名信号量通过 sem_init 进行初始化,使用完之后用 sem_destroy 进行销毁,常用在线程间

# sem_init

该函数初始化由 sem 指向的信号对象,并给它一个初始的整数值 value。

/*
@param sem: 要初始化的目标信号量
@param pshared: 控制信号量的类型,值为 0 代表该信号量用于多线程间的同步,值如果大于 0 表示可以共享,用于多个相关进程间的同步
@return : 当 sem_init () 成功完成初始化操作时,返回值为 0,否则返回 -1
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);

# sem_destroy

该函数用于对用完的信号量的清理

/*
@param sem: 要初始化的目标信号量
@return : 当 sem_destroy 成功完成信号量的清理时,返回值为 0, 否则返回 -1
*/
int sem_destroy(sem_t *sem);

# 信号量通用函数

# sem_wait

sem_wait 是一个阻塞的函数,测试所指定信号量的值,它的操作是原子的。若 sem value > 0,则该信号量值减去 1 并立即返回。若 sem value = 0,则阻塞直到 sem value > 0,此时立即减去 1,然后返回。

/*
@param sem: 要初始化的目标信号量
*/
int sem_wait(sem_t *sem);

sem_trywait 函数是非阻塞的函数,它会尝试获取获取 sem value 值,如果 sem value = 0,不是阻塞住,而是直接返回一个错误 EAGAIN。

/*
@param sem: 要初始化的目标信号量
*/
int sem_trywait(sem_t *sem);

# sem_post

把指定的信号量 sem 的值加 1,唤醒正在等待该信号量的任意线程

/*
@param sem: 要初始化的目标信号量
*/
int sem_post(sem_t *sem);

# sem_getvalue

获取信号量 sem 的当前值,把该值保存在 sval,若有 1 个或者多个线程正在调用 sem_wait 阻塞在该信号量上,该函数返回阻塞在该信号量上进程或线程个数

/*
@param sem: 要初始化的目标信号量
*/
int sem_getvalue(sem_t *sem, int *sval);

# 示例一: 4 个售票员卖 10 张票

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<semaphore.h>
#include<unistd.h>
// 创建信号量
sem_t mySem;
// 设置总票数
int ticket_sum = 10;
// 模拟买票过程
void *sell_ticket(void *arg) {
    printf("当前线程ID:%u\n", pthread_self());
    int i;
    int flag;
    for (i = 0; i < 10; i++)
    {
        // 完成信号量 "减 1" 操作,否则暂停执行
        flag = sem_wait(&mySem);
        if (flag == 0) {
            if (ticket_sum > 0)
            {
                sleep(1);
                printf("%u 卖第 %d 张票\n", pthread_self(), 10 - ticket_sum + 1);
                ticket_sum--;
            }
            // 执行 “加 1” 操作
            sem_post(&mySem);
            sleep(1);
        }
    }
    return 0;
}
int main() {
    int flag;
    int i;
    void *ans;
    // 创建 4 个线程
    pthread_t tids[4];
    // 初始化信号量
    flag = sem_init(&mySem, 0, 1);
    if (flag != 0) {
        printf("初始化信号量失败\n");
    }
    for (i = 0; i < 4; i++)
    {
        flag = pthread_create(&tids[i], NULL, &sell_ticket, NULL);
        if (flag != 0) {
            printf("线程创建失败!");
            return 0;
        }
    }
    sleep(10);
    for (i = 0; i < 4; i++)
    {
        flag = pthread_join(tids[i], &ans);
        if (flag != 0) {
            printf("tid=%d 等待失败!", tids[i]);
            return 0;
        }
    }
    // 执行结束前,销毁信号量
    sem_destroy(&mySem);
    return 0;
}
/*
当前线程 ID:1199965952
当前线程 ID:1189476096
当前线程 ID:1168496384
当前线程 ID:1178986240
1199965952 卖第 1 张票
1189476096 卖第 2 张票
1199965952 卖第 3 张票
1178986240 卖第 4 张票
1168496384 卖第 5 张票
1189476096 卖第 6 张票
1199965952 卖第 7 张票
1178986240 卖第 8 张票
1168496384 卖第 9 张票
1189476096 卖第 10 张票
*/

# 进程间通信的几种方式

# 消息队列

消息队列是消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。消息队列是 UNIX 下不同进程之间可实现共享资源的一种机制,UNIX 允许不同进程将格式化的数据流以消息队列形式发送给任意进程。对消息队列具有操作权限的进程都可以使用 msget 完成对消息队列的操作控制。通过使用消息类型,进程可以按任何顺序读信息,或为消息安排优先级顺序.

# 相关函数

使用消息队列需要包含头文件 sys/msg.h

# msgget

创建消息队列,key 值唯一标识该消息队列

/*
@param key: 消息队列的名称 (非零整数)
@param msgflg: 控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。msgflg 通常取值 0666, msgflg 可以与 IPC_CREAT 做或操作,表示当 key 所命名的消息队列不存在时创建一个消息队列
*/
int msgget(key_t key, int msgflg);

# msgctl

控制消息队列

/*
@param msqid: 由 msgget 函数返回的消息队列标识符。
@param: cmd: 是将要采取的动作,它可以取 3 个值:IPC_STAT, IPC_SET, IPC_RMID
IPC_STAT:把 msqid_ds 结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖 msqid_ds 的值。
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为 msqid_ds 结构中给出的值
IPC_RMID:删除消息队列
@param buf: buf 是指向 msgid_ds 结构的指针,它指向消息队列模式和访问权限的结构。msqid_ds 结构至少包括以下成员:
struct msqid_ds
{
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
};
*/
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

# msgsnd

发送消息

/*
@param msqid: 由 msgget 函数返回的消息队列标识符。
@param msg_ptr: 一个指向准备发送消息的指针,指针 msg_ptr 所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。
@param msg_sz: msg_ptr 指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说 msg_sz 是不包括长整型消息类型成员变量的长度。
@param msgflg: 控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
*/
int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);

# msgrcv

接收消息

/*
@param msqid: 由 msgget 函数返回的消息队列标识符。
@param msg_ptr: 一个指向准备发送消息的指针,指针 msg_ptr 所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。
@param msg_sz: msg_ptr 指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说 msg_sz 是不包括长整型消息类型成员变量的长度。
@param msgtype: msgtype 可以实现一种简单的接收优先级。如果 msgtype 为 0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于 msgtype 的绝对值的第一个消息。
@param msgflg: 控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
*/
int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);

# 示例:利用 Linux 的消息队列通信机制实现两个线程间的通信

编写程序创建三个线程:sender1 线程、sender2 线程和 receive 线程,三个线程的功能。描述如下:

  • sender1 线程:运行函数 sender1 (),它创建一个消息队列,然后等待用户通过终端输入一串字符,并将这串字符通过消息队列发送给 receiver 线程;可循环发送多个消息,直到用户输入 “exit” 为止,表示它不再发送消息,最后向 receiver 线程发送消息 “end1”,并且等待 receiver 的应答,等到应答消息后,将接收到的应答信息显示在终端屏幕上,结束线程的运行。
  • sender2 线程:运行函数 sender2 (),共享 sender1 创建的消息队列,等待用户通过终端输入一串字符,并将这串字符通过消息队列发送给 receiver 线程;可循环发送多个消息, 直到用户输入 “exit” 为止,表示它不再发送消息,最后向 receiver 线程发送消息 “end2”, 并且等待 receiver 的应答,等到应答消息后,将接收到的应答信息显示在终端屏幕上,结束 线程的运行。
  • receiver 线程:运行函数 receive (),它通过消息队列接收来自 sender1 和 sender2 两 个线程的消息,将消息显示在终端屏幕上,当收到内容为 “end1” 的消息时,就向 sender1 发送一个应答消息 “over1”; 当收到内容为 “end2” 的消息时,就向 sender2 发送一个应答消息 “over2”;消息接收完成后删除消息队列,结束线程的运行。选择合适的信号量机制实现三个线程之间的同步与互斥。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <semaphore.h>
#define MAX_MSG_SIZE 256
#define SENDER1_MSG_TYPE 1
#define SENDER2_MSG_TYPE 2
#define RECEIVER_MSG_TYPE 3
struct message {
    long mtype;
    char mtext[MAX_MSG_SIZE];
};
int msgQueueID;
/*
@signal send: 对线程 sender1 和 sender2 上锁,两个线程是互斥的
@signal receive: 对 sender 发送消息给 receiver 的过程上锁,防止 receiver 完成接受消息的动作之前,sender 开始下一轮接受消息
@signal ifexit: 当 sender1 发送 end1 消息给 receiver,receiver 将会发送 over1 给 sender1, 但是 over1 消息有概率会被 receiver 自身接受
*/
sem_t send,receive,ifexit;
void* sender1() {
    struct message msg;
    int running = 1;
	sem_wait(&send);
    while (running) {
    	sem_wait(&receive);
        printf("Sender1: Enter a message (or 'exit' to quit): ");
        fgets(msg.mtext, MAX_MSG_SIZE, stdin);
        msg.mtext[strcspn(msg.mtext, "\n")] = '\0';
        msg.mtype = SENDER1_MSG_TYPE;
        msgsnd(msgQueueID, &msg, strlen(msg.mtext) + 1, 0);
        if (strcmp(msg.mtext, "exit") == 0) {
            running = 0;
        }
    }
	sem_wait(&receive);// 这行代码的作用是防止信号量 receive 被赋值为 2, 同时也确保了消息发送和接收的正确进行
    strcpy(msg.mtext, "end1");
    msg.mtype = SENDER1_MSG_TYPE;
    msgsnd(msgQueueID, &msg, strlen(msg.mtext) + 1, 0);
	msgrcv(msgQueueID, &msg, MAX_MSG_SIZE, 0, 0);
	if(strcmp(msg.mtext, "over1") == 0){
        printf("Sender1: Exiting.\n");
	}
	sem_post(&ifexit);// 通知 receiver: sender1 准备退出
	sem_post(&send);// 通知 sender2: 可以准备接收消息
    pthread_exit(NULL);
}
void* sender2() {
    struct message msg;
    int running = 1;
	sem_wait(&send);
    while (running) {
    	sem_wait(&receive);
        printf("Sender2: Enter a message (or 'exit' to quit): ");
        fgets(msg.mtext, MAX_MSG_SIZE, stdin);
        msg.mtext[strcspn(msg.mtext, "\n")] = '\0';
        msg.mtype = SENDER2_MSG_TYPE;
        msgsnd(msgQueueID, &msg, strlen(msg.mtext) + 1, 0);
        if (strcmp(msg.mtext, "exit") == 0) {
            running = 0;
        }
    }
	sem_wait(&receive);// 这行代码的作用是防止信号量 receive 被赋值为 2, 同时也确保了消息发送和接收的正确进行
    strcpy(msg.mtext, "end2");
    msg.mtype = SENDER2_MSG_TYPE;
    msgsnd(msgQueueID, &msg, strlen(msg.mtext) + 1, 0);
	msgrcv(msgQueueID, &msg, MAX_MSG_SIZE, 0, 0);
	if(strcmp(msg.mtext, "over2") == 0){
    printf("Sender2: Exiting.\n");
	}
	sem_post(&ifexit);
	sem_post(&send);
    pthread_exit(NULL);
}
void* receiver() {
    struct message msg;
    int running = 2;
    while (running) {
        msgrcv(msgQueueID, &msg, MAX_MSG_SIZE, 0, 0);
		if(msg.mtype == SENDER1_MSG_TYPE){
			if (strcmp(msg.mtext, "end1") == 0) {
				strcpy(msg.mtext, "over1");
            	msg.mtype = RECEIVER_MSG_TYPE;
            	msgsnd(msgQueueID, &msg, strlen(msg.mtext) + 1, 0);
            	running-=1;
            	sem_wait(&ifexit);// 等待退出动作完成
			}
			else{
				printf("Receiver: Message received: %s from sender1\n", msg.mtext);
			}
			
		}
		else if(msg.mtype == SENDER2_MSG_TYPE){
			if (strcmp(msg.mtext, "end2") == 0) {
				strcpy(msg.mtext, "over2");
            	msg.mtype = RECEIVER_MSG_TYPE;
            	msgsnd(msgQueueID, &msg, strlen(msg.mtext) + 1, 0);
            	running-=1;
            	sem_wait(&ifexit);// 等待退出动作完成
			}
			else{
				printf("Receiver: Message received: %s from sender2\n", msg.mtext);
			}
			
		}
		sem_post(&receive);
    }
    msgctl(msgQueueID, IPC_RMID, NULL);
    printf("Receiver: Exiting.\n");
    pthread_exit(NULL);
}
int main() {
    pthread_t sender1Thread, sender2Thread, receiverThread;
	
    key_t key = 5555;
    msgQueueID = msgget(key, IPC_CREAT | 0666);
    if (msgQueueID == -1) {
        perror("Failed to create message queue");
        exit(1);
    }
	sem_init(&send,1,1);
	sem_init(&receive,1,1);
    sem_init(&ifexit,1,0);
    pthread_create(&sender1Thread, NULL, sender1, NULL);
    pthread_create(&sender2Thread, NULL, sender2, NULL);
    pthread_create(&receiverThread, NULL, receiver, NULL);
    pthread_join(sender1Thread, NULL);
    pthread_join(sender2Thread, NULL);
    pthread_join(receiverThread, NULL);
    return 0;
}

# 共享内存

共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以我们通常需要用其他的机制来同步对共享内存的访问,例如前面说到的信号量

使用共享内存需要包含头文件 sys/shm.h

# 相关函数

# shmget

创建共享内存

/*
@param key: 为共享内存段命名
@param size: 以字节为单位指定需要共享的内存容量
@param shmflg: 控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。shmflg 通常取值 0666, shmflg 可以与 IPC_CREAT 做或操作,表示当 key 所命名的共享内存不存在时创建一个共享内存
@return: 成功时返回一个与 key 相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回 - 1
*/
int shmget(key_t key, size_t size, int shmflg);

# shmat

第一次创建完共享内存时,它还不能被任何进程访问,shmat 函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间

/*
@param shm_id: 由 shmget 函数返回的共享内存标识。
@param shm_addr: 指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
@param shm_flg: 标志位,通常为 0。
@return: 调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回 - 1.
*/
void *shmat(int shm_id, const void *shm_addr, int shmflg);

# shmdt

该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用

/*
@param shmaddr: shmat 函数返回的地址指针
@return: 调用成功时返回 0,失败时返回 - 1
*/
int shmdt(const void *shmaddr);

# shmctl

类比信号量中的 semctl 函数,用来控制共享内存

/*
@param shm_id: 由 shmget 函数返回的共享内存标识。
@param command: 要采取的操作,它可以取下面的三个值 :
IPC_STAT:把 shmid_ds 结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖 shmid_ds 的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为 shmid_ds 结构中给出的值
IPC_RMID:删除共享内存段
@param buf: buf 是一个结构指针,它指向共享内存模式和访问权限的结构。
shmid_ds 结构至少包括以下成员:
struct shmid_ds
{
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
};
*/
int shmctl(int shm_id, int command, struct shmid_ds *buf);

# 示例:利用 Linux 的共享内存通信机制实现两个进程间的通信

  • 编写程序 sender,它创建一个共享内存,然后随机产生一个 100 以内的计算表达式(例如 12+34),并将这串表达式字符串通过共享内存发送给 receiver;最后,receiver 完成表达式运算后,将计算结果 (36) 写到共享内存 ,sender 收到应答消息后,将接收到的计算结果显示在终端屏幕上。上述计算重复 10 次后,sender 向 receiver 发送”end”,等待 recever 发送”over” 信息后,删除共享内存,结束程序的运行。
  • 编写程序 receiver, 它通过共享内存接收来自 sender 产生的信息,如果该信息是计算表达式,则将表达式显示在终端屏幕上,然后计算表达式的结果,再通过该共享内存向 sender 发送计算结果,等待接收下一个消息;如果该信息是”end”,则向 sender 发送一个应答消息”over”,并结束程序的运行。选择合适的信号量机制实现两个进程对共享内存的互斥及同步使用。
//common.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <time.h>
#define TEXT_SZ 100
/*
MSG1 表示 sender 为写端,receiver 为读端
MSG2 表示 sender 为读端,receiver 为写端
*/
#define MSG1_SEMRD "msg1_sem_read"
#define MSG1_SEMWR "msg1_sem_write"
#define MSG2_SEMRD "msg2_sem_read"
#define MSG2_SEMWR "msg2_sem_write"
struct shm_data
{
	char text[TEXT_SZ];
};
//sender.c
#include"common.h"
int main() {
    struct shm_data *shared;
    // 创建共享内存
    int shm_id = shmget((key_t)1234, sizeof(struct shm_data), 0666|IPC_CREAT);
    if (shm_id == -1) {
        perror("Failed to get shared memory");
        exit(1);
    }
	// 将共享内存连接到当前进程的地址空间
    char *shm = (char *)shmat(shm_id, NULL, 0);
    if (shm == (char *)(-1)) {
        perror("Failed to attach shared memory");
        exit(1);
    }
    // 设置共享内存
	shared = (struct shm_data*)shm;
    // 设置信号量
    sem_t *msg1_semwr,*msg1_semrd,*msg2_semwr,*msg2_semrd;
    msg1_semwr=sem_open(MSG1_SEMWR,O_CREAT,0666,1);
    msg1_semrd=sem_open(MSG1_SEMRD,O_CREAT,0666,0);
    msg2_semwr=sem_open(MSG2_SEMWR,O_CREAT,0666,1);
    msg2_semrd=sem_open(MSG2_SEMRD,O_CREAT,0666,0);
    if(msg1_semwr==(void*)-1 || msg1_semrd==(void*)-1 ||msg2_semwr==(void*)-1 || msg2_semrd==(void*)-1){
        perror("sem_open failure");
    }
    // 随机产生并发送表达式
    srand(time(NULL));
    for (int i = 0; i < 10; i++) {
        sem_wait(msg1_semwr);
        int num1 = rand() % 100;
        int num2 = rand() % 100;
        sprintf(shared->text, "%d+%d", num1, num2);
        //printf("Sender: Sent expression '%s'\n", expression);
        sem_post(msg1_semrd);
        sem_wait(msg2_semrd);
        printf("Sender: received calculation results %s from Receiver\n",shared->text);
        sem_post(msg2_semwr);
        sleep(1);
    }
    // 发送结束信号
    sem_wait(msg1_semwr);
    strcpy(shared->text, "end");
    sem_post(msg1_semrd);
    // 等待接收 'over' 信号并删除共享内存
    sem_wait(msg2_semrd);
    printf("Sender: received %s from Receiver\n",shared->text);
    // 把共享内存从当前进程中分离
    if(shmdt(shm) == -1)
    {
        perror("shmdt failed\n");
        exit(EXIT_FAILURE);
    }
    // 删除共享内存
    if(shmctl(shm_id, IPC_RMID, 0) == -1)
    {
        perror("shmctl(IPC_RMID) failed\n");
    }
    sem_post(msg2_semwr);
    // 关闭信号量
    sem_close(msg1_semwr);
    sem_close(msg1_semrd);
    sem_close(msg2_semwr);
    sem_close(msg2_semrd);
    // 删除信号量文件
    sem_unlink(MSG1_SEMRD);
    sem_unlink(MSG1_SEMWR);
    sem_unlink(MSG2_SEMRD);
    sem_unlink(MSG2_SEMWR);
    return 0;
}
//receiver.c
#include"common.h"
int main() {
    struct shm_data *shared;
    // 创建共享内存
    int shm_id = shmget((key_t)1234, sizeof(struct shm_data), 0666|IPC_CREAT);
    if (shm_id == -1) {
        perror("Failed to get shared memory");
        exit(1);
    }
	// 将共享内存连接到当前进程的地址空间
    char *shm = (char *)shmat(shm_id, NULL, 0);
    if (shm == (char *)(-1)) {
        perror("Failed to attach shared memory");
        exit(1);
    }
    // 设置共享内存
	shared = (struct shm_data*)shm;
    // 设置信号量
    sem_t *msg1_semwr,*msg1_semrd,*msg2_semwr,*msg2_semrd;
    msg1_semwr=sem_open(MSG1_SEMWR,O_CREAT,0666,1);
    msg1_semrd=sem_open(MSG1_SEMRD,O_CREAT,0666,0);
    msg2_semwr=sem_open(MSG2_SEMWR,O_CREAT,0666,1);
    msg2_semrd=sem_open(MSG2_SEMRD,O_CREAT,0666,0);
    if(msg1_semwr==(void*)-1 || msg1_semrd==(void*)-1 ||msg2_semwr==(void*)-1 || msg2_semrd==(void*)-1){
        perror("sem_open failure");
    }
    int running = 1;
    // 接收并处理消息
    while (running) {
        sem_wait(msg1_semrd);
        char expression[100];
        strcpy(expression,shared->text);
        if (strcmp(expression, "end") == 0) {
            printf("Receiver: Received end from Sender\n");
            sem_post(msg1_semwr);
            sem_wait(msg2_semwr);
            strcpy(shared->text, "over");
            running=0;
            sem_post(msg2_semrd);
        }
        else{
            printf("Receiver: Received expression '%s' from Sender\n", expression);
            sem_post(msg1_semwr);
            // 计算表达式结果
            sem_wait(msg2_semwr);
            int num1, num2;
            sscanf(expression, "%d+%d", &num1, &num2);
            int result = num1 + num2;
            sprintf(shared->text, "%d", result);
            sem_post(msg2_semrd);
        }
        
    }
    // 关闭信号量
    sem_close(msg1_semwr);
    sem_close(msg1_semrd);
    sem_close(msg2_semwr);
    sem_close(msg2_semrd);
    // 删除信号量文件
    sem_unlink(MSG1_SEMRD);
    sem_unlink(MSG1_SEMWR);
    sem_unlink(MSG2_SEMRD);
    sem_unlink(MSG2_SEMWR);
    return 0;
}

# 管道

操作系统在内核中为进程创建的的一块缓冲区,若多个进程可以访问到同一块缓冲区,就可以实现进程间通信,通过半双工通信(可以选择方向的单向通信)实现数据传输

# 匿名管道

在这块内核中的缓冲区没有明确的标识符,其他进程无法直接访问管道

使用匿名管道需要包含头文件 unistd.h

特性

  • 匿名管道只能用于具有亲缘关系的进程间通信
  • 匿名管道创建时,操作系统会提供两个操作句柄(文件描述符)(其中一个用于从操作管道读取数据,一个向管道中写入数据)
  • 因此只能通过创建子进程,子进程通过复制父进程的方式,获取到管道的操作句柄,进而实现访问同一个管道通信

image-20230529111643432

# pipe

创建匿名管道

/*
@param fd [2]: 文件描述符数组,其中 fd [0] 表示读端,fd [1] 表示写端 
@return: 成功返回 0,失败返回 - 1
*/
int pipe(int fd[2]);
  • 管道要不然只能读,要不然只能写(单向传输);使用的时候如果不使用哪一端,关闭哪一端就可以

  • 若管道中没有数据,则 read 就会阻塞;若管道中数据写满了则 write 就会阻塞

# 示例

从键盘读取数据,写入管道,读取管道,写到屏幕

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h>
int main()
{
    int fds[2];
    char buf[100];
    int len;
    
	if ( pipe(fds) == -1 ) 
		perror("make pipe error"),exit(1);
		
    // read from stdin
    while ( fgets(buf, 100, stdin) ) {
        len = strlen(buf);
        // write into pipe
        if ( write(fds[1], buf, len) != len ) {
            perror("write to pipe error");
			break; 
		}
        memset(buf, 0x00, sizeof(buf));
        
        // read from pipe
		if ( (len=read(fds[0], buf, 100)) == -1 ) { 
			perror("read from pipe error");
			break;
		}
		
        // write to stdout
        if ( write(1, buf, len) != len ) {
            perror("write to stdout error");
            break;
		} 	
	}
	return 0;
}

# 命名管道

命名管道是内核中的缓冲区,这块缓冲区具有标识符(标识符是一个可见于文件系统的管道文件),其他的进程可以通过这个标识符,找到这个缓冲区(通过打开一个管道文件,进而访问到同一块缓冲区),进而实现通信

使用命名管道需要包含头文件 sys/types.h , sys/stat.h

特性

  • 可用于同一主机上的任意进程间通信
  • 多个进程通过命名管道通信时通过打开命令管道文件访问同一块内核中的缓冲区实现通信

# mkfifo

创建命名管道

mkfifo 函数默认指定 O_CREAT | O_EXECL 方式创建 FIFO, mkfifo 的一般使用方式是:通过 mkfifo 创建 FIFO,然后调用 open,以读或者写的方式之一打开 FIFO,然后进行数据通信。

/*
@param filename: 管道文件名称
@param mode: 指定的文件权限位,类似于 open 函数的第三个参数。即创建该 FIFO 时,指定用户的访问权限,有以下值:S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH,S_IWOTH。
@return: 成功返回 0,失败返回 - 1
*/
int mkfifo(const char *filename, mode_t mode);

# 示例

// FIFOwrite.c
#include <stdio.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
  
int main(int argc, char *argv[])  
{  
    int fd;  
    int ret;      
    ret = mkfifo("my_fifo", 0666); // 创建命名管道  
    if(ret != 0)  
    {
        perror("mkfifo");  
    } 
    fd = open("my_fifo", O_WRONLY); // 等着只读  
    if(fd < 0)  
    {  
        perror("open fifo");  
    }  
    char send[100] = "Hello World";  
    write(fd, send, strlen(send));  // 写数据  
    printf("write to my_fifo buf=%s\n",send);  
    while(1); // 阻塞,保证读写进程保持着通信过程
    close(fd);
    return 0;  
}
// FIFOread.c
#include <stdio.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
  
int main(int argc, char *argv[])  
{  
    int fd;  
    int ret;      
    ret = mkfifo("my_fifo", 0666); // 创建命名管道  
    if(ret != 0)  
    {  
        perror("mkfifo");  
    }  
    fd = open("my_fifo", O_RDONLY); // 等着只写  
    if(fd < 0)  
    {  
        perror("open fifo");  
    }  
    while(1)  
    {  
        char recv[100] = {0};
        read(fd, recv, sizeof(recv)); // 读数据  
        printf("read from my_fifo buf=[%s]\n", recv);  
        sleep(1);  
    }
    close(fd);
    return 0;  
}

# 参考资料

  • 6 种 Linux 进程间的通信方式
  • Linux 进程间通信 —— 使用消息队列
  • linux 多线程之信号量 sem_init
  • Linux 信号量详解
  • 进程间通信 —POSIX 信号量实现机制
  • 有名信号量 —— 无关进程间同步
  • 利用信号量 semaphore 实现两个进程读写同步
  • Linux 进程间通信 —— 使用共享内存
  • Linux:带你理解进程间通信–管道
  • 进程间通信之管道(pipe)和命名管道(FIFO)
更新于 阅读次数