Linux进程间通信(超详细介绍各种IPC方式)

Linux进程间通信

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

一、管道

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

1、特点

  1. **半双工通信:**管道是一种半双工的通信方式(即数据只能在一个方向上流动),具有固定的读端和写端;
  2. 亲缘关系:通常情况下,管道只能在具有亲缘关系的进程之间使用,例如父子进程或兄弟进程;
  3. 字节流:管道中的数据以字节流的形式传输,没有消息边界。写入管道的数据会被缓冲,直到读取端读取为止;
  4. 同步:管道的读取和写入是同步的,即读取操作会阻塞,直到有数据可供读取;写入操作会阻塞,直到有足够的空间可供写入;
  5. 有限容量:管道具有一定的容量限制,如果写入的数据超过了管道的容量,写入操作可能会阻塞或导致错误。
  6. **特殊文件:**它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

2、函数原型

#include <unistd.h>
int pipe(int fd[2]);    
// 返回值:若成功返回0,失败返回-1

当一个管道建立时,它会创建两个文件描述符:fd[0]用于读取管道,fd[1]用于写入管道。如下图:

在这里插入图片描述

3、通信案例

单个进程中的管道几乎没有任何用处。通常,进程会先调用 pipe ,接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。

在这里插入图片描述

fork之后要确定数据流的方向,若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    int fd[2]; // 用于存储管道的文件描述符
    pid_t pid; // 用于存储子进程的 PID
    char *writebuf = "hello chiled,i'm father"; // 要写入的字符串
    char readbuf[128]; // 用于存储读取的数据

    // 创建管道
    if (pipe(fd) == -1) { 
        perror("pipe"); // 如果创建管道失败,输出错误信息
        exit(0); 
    }

    // 创建子进程
    if ((pid = fork()) < 0) { 
        perror("fork"); // 如果创建子进程失败,输出错误信息
        exit(0); 
    }

    // 父进程
    if (pid > 0) { 
        sleep(1); // 休眠一秒,让子进程先运行
        printf("this is father\n"); 
        close(fd[0]); // 关闭读端
        write(fd[1], writebuf, strlen(writebuf)); // 向管道中写入数据
        wait(NULL); // 等待子进程结束
    } else if (pid == 0) { 
        printf("this is chiled\n"); 
        close(fd[1]); // 关闭写端
        read(fd[0], readbuf, sizeof(readbuf)); // 从管道中读取数据
        printf("from father: %s\n", readbuf); // 打印读取到的数据
        exit(0); 
    }
}
/*运行结果:
this is chiled
this is father
from father: hello chiled,i'm father*/

二、FIFO(命名管道)

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

1、特点

  1. 先进先出:数据以先进先出的方式在FIFO中排队,先写入的数据先被读取;
  2. 无缓冲区:FIFO本身不提供缓冲区,操作系统会为每个打开的FIFO文件描述符分配缓冲区;
  3. 文件系统对象:FIFO在文件系统中表现为一个特殊类型的文件,可以被任何有权限的进程访问;
  4. 非阻塞性:默认情况下,FIFO是阻塞的,但可以通过设置使其变为非阻塞;
  5. 进程间通信:FIFO允许不具有亲缘关系的进程之间进行通信;
  6. 数据流:FIFO支持数据流的传输,数据以字节流的形式存在。

2、函数原型

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
// 返回值:成功返回0,出错返回-1
  • pathname:指定要创建的 FIFO 的路径名。这是一个字符串,包含了 FIFO 的名称和路径。
  • mode: 指定 FIFO 的权限模式,是一个八进制数,类似于文件的权限设置。参数与open函数中的 mode 相同,一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:
    • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
    • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

3、创建FIFO

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

int main()
{
    int fd;
    fd = mkfifo("./file",0666);
    if(fd == -1){
        perror("why");
    }  
    close(fd);

    //unlink("./file");//删除fifo文件     
    return 0;
}

运行结果:创建了一个名为file的特殊格式文件。

在这里插入图片描述

4、编程实现FIFO数据通信

  • fifo_write:写数据
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    int fd; // 文件描述符
    char *writeBuf = "fifo write success"; // 要写入的字符串

    // 以只写方式打开文件
    fd = open("./file", O_WRONLY); 
    if (fd == -1) { // 如果打开失败
        perror("open"); // 输出错误信息
    }
    printf("open success\n"); // 打印打开成功的信息

    // 将字符串写入文件
    write(fd, writeBuf, strlen(writeBuf)); 

    close(fd); // 关闭文件描述符

    return 0;
}
  • fifo_read:读数据
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

int main()
{
    int ret; // 用于存储函数返回值
    int fd; // 文件描述符
    int n_read; // 读取的字节数
    char readBuf[128] = {'\0'}; // 用于存储读取的数据

    // 创建命名管道
    ret = mkfifo("./file", 0600); 
    if ((ret == -1) && (errno!= EEXIST)) { // 如果创建失败且错误码不是 EEXIST(文件已存在)
        perror("mkfifo"); // 输出错误信息
    }
    printf("fifo ok\n"); // 打印创建成功的信息

    // 以只读方式打开命名管道
    if ((fd = open("./file", O_RDONLY)) < 0) { 
        perror("open"); // 输出打开文件失败的错误信息
    }
    printf("open ok\n"); // 打印打开文件成功的信息

    // 从命名管道中读取数据
    n_read = read(fd, readBuf, sizeof(readBuf)); 
    printf("read %d byte, content: %s\n", n_read, readBuf); // 打印读取的字节数和内容

    close(fd); // 关闭文件描述符

    unlink("./file"); // 删除命名管道
    return 0;
}

运行结果:默认情况下创建FIFO后的mode是阻塞状态,所以先运行只读程序会阻塞在"fifo ok"等待写入数据,等到执行写入程序后只读程序会执行完毕,读到写入程序的写入内容。
在这里插入图片描述

三、消息队列

  • 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识;
  • 消息被发送到队列中。“消息队列”是在消息的传输过程中保存消息的容器。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。

1、特点

  1. 异步性:发送和接收消息是异步进行的;
  2. 持久性:消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
  3. 消息顺序:在同一个消息类型中,消息队列通常保证消息的顺序,即先发送的消息先被接收。在不同的类型或优先级中,允许接收者根据类型或优先级处理消息;
  4. 多生产者和多消费者:多个进程可以同时向同一个消息队列发送或接收消息;
  5. 解耦生产者和消费者:消息队列允许生产者和消费者在不同的时间点独立工作,从而降低了系统组件之间的耦合度。

2、相关API介绍

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

key_t ftok(const char *pathname, int proj_id);
// 通常用于生成消息队列、信号量或共享内存的键值:成功返回生成的键值,失败返回-1

int msgget(key_t key, int flag);
// 创建或打开消息队列:成功返回队列ID,失败返回-1

int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 添加消息:成功返回0,失败返回-1

int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// 控制消息队列:成功返回0,失败返回-1
(1)ftok

ftok函数用于将一个路径名和一个项目标识符转换为一个键值,通常用于生成消息队列、信号量或共享内存的键。

#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
//返回值:成功返回生成的键值,失败返回-1,并设置error错误码
/*在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算
成16进制为0x26,则最后的key_t返回值为0x26010002。*/

参数说明:

  • pathname:指向文件路径的指针,这个文件必须是已经存在且可以访问的。
    • 一般使用当前目录,如key = ftok(“.”,1),"."代表当前目录;
    • ls -i:可以查找文件的索引节点号。
  • proj_id:一个 8 bit的整数,虽然是int类型,不过值的范围通常是 1 到 255。
(2)msgget

msgget函数用于获取一个已存在的消息队列的标识符,或者在 IPC_CREAT 标志被设置的情况下创建一个新的消息队列。通常,msgget函数与其他消息队列操作函数(如msgsndmsgrcv)一起使用,用于实现进程间通信。

#include <sys/msg.h>

int msgget(key_t key, int flag);
// 返回值:成功返回队列ID,失败返回-1,并设置error错误码

参数说明:

  • key:用于指定消息队列的唯一标识符。如果使用 IPC_PRIVATE 作为 key,将创建一个私有的消息队列,只对当前进程可见。
  • flag:标志位,用于指定消息队列的访问权限和操作方式。常见的标志位有:
    • IPC_CREAT:如果消息队列不存在,则创建它。
    • IPC_EXCL:与IPC_CREAT一起使用,如果消息队列已经存在,则返回错误。
    • IPC_NOWAIT:如果消息队列已满或为空,立即返回错误,而不是阻塞等待。
(3)msgsend

msgsend函数用于将消息发送到指定的消息队列中。

#include <sys/msg.h>

int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 返回值:成功返回0,失败返回-1,并设置error错误码

参数说明:

  • msqid:消息队列的标识符,通过msgget函数获取。

  • ptr:指向要发送的消息的指针。消息通常是一个结构体,包含一个 long mtype 类型的消息类型字段和一个 char mtext[1] 类型的字符数组,用于存储消息的实际内容。

  • size:消息的大小,不包括消息类型的长度。

  • flag:标志位,用于指定发送消息的方式。常见的标志位有:

    • 0:正常发送消息;

    • IPC_NOWAIT:如果消息队列已满,立即返回错误,而不是阻塞等待。

(4)msgrcv

msgrcv函数用于从消息队列中接收消息。

#include <sys/msg.h>

int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 返回值:成功返回消息数据的长度,失败返回-1,并设置error错误码

参数说明:

  • msqid:消息队列的标识符,通过msgget函数获取。
  • ptr::指向用于存储接收消息的缓冲区的指针。
  • size:要接收的消息的大小(不包括消息类型的长度)。
  • type:指定要接收的消息类型。type值非 0 时用于以非先进先出次序读消息,也可以把 type 看做优先级的权值。可以是以下几种情况之一:
    • type == 0:接收队列任意类型的第一条消息;
    • type > 0:接收队列中消息类型为 type 的第一个消息;
    • type < 0:接收队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
  • flag:标志位,用于指定接收消息的方式。常见的标志位有:
    • IPC_NOWAIT:如果没有满足条件的消息,立即返回错误,而不是阻塞等待。
    • MSG_NOERROR:如果接收到的消息长度大于msgsz,则截断消息。
(5)msgctl

msgctl函数用于对消息队列进行控制操作,例如获取消息队列的信息、删除消息队列等。

#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//返回值:成功返回0,失败返回-1,并设置error错误码

参数说明:

  • msqid:消息队列的标识符,通过msgget函数获取。
  • cmd:控制命令,可以是以下几种值之一:常用IPC_RMID
    • IPC_STAT:获取消息队列的信息,并将其存储在buf指向的结构体中。
    • IPC_SET:设置消息队列的属性,使用buf指向的结构体中的值。
    • IPC_RMID:删除消息队列。
  • buf:指向一个msqid_ds结构体的指针,用于存储或设置消息队列的信息,通常设置为NULL。

3、编程实现消息队列收发消息

  • msg_read:读取消息队列的消息,然后通知发送端已经收到消息
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>

// 定义消息结构体
struct msgbuf {
    long mtype;    // 消息类型
    char mtext[128];  // 消息内容
};

int main() {
    int ret;  // 用于存储函数返回值
    int msgID;  // 消息队列标识符
    key_t key; //键值
    struct msgbuf readBuf;  // 用于存储接收的消息
    struct msgbuf sendBuf = {999, "tankyou for read"};  // 要发送的消息

    // 使用 ftok 函数生成键值
    key = ftok(".",'a');//第二个参数id可以用字符,系统会根据Ascll码转换成相应数字
     if (msgID == -1) {
        perror("ftok");  // 如果键值生成失败,输出错误信息
    }
    printf("key = %x\n", key);  // 输出生成的键值
    
    // 获取消息队列,IPC_CREAT 表示如果不存在则创建,0777 表示权限
    msgID = msgget(0x1234, IPC_CREAT | 0777);
    if (msgID == -1) {
        perror("msgget");  // 如果获取失败,输出错误信息
    }

    // 从消息队列中接收消息,888 为消息类型
    ret = msgrcv(msgID, &readBuf, sizeof(readBuf.mtext), 888, 0);
    if (ret == -1) {
        perror("msgrcv");  // 如果接收失败,输出错误信息
    }
    printf("read from send:%s\n", readBuf.mtext);  // 输出接收到的消息内容
    printf("receve success,return message\n");

    // 发送消息到消息队列
    ret = msgsnd(msgID, &sendBuf, strlen(sendBuf.mtext), 0);
    if (ret == -1) {
        printf("msgsnd");  // 如果发送失败,输出错误信息
    }
    printf("return ok\n");

    return 0;
}
  • msg_send:将消息发送至消息队列,然后接收读取端返回的消息
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>

// 定义消息结构体
struct msgbuf {
    long mtype;    // 消息类型
    char mtext[128];  // 消息内容
};

int main() {
    int ret;  // 用于存储函数返回值
    int msgID;  // 消息队列标识符
    key_t key; //键值
    struct msgbuf readBuf;  // 用于存储接收的消息
    struct msgbuf sendBuf = {888, "this is massage from queue"};  // 要发送的消息

    // 使用 ftok 函数生成键值
    key = ftok(".",97);
     if (msgID == -1) {
        perror("ftok");  // 如果键值生成失败,输出错误信息
    }
    printf("key = %x\n", key);  // 输出生成的键值
    
    
    // 获取消息队列,IPC_CREAT 表示如果不存在则创建,0777 表示权限
    msgID = msgget(0x1234, IPC_CREAT | 0777);
    if (msgID == -1) {
        perror("msgget");  // 如果获取失败,输出错误信息
    }

    // 发送消息到消息队列
    ret = msgsnd(msgID, &sendBuf, strlen(sendBuf.mtext), 0);
    if (ret == -1) {
        perror("msgsnd");  // 如果发送失败,输出错误信息
    }
    printf("send ok\n");

    // 从消息队列中接收消息,999 为消息类型
    ret = msgrcv(msgID, &readBuf, sizeof(readBuf.mtext), 999, 0);
    if (ret == -1) {
        perror("msgsnd");  // 如果接收失败,输出错误信息
    }
    printf("read from return:%s\n", readBuf.mtext);  // 输出接收到的消息内容
    printf("receve success\n");

    return 0;
}

运行结果:

在这里插入图片描述

四、共享内存

在系统内存中开辟一块特定区域,多个进程可以将这块内存区域映射到自己的地址空间中。这样,这些进程就可以直接读写这块共享内存,就好像在操作自己进程内的内存一样,从而实现快速的数据交换,无需进行数据的复制传递。

1、特点

  1. 高效性:由于多个进程可以直接访问共享内存,而不需要进行数据复制或通过内核进行消息传递,因此可以提供较高的数据传输效率;
  2. 快速访问:共享内存的访问速度通常比其他进程间通信方式更快,因为它不需要经过系统调用和内核的干预;
  3. 共享数据:多个进程可以同时访问共享内存中的数据,这使得它们可以方便地进行数据共享和协作;
  4. 进程独立性:每个进程都有自己的地址空间,但通过共享内存,它们可以访问同一块内存区域,实现进程间的通信和协作。

在使用共享内存时,需要注意以下几点:

  1. 同步问题:由于多个进程可以同时访问共享内存,可能会出现数据竞争和不一致的情况。因此,需要使用适当的同步机制,如信号量、互斥锁等来确保数据的一致性和正确性;
  2. 内存管理:共享内存需要由操作系统进行管理,包括分配、释放和保护等。进程在使用共享内存时,需要遵循操作系统的相关规定和限制;
  3. 安全性:共享内存可能会受到其他进程的非法访问或修改,因此需要采取适当的安全措施来保护共享内存中的数据。

共享内存通常用于需要高效数据交换和共享的场景,如多进程并发编程、分布式系统等。它是一种强大的进程间通信机制,但在使用时需要谨慎处理同步和安全问题,以确保程序的正确性和稳定性。

2、共享内存的实现步骤

  1. 创建共享内存段:使用如shmget之类的系统调用来创建一个新的共享内存段。
  2. 附加共享内存:使用 shmat 将共享内存段附加到调用进程的地址空间。
  3. 同步访问:使用信号量(semaphores)或其他同步机制来控制对共享内存的访问。
  4. 访问共享内存:进程可以直接读写附加到地址空间的共享内存区域。
  5. 分离共享内存:使用 shmdt 将共享内存段从进程的地址空间分离。
  6. 删除共享内存:使用 shmctl与适当的命令删除共享内存段,释放资源。

3、相关API介绍

指令查询/删除共享内存:

  • ipcs -m 查看共享内存
  • ipcrm -m + shmid 删除共享内存
#include <sys/sem.h>
#include <sys/ipc.h>

int shmget(key_t key, size_t size, int shmflg);
//创建或获取一个共享内存:成功返回共享内存ID,失败返回-1

void *shmat(int shmid, const void *shmaddr, int shmflg);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1

int shmdt(const void *shmaddr);
// 断开与共享内存的连接:成功返回0,失败返回-1

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 控制共享内存的相关信息:成功返回0,失败返回-1
(1)shmget

shmget函数用来创建一个新的共享内存段或获取一个已有共享内存段的标识符。如果共享内存段不存在,shmget会创建一个新的,并初始化其大小和权限。

int shmget(key_t key, size_t size, int shmflg);
//返回值:成功返回共享内存段的标识符,失败返回-1,并设置error错误代码

参数说明:

  • key:共享内存段的键值,用于标识共享内存段。可以通过 ftok 函数或其他方式生成。
  • size:共享内存段的大小,以字节为单位。
  • shmflg:标志位,用于指定创建或获取共享内存段的方式和权限。常见的标志位如下:
    • IPC_CREAT:如果指定此标志,且 key 已存在,则创建一个新的共享内存段;
    • IPC_EXCL:与 IPC_CREAT 一起使用,如果共享内存已存在,则 shmget 调用失败;
    • 权限掩码(如 0666):设置共享内存的访问权限。
(2)shmat

shmat函数用于将一个共享内存段附加(映射)到调用进程的地址空间中,使得进程可以访问在该地址空间中的共享内存。请注意:子进程继承其父进程附加的共享内存段。如果子进程不再需要访问共享内存,可以使用 shmdt 来分离。

void *shmat(int shmid, const void *shmaddr, int shmflg);
//返回值:成功返回附加到的共享内存地址的指针,失败返回 (void *)-1,并设置error错误代码

参数说明:

  • shmid:由 shmget 函数返回的共享内存段的标识符。
  • shmaddr:指向一个缓冲区,该缓冲区将被用来存放共享内存段在调用进程地址空间中的地址。如果设置为NULL或者0,表示让系统选择一个合适的地址。
  • shmflg:控制共享内存映射的附加标志。可以包括:
    • SHM_RDONLY:以只读方式附加共享内存;
    • 0:以读写方式附加共享内存。
(3)shmdt

shmdt函数用于将之前通过 shmat 附加到进程地址空间的共享内存段分离(detach)或解除映射。

int shmdt(const void *shmaddr);
//返回值:成功返回0,失败返回-1,并设置error错误代码

参数说明:

  • shmaddr:共享内存在调用进程地址空间中的地址。这个地址是由之前 shmat 函数调用返回的。

shmdt函数只是是通知系统,调用进程不再需要访问之前附加的共享内存段。这并不会删除共享内存段,只是撤销了该进程对共享内存段的访问权限。系统会减少共享内存的引用计数。当引用计数到达0,即没有任何进程附加该共享内存时,共享内存段可能会被系统回收。

(4)shmctl

shmctl函数提供了对共享内存段的控制操作,允许用户获取共享内存的状态、设置共享内存的属性或删除共享内存段。

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//返回值:成功返回0,失败返回-1,并设置error错误代码

参数说明:

  • shmid:共享内存段的标识符,由 shmget 函数返回。
  • cmd:指定要对共享内存执行的控制命令,可以是以下值之一:
    • IPC_STAT:获取共享内存的状态信息,将共享内存的 shmid_ds 结构体复制到 buf 指向的地址;
    • IPC_SET:设置共享内存的权限、所有者等属性,将 buf 指向的 shmid_ds 结构体中的 uidgidmode 等信息更新到共享内存的控制结构中;
    • IPC_RMID:删除共享内存段,释放所有相关资源。
  • buf:指向 struct shmid_ds 的指针,该结构体用于存储共享内存的状态信息或作为设置共享内存属性的数据。

4、编程实现共享内存

shm_write:写数据端

#include <stdio.h>     
#include <sys/shm.h>    
#include <sys/ipc.h>    
#include <string.h>     
#include <unistd.h>     

int main() {
    int shmid;          // 共享内存标识符
    char *shmaddr;     // 指向共享内存的指针
    
    key_t key;          // 用于创建唯一键的变量
    key = ftok(".", '2'); // 使用当前目录和字符'2'作为项目标识符创建键

	// 创建或获取共享内存,大小为4096字节,设置权限
    shmid = shmget(key, 1024 * 4, IPC_CREAT|0666); 
    
    if (shmid == -1) {
        perror("shmget"); // 如果shmget失败,打印错误信息
    }

    shmaddr = shmat(shmid, NULL, 0); // 将共享内存附加到当前进程的地址空间
    if (shmaddr == (void *)-1) {
        perror("shmat"); // 如果shmat失败,打印错误信息
    }
    printf("shmat ok\n"); // 打印附加成功信息

    strcpy(shmaddr, "hello nice meet to you"); // 向共享内存中写入字符串
    sleep(5); // 睡眠5秒,等待其他进程操作共享内存

    shmdt(shmaddr); // 从当前进程的地址空间分离共享内存

    shmctl(shmid, IPC_RMID, 0); // 删除共享内存段,释放资源
    printf("quit\n"); // 打印退出信息

    return 0; // 正常退出程序
}

shm_read:读数据端

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

int main() {
    int ret;            // 用于存储返回值的变量
    int shmid;          // 共享内存标识符
    char *shmaddr;     // 指向共享内存的指针

    key_t key;          // 用于创建唯一键的变量
    key = ftok(".", '2'); // 使用当前目录和字符'2'作为项目标识符创建键

    shmid = shmget(key, 1024 * 4, 0); // 创建或获取共享内存标识符,大小为4096字节
    if (shmid == -1) {
        perror("shmget"); // 如果shmget失败,打印错误信息
    }

    shmaddr = shmat(shmid, NULL, 0); // 将共享内存附加到当前进程的地址空间
    if (shmaddr == (void *)-1) {
        perror("shmaddr"); // 如果shmat失败,打印错误信息
    }

    printf("shmat ok\n"); // 打印附加成功信息
    printf("data:%s\n", shmaddr); // 打印共享内存中的数据

    shmdt(shmaddr); // 从当前进程的地址空间分离共享内存
    printf("quit\n"); // 打印退出信息

    return 0; // 正常退出程序
}

运行结果:代码中没有实现同步机制,如果多个进程并发访问共享内存,可能会产生竞态条件。后面会使用信号量的机制来控制。

在这里插入图片描述

五、信号

1、信号概述

Linux信号是一种进程间通信机制,用于通知进程发生了某些事件或异常。信号为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。

  • 信号是一种软中断,用于通知进程某个事件已经发生;
  • 信号产生后,不是立即处理,而是先存储下来,等待进程处理;
  • 信号本身不携带数据,仅作为一种通知手段。
(1)信号的名字和编号
  • 每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等;
  • 信号定义在signal.h头文件中,信号名都定义为正整数;
  • 具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0有特殊的应用。

在这里插入图片描述

  • 不可靠信号(1~31号信号):这些信号在进程尚未处理前可能丢失,不支持信号排队;

  • 可靠信号(34~64号信号):支持信号排队,不会丢失。

  • 常见的信号:

    • SIGINT:中断信号,通常由用户按下 Ctrl+C 键发送,用于终止当前进程;
    • SIGTERM:终止信号,用于请求进程正常结束;
    • SIGKILL:强制终止信号,用于立即终止进程,不能被捕获或忽略;
    • SIGHUP:挂起信号,当终端关闭或进程的控制终端发生变化时发送;
    • SIGUSR1SIGUSR2:用户自定义信号,可用于应用程序特定的目的。
(2)信号的处理

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

  1. 忽略信号:进程可以选择忽略某些信号。但是有两种信号不能被忽略(分别是 SIGKILLSIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景;
  2. 捕捉信号:捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理;
  3. 默认处理:系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。
(3)信号的使用方式

其实对于常用的 kill 命令就是一个发送信号的工具,kill 9 PID来杀死进程。比如,我在后台运行了一个 top 工具,通过 ps 命令可以查看他的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了 top 进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。

在这里插入图片描述

对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段,下面将会介绍如何处理自定义信号的处理函数。

2、信号处理函数

  • 信号处理函数的注册

    • 入门版:函数signal
    • 高级版:函数sigaction
  • 信号发送函数

    • 入门版:kill
    • 高级版:sigqueue
(1)入门版:signal和kill
1.signal

signal函数是用于在 Linux 和其他类 Unix 系统中设置和捕捉信号的标准库函数。

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//返回值:成功返回之前设置的信号处理函数,失败返回SIG_ERR,并设置error错误码

参数说明:

  • signum:指定要设置处理方式的信号的编号。
  • handler:指定当信号发生时,要调用的信号处理函数。可以取以下三种特殊值:
    • SIG_IGN:忽略该信号。
    • SIG_DFL:恢复该信号的默认处理方式。
    • 自定义函数:当信号发生时,调用用户定义的函数。

代码示例:

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

// 定义信号处理函数
void handler(int signum) {
    printf("get signum = %d\n", signum);
    switch (signum) {
        case SIGINT: // Ctrl+C 信号
            printf("SIGINT");
            break;
        case SIGTERM: // 终止信号,可通过 kill 命令发送
            printf("SIGTERM");
            break;
        // SIGSTOP 和 SIGKILL 不能被捕捉或忽略
        case SIGSTOP: // 停止进程的信号
            printf("SIGSTOP");
            break;
        case SIGKILL: // 强制杀死进程的信号
            printf("SIGKILL");
            break;
    }
    printf(" cannot quit\n");
}

int main() {
    int signum;

    // 为 SIGINT 和 SIGTERM 注册 handler 函数
    signal(SIGINT, handler);
    signal(SIGTERM, handler);

    // SIGSTOP 和 SIGKILL 不能被捕捉或忽略,注册 handler 函数将无效
    signal(SIGSTOP, handler);
    signal(SIGKILL, handler);

    // 无限循环,等待信号发生
    while (1);
    return 0;
}

运行结果:左侧是注册信号处理函数的运行界面,右侧是发送信号的界面。可以看到:

  • 发送正常终止函数的信号2)SIGINT(等于按Ctcl + C快捷键)和15)SIGTERM程序都不能被结束;
  • 发送19)SIGSTOP信号可以终止函数,但是查看进程51828并未被结束,只是处于T(停止状态);发送18)SIGCONT信号可以让51828进程继续运行;发送9)SIGKILL信号直接把进程杀死。
    • 这两个信号由内核直接处理,不能被捕获或忽略。SIGSTOP 会停止进程,而 SIGKILL 会立即终止进程。
      在这里插入图片描述
2.kill

kill函数是 C 标准库中的一个函数,用于向进程发送信号。

#include <signal.h>

int kill(pid_t pid, int sig);
//返回值:成功发送信号返回0,失败返回-1,并设置error错误码

参数说明:

  • pid:要发送信号的进程 ID。
  • sig:要发送的信号编号。

通过信号指令可以直接发送信号,我们也可以通过代码来实现发送信号的目的:

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

int main(int argc, char **argv) {
    pid_t pid;       // 定义pid_t类型变量,用于存储进程ID
    int signum;      // 定义int类型变量,用于存储信号编号
    char cmd[128] = {0};  // 定义一个字符数组,用于存储构建的命令

    // 检查命令行参数数量是否正确
    if(argc != 3){
        printf("argument set failed\n");
        exit(0);
    }

    // 将命令行参数转换为进程ID和信号编号
    pid = atoi(argv[1]);  // 将字符串转换为整数
    signum = atoi(argv[2]);

 	/*第一种实现发送指令方法*/
    // 使用kill函数发送信号给指定的进程ID
    kill(pid,signum);

    /*第二种实现发送指令方法
    // 构建一个命令字符串,使用kill命令发送相同的信号
    sprintf(cmd,"kill -%d %d",signum,pid);
    system(cmd);  // 执行构建的命令
	*/
    return 0;
}

运行结果:

在这里插入图片描述

(2)高级版:sigaction和sigqueue

上述过程中我们完成了信号的收发,不过这种信号类似于你正在家里看电视,然后有人来敲门,你只知道有人敲门这个信号发生,其它情况啥都不知道。如果在敲门的过程中告诉我是谁,这种情况下不仅收到了有人敲门的信号还能知道是谁敲的门,这就是下面要介绍的情况:在传递信号的过程中携带一些数据。

1.sigaction

sigaction函数是 POSIX 标准中用于处理和设置信号的函数。它提供了比旧的 signal 函数更为灵活和强大的功能。

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//返回值:成功返回0,失败返回-1,并设置error错误码
  • signum:指定要设置或获取动作的信号编号。

  • act:指向 sigaction 结构的指针,如果非空,则用于设置指定信号的动作。结构中可以包含信号处理函数、信号处理时的屏蔽信号集、以及一些标志位。

    • struct sigaction结构体详情:

      struct sigaction {
          void (*sa_handler)(int); 
          //信号处理函数指针,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
          void (*sa_sigaction)(int, siginfo_t *, void *); 
          //扩展信号处理函数指针,能够接受额外数据和sigqueue配合使用
          sigset_t sa_mask; 
          /*阻塞关键字的信号集,默认阻塞;可以再调用捕捉函数之前,把信号添加到信号阻塞字,
          信号捕捉函数返回之前恢复为原先的值。*/
          int sa_flags; // 信号处理标志,SA_SIGINFO表示能够接受数据
      };
      
    • sa_handlersa_sigaction 为指定信号处理函数,使用的是同一块内存空间,只能设置其中的一个,不能两个都同时设置。

    • sa_sigactionsigaction 结构中的一个函数指针,除了标准信号处理函数之外的扩展信号处理函数。这个函数指针允许更复杂的信号处理,例如访问信号的附加信息和设置信号处理函数的上下文。

  • oldact:指向 sigaction 结构体的指针,如果不为NULL ,那么可以对之前的信号配置进行备份,以便之后进行恢复。

关于结构体struct sigaction中的void (*sa_sigaction)(int, siginfo_t *, void *);处理函数还需要有一些说明:

  • void* 是接收到信号所携带的额外数据;而struct siginfo_t这个结构体主要适用于记录接收信号的一些相关信息。
void (*sa_sigaction)(int signum, siginfo_t *info, void *context);
/*signum:信号编号,表示触发的信号类型。
info:指向 siginfo_t 结构的指针,包含信号的附加信息。这个结构提供了信号的详细信息,例如信号的
发送者、发送信号的原因等。
context:指向context_t 结构的指针,表示信号处理函数被调用时的上下文信息。这可以用于获取程序
计数器、信号屏蔽字等信息,但请注意,这个参数在 POSIX 标准中是可选的,并且在某些系统上可能不
可用。*/
  • struct siginfo_t是一个结构体,用于存储与信号相关的详细信息。它的具体内容可能因操作系统和编译器而有所不同,但通常包含以下一些常见的成员:

    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 */
        int      si_band;     /* Band event */
        int      si_fd;       /* File descriptor */
    }
    
2.sigqueue

sigqueue函数使得发送者可以向接收者发送一个实时信号,并且附带最多4个字节的数据。这个函数与 sigaction 函数配合使用,提供了一种灵活的进程间通信方式。

#include <signal.h>

int sigqueue(pid_t pid, int signum, const union sigval value);
//返回值:成功返回0,失败返回-1,并设置error错误码

参数说明:

  • pid:接收信号的进程ID。如果 pid 是负值,表示向多个进程发送信号。

  • signum:指定要发送的信号编号。应该是一个实时信号(信号值大于 SIGRTMIN 并且小于或等于 SIGRTMAX)。

  • value:联合体 sigval 的实例,用于传递附加数据。sigval 可以包含一个整数值 sival_int 或一个指针 sival_ptr

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

使用这个函数之前,必须要有几个操作需要完成:

  1. 使用 sigaction 函数安装信号处理程序时,制定了 SA_SIGINFO 的标志;
  2. sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的时 sa_handler 成员,那么将无法获取额外携带的数据。
3.编程实现携带消息发送信号
1)发送信号并接收一个整形数

sig_receve:接收端

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

// 定义信号处理函数 handler
void handler(int signum, siginfo_t *info, void *content) 
{
    printf("get signum %d\n", signum);// 打印接收到的信号编号
    // 如果 content 不为空,则打印 si_int 和 si_value.sival_int
    if (content != NULL) {
        printf("get data1 = %d\n", info->si_int); //打印附加的整数值
        printf("get data2 = %d\n", info->si_value.sival_int); //打印附加的整数值
    }
}

int main() 
{
    // 打印当前进程的 PID
    printf("pid = %d\n", getpid());

    // 定义 sigaction 结构体,设置信号处理函数和相关标志
    struct sigaction act;
    act.sa_sigaction = handler;
    act.sa_flags = SA_SIGINFO;

    // 为 SIGUSR1 信号设置信号处理函数
    if (sigaction(SIGUSR1, &act, NULL) == -1) {
        perror("sigaction"); // 如果失败,打印错误信息
    }
    // 主循环,使程序持续运行
    while (1);
    return 0;
}

sig_send:发送端

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

int main(int argc, char **argv) 
{
    // 检查命令行参数的数量是否正确
    if (argc != 3) {
        printf("param error\n");// 如果参数数量不正确,则打印错误信息并退出程序
        exit(EXIT_FAILURE);
    }

    // 定义一个 sigval 联合体,用于存储要随信号发送的附加数据
    union sigval value;
    value.sival_int = 98;// 将 value 设置为一个整数 98

    
    int pid = atoi(argv[1]);// 从命令行参数中获取目标进程的 PID
    int signum = atoi(argv[2]);  // 从命令行参数中获取要发送的信号编号

    // 使用 sigqueue 发送信号和附加的整数值给指定的进程
    if (sigqueue(pid, signum, value) == -1) {
        perror("sigqueue");// 如果 sigqueue 调用失败,则打印错误信息 
        exit(EXIT_FAILURE);
    }
  
    printf("done\n"); // 打印完成信息
    return 0;
}

运行结果:左侧为接收信号并打印sigqueue携带的数据,右侧为sigqueue发送信号函数运行结果

在这里插入图片描述

2)发送信号并接收字符串(同一进程内)

sigqueuesigval_t联合体不能直接传递字符串,我们通过传递指向字符串的指针来间接实现。

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

// 定义信号处理函数 handler
void handler(int signum, siginfo_t *info, void *content) { 
    printf("get signum %d\n", signum);// 打印接收到的信号编号
    
    // 注意:这里假设
    char *str = (char *)info->si_value.sival_ptr;//将发送的信号附加值转换成char*字符串
    // 如果 content 不为空,打印附加的字符串
    if (content != NULL) {
        printf("get str = %s\n", str);
    }
}

int main() {
    // 打印当前进程的 PID
    printf("pid = %d\n", getpid());

    // 定义 sigaction 结构体,设置信号处理函数和相关标志
    struct sigaction act;
    act.sa_sigaction = handler; // 设置信号处理函数
    act.sa_flags = SA_SIGINFO;  // 设置标志,使用 siginfo_t 结构体传递信号信息

    // 为 SIGUSR1 信号设置信号处理函数
    if (sigaction(SIGUSR1, &act, NULL) == -1) {
        perror("sigaction"); // 如果失败,打印错误信息
        exit(EXIT_FAILURE);
    }
    printf("===================\n");
   
    char *str = "hello"; // 定义要发送的字符串
    union sigval value;   // 定义 sigval 联合体,用于存储要随信号发送的附加数据
    value.sival_ptr = str;// 将 value 设置为指向字符串的指针

    // 使用 sigqueue 发送信号和附加的字符串指针给当前进程
    if (sigqueue(getpid(), SIGUSR1, value) == -1) {
        perror("sigqueue");
        exit(EXIT_FAILURE);
    }

    // 无限循环,使程序持续运行,直到信号被接收和处理
    while (1);
    return 0;
}

运行结果:在同一个进程内,实现了信号携带字符串的收发
在这里插入图片描述

3)不同进程间发送与接收携带字符串的信号(共享内存方式)

在实验的过程中发现,在父子进程,或者同一进程中发送一个带有字符串的信号是可以完成字符串打印的工作,但是如果在不同进程之间打印传递的字符串便会出现段错误的情况,为了解决这个问题,可以使用共享内存的方法,即在发送信号时,将我们需要传递的字符串放入共享内存中,在接收信号时,再将我们传递的数据从共享内存中将字符串读取出来。

sig_receve:接收端

#include <stdio.h>
#include <signal.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>

// 定义处理 SIGUSR1 信号的函数
void handler_USR1(int signum, siginfo_t *info, void *content) {
    
    printf("get signum %d\n", signum);// 打印接收到的信号编号
    int data = info->si_int;  //从siginfo_t结构体中获取附加的整数值
    
    // 如果 content 不为空,则打印附加的整数值
    if (content != NULL) {
        printf("get data = %d\n", data);
    }
}

// 定义处理 SIGUSR2 信号的函数
void handler_USR2(int signum, siginfo_t *info, void *content) {
    
    printf("get signum %d\n", signum);// 打印接收到的信号编号
    int shmid;
    char *shmaddr = info->si_ptr;// 从 siginfo_t 结构体中获取附加的共享内存地址
    
    key_t key;// 用于创建共享内存键的 key_t 类型变量
    key = ftok(".", 3);// 创建一个唯一的键
    shmid = shmget(key, 1024 * 4, 0);// 使用 ftok 生成的键来获取共享内存的标识符
    shmaddr = shmat(shmid, NULL, 0);// 将共享内存附加到调用进程的地址空间
    
    // 如果 content 不为空,则打印共享内存中的字符串
    if (content != NULL) {
        printf("str = %s\n", shmaddr);
    }
    
    shmdt(shmaddr);// 从进程的地址空间分离共享内存
    shmctl(shmid, IPC_RMID, 0); // 删除共享内存段
}

int main() {
    // 定义 sigaction 结构体,为两个信号设置信号处理函数
    struct sigaction act1, act2;
    act1.sa_sigaction = handler_USR1; // 设置 SIGUSR1 的信号处理函数
    act1.sa_flags = SA_SIGINFO;       // 设置标志,使用 siginfo_t 结构体传递信号信息

    act2.sa_sigaction = handler_USR2; // 设置 SIGUSR2 的信号处理函数
    act2.sa_flags = SA_SIGINFO;       // 设置标志,使用 siginfo_t 结构体传递信号信息

    
    printf("pid = %d\n", getpid());// 打印当前进程的 PID
   
    sigemptyset(&act1.sa_mask); // 初始化信号掩码
    // 为 SIGUSR1 信号设置信号处理函数
    if (sigaction(SIGUSR1, &act1, NULL) == -1) {
        perror("sigaction1");
    }
    sigemptyset(&act2.sa_mask);
    
    // 为 SIGUSR2 信号设置信号处理函数
    if (sigaction(SIGUSR2, &act2, NULL) == -1) {
        perror("sigaction2");
    }
    
    
    while (1);// 一直等待信号触发
    return 0;
}

sig_send:发送端

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/ipc.h>


int main(int argc, char **argv) {
    // 检查命令行参数数量是否正确
    if (argc != 3) {
        printf("param error\n");// 如果参数数量不正确,打印错误信息并退出程序
        exit(EXIT_FAILURE);
    }
    
    int pid = atoi(argv[1]);// 从命令行参数中获取目标进程的 PID
    int signum = atoi(argv[2]);// 从命令行参数中获取信号编号

    key_t key;
    key = ftok(".", 3);// 使用 ftok 生成共享内存的键

    
    union sigval value1, value2;// 定义两个 sigval 联合体,用于存储要随信号发送的附加数据
    value1.sival_int = 98; // 设置 value1 为一个整数 98
    
    // 如果信号编号为 SIGUSR1,则向目标进程发送信号和附加的整数值
    if (signum == SIGUSR1) { 
        if (sigqueue(pid, signum, value1) == -1) {
            perror("sigqueue1");
        }
        printf("SIGUSR1 send ok\n");
    }

    
    int shmid = shmget(key, 1024 * 4, IPC_CREAT | 0666);// 创建或获取共享内存的标识
    char *shmaddr = shmat(shmid, NULL, 0);// 将共享内存附加到当前进程的地址空间 
    strcpy(shmaddr, "hello word");// 向共享内存中复制字符串 "hello word"
    value2.sival_ptr = shmaddr; // 设置 value2 为共享内存的地址
    
    // 如果信号编号为 SIGUSR2,则向目标进程发送信号和附加的共享内存地址
    if (signum == SIGUSR2) { 
        if (sigqueue(pid, signum, value2) == -1) {
            perror("sigqueue2");
        }
        printf("SIGUSR2 send ok\n");
    }

    return 0;
}

运行结果:左侧接收信号,并做出相应的处理,右侧发送信号。
在这里插入图片描述

信号处理函数应该尽量简单和快速,避免在其中进行耗时或阻塞的操作,以免影响程序的正常执行。对于复杂的信号处理逻辑,可能需要考虑使用其他机制,如信号队列或异步 I/O。

六、信号量

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,表示可用资源的数量。信号量用于控制多个进程或线程对共享资源访问的同步机制,而不是用于存储进程间通信数据。

1、特点

  1. 信号量有两种操作:P操作(也称为等待操作)和 V操作(也称为释放操作);
    • P操作(等待或下取):当进程或线程需要访问共享资源时,它首先执行P操作,如果信号量的值大于零,则将其减一并继续执行。如果信号量的值为零,则进程或线程将等待,直到其他进程或线程释放资源。
    • V操作(信号或上取):当进程或线程完成对共享资源的访问时,它执行V操作,将信号量的值增加一,这可能会唤醒等待的进程或线程。
  2. 互斥:信号量可以用来实现互斥,确保一次只有一个进程或线程访问特定的资源;
  3. 同步:信号量可以用于同步进程或线程的执行,若要在进程间传递数据需要结合共享内存。

2、相关API介绍

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1

int semctl(int semid, int semnum, int cmd, ...);
// 控制信号量的相关信息

int semop(int semid, struct sembuf *sops, size_t nsops);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
(1)semget

semget函数用于创建或获取信号量集标识符。

int semget(key_t key, int nsems, int semflg);
//返回值:成功返回信号量的标识符,失败返回-1,并设置error错误码

参数说明:

  • key:用于标识信号量集的键。如果 keyIPC_PRIVATE,则创建一个新的信号量集。
  • nsems:信号量集的大小,即信号量的数量。
  • semflg:信号量集的权限和控制标志。可以包括:
    • IPC_CREAT:如果信号量集不存在,则创建一个新的;
    • IPC_EXCL:与 IPC_CREAT 一起使用,如果信号量集已存在,则 semget 调用失败;
    • 权限掩码(如 0666):设置信号量集的访问权限;
    • IPC_NOWAIT:如果对信号量的操作不能立即完成(例如,信号量已被占用),则立即返回错误,而不是阻塞等待。
(2)semctl

semctl函数用于对信号量进行控制操作。

int semctl(int semid, int semnum, int cmd, ...);
//返回值:成功返回一个正数,失败返回-1,并且设置error错误码

参数说明:

  • semid:信号量集的标识符,由 semget 函数返回。

  • semnum:信号量集中信号的序号,从0开始。

  • cmd:指定要执行的控制命令,常用的cmd命令包括:

    • IPC_RMID:删除信号量集,释放所有相关资源;

    • SETVAL:将信号量集合中第 semnum 个信号量的 semval 值设置为 semun.val

    • IPC_STAT:获取信号量集的状态,需要提供 sembuf 结构的指针作为额外参数;

    • IPC_SET:设置信号量集的属性,如更改所有权或权限,需要提供 sembuf 结构的指针;

    • GETALL:获取信号量集所有信号的值,需要提供数组指针作为额外参数;

    • SETALL:设置信号量集所有信号的值,需要提供数组指针作为额外参数;

    • GETNCNT:获取等待指定信号量变为可用的进程数量。

  • 省略号(…):表示该函数可能有额外的参数,根据 cmd 的值确定, 设置cmd后需要结合下面结构体使用:

    union semun {
        int val;               /* 用于SETVAL命令 */
        struct semid_ds *buf;  /* 指向struct semid_ds的指针,这个结构包含了信号量集的状态
        						信息。用于IPC_STAT和IPC_SET命令 */
        unsigned short *array; /* 指向unsigned short的数组指针,这个数组用于获取或设置信号
        						量集中所有信号量的值。用于GETALL和SETALL命令 */
        struct seminfo *__buf; /* 系统内部使用 ,通常在用户空间不使用。*/
    };
    
(3)semop

semop函数用于对信号量进行操作,它允许对信号量集(semaphore set)中的一个或多个信号量执行原子性的操作。这些操作可以是 P 操作(等待或减量)或 V 操作(信号或增量)。

int semop(int semid, struct sembuf *sops, size_t nsops);
//返回值:成功返回0;失败返回-1,并设置error错误码

参数说明:

  • semid:信号量集的标识符,由 semget 函数返回。

  • sops:指向 sembuf 结构数组的指针,每个结构指定了一个信号量操作。

    struct sembuf {
        unsigned short sem_num; /* 信号量在信号量集中的索引 */
        short sem_op;           /* 要执行的操作:正值表示V操作(增加信号量的值),
        						负值表示P操作(减少信号量的值) */
        int sem_flg;           /* 操作标志,通常为 SEM_UNDO,表示如果进程在操作信号量时被
        						终止,系统将自动撤销该操作,以避免死锁。 */
    };
    
  • nsops:数组中 sembuf 结构的数量,即要执行的操作数量。

3、编程实现信号量控制进程

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

// 定义信号量联合体
union semun {
    int val;               // SETVAL命令使用
    struct semid_ds *buf;  // IPC_STAT, IPC_SET命令使用
    unsigned short *array; // GETALL, SETALL命令使用
    struct seminfo *__buf; // Linux特有的IPC_INFO命令使用
};

// P操作:等待信号量
void pGetKey(int id) {
    struct sembuf set;
    set.sem_num = 0; // 指定信号量集中的第一个信号量
    set.sem_op = -1; // 执行P操作,信号量值减1,表示获取信号量
    set.sem_flg = SEM_UNDO; // 设置信号量撤销选项

    semop(id, &set, 1); // 对信号量执行操作
    printf("get key\n");
}

// V操作:发布信号量
void vPutKey(int id) {
    struct sembuf set;
    set.sem_num = 0;
    set.sem_op = 1; // 执行V操作,信号量值加1,表示释放信号量
    set.sem_flg = SEM_UNDO;

    semop(id, &set, 1);
    printf("put key\n");
}

int main() {
    key_t key; 
    key = ftok(".", 3); // 创建唯一的键值

    int semid = semget(key, 1, IPC_CREAT | 0666); // 创建或获取信号量集标识符
    union semun initsem;
    initsem.val = 0; // 初始化信号量的值为0

    semctl(semid, 0, SETVAL, initsem); // 设置信号量的初识值

    pid_t pid; // 进程ID
    pid = fork(); // 创建子进程

    if (pid > 0) {
        // 父进程
        pGetKey(semid); // 执行P操作
        printf("this is father\n");
        vPutKey(semid); // 执行V操作
    } else if (pid == 0) {
        // 子进程
        printf("this is child\n");
        vPutKey(semid); // 执行V操作
    } else {
        // fork失败
        perror("fork");
    }
    //销毁信号量
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        exit(EXIT_FAILURE);
    }
    printf("semaphore set destroyed\n");

    return 0;
}

运行结果:初始化信号量的过程中让信号量值为0,父进程获想要取信号量但是没有,等到子进程执行V操作释放信号量之后父进程执行P操作才能得到信号量。保证了子进程先运行父进程后运行。

在这里插入图片描述

七、IPC结合实现进程间通信

1、IPC结合实现进程间通信实例

下面将使用【共享内存+信号量+消息队列】的组合来实现服务器进程与客户进程间的通信。

  • 共享内存用来传递数据;
  • 信号量用来同步;
  • 消息队列用来 在客户端修改了共享内存后通知服务器读取。

server.c:服务端接收信息

#include <sys/ipc.h>    // 包含进程间通信相关的头文件
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义消息队列中的消息结构
struct msgbuf {
    long mtype; // 消息类型
    char mtext;  // 消息文本
};

// 定义信号量联合体,用于semctl函数
union semun {
    int val;            // 用于SETVAL命令
    struct semid_ds *buf; // 用于IPC_STAT和IPC_SET命令
};

// 删除IPC资源的函数
void delet_IPC(int msgid, char *shm, int shmid, int semid) {
    shmdt(shm);// 断开共享内存 
    // 删除共享内存
    if (shmctl(shmid, IPC_RMID, NULL) == -1){
        perror("Failed to delete shared memory");
    }
        // 删除消息队列
	if (msgctl(msgid, IPC_RMID, NULL) == -1){
        perror("Failed to delete message queue");
    }
    // 删除信号量集
    if (semctl(semid, 0, IPC_RMID) == -1){
        perror("Failed to delete semaphore");
    }
}

// P操作:等待信号量
void p_handler(int semid) {
    struct sembuf set;
    set.sem_num = 0;
    set.sem_op = -1; // 执行P操作
    set.sem_flg = SEM_UNDO;
    semop(semid, &set, 1);
}

// V操作:释放信号量
void v_handler(int semid) {
    struct sembuf set;
    set.sem_num = 0;
    set.sem_op = 1; // 执行V操作
    set.sem_flg = SEM_UNDO;
    semop(semid, &set, 1);
}

int main() {
    key_t key;              // 用于ftok的键
    char *shm;               // 共享内存的指针
    int flag = 1;           // 控制循环的标记
    int msgid, shmid, semid; // 消息队列、共享内存和信号量的ID
    struct msgbuf rcvmsg;   // 定义接收消息的缓冲区
    union semun initsem;	// 定义信号量的初始化联合体

    // 获取或创建key    
    if ((key = ftok(".", 'a')) == -1){
        perror("ftok failed");
        exit(EXIT_FAILURE);
    }
    // 创建或获取消息队列
    if ((msgid = msgget(key, IPC_CREAT | 0666)) == -1) {
        perror("msgget failed");
        exit(EXIT_FAILURE);
    }
    // 创建或获取共享内存
   
    if ((shmid = shmget(key, 1024, IPC_CREAT | 0666)) == -1) {
        perror("shmget failed");
        exit(EXIT_FAILURE);
    }
    // 挂载共享内存 
    if ((shm = (char *)shmat(shmid, 0, 0)) == (void *)(-1)) {
        perror("shmat failed");
        exit(EXIT_FAILURE);
    }
    // 创建或获取信号量集
    if ((semid = semget(key, 1, IPC_CREAT | 0666)) == -1) {
        perror("semget failed");
        exit(EXIT_FAILURE);
    }
    // 初始化信号量的值为1
    initsem.val = 1;
    if (semctl(semid, 0, SETVAL, initsem) == -1) {
        perror("semctl SETVAL failed");
    }

    while (flag) {
        // 接收消息
        msgrcv(msgid, &rcvmsg, sizeof(struct msgbuf), 888, 0);
        // 根据消息内容执行不同的操作
        switch (rcvmsg.mtext) {
            case 'r': // 读操作
                printf("read: ");
                p_handler(semid); // 执行P操作
                printf("%s\n", shm); // 打印共享内存中的内容
                v_handler(semid); // 执行V操作
                break;

            case 'q': // 退出操作
                printf("quit\n");
                // 删除所有IPC资源
                delet_IPC(msgid, shm, shmid, semid);
                flag = 0; // 设置退出标志
                break;
        }
    }
    return 0;
}

client.c客户端发送消息

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <string.h>

struct msgbuf {
    long mtype; // 消息类型
    char mtext; // 消息文本
};

// P操作:等待信号量,直到信号量的值大于0,然后将其减1
void p_handler(int semid) {
    struct sembuf set;
    set.sem_num = 0;
    set.sem_op = -1;
    set.sem_flg = SEM_UNDO;
    semop(semid, &set, 1);
}

// V操作:释放信号量,将其值加1,并可能唤醒等待该信号量的其他进程
void v_handler(int semid) {
    struct sembuf set;
    set.sem_num = 0;
    set.sem_op = 1;
    set.sem_flg = SEM_UNDO;
    if (semop(semid, &set, 1) == -1){
        perror("V operation failed");
    }
}

int main() {
    key_t key;
    char *shm;
    char str[128];
    int flag = 1;
    int msgid, shmid, semid;
    struct msgbuf readmsg;

    // 创建或获取IPC的键
  	if ((key = ftok(".", 'a')) < 0){
        perror("ftok failed");
        exit(EXIT_FAILURE);
    }
    // 获取消息队列ID
    if ((msgid = msgget(key, 0666 | IPC_CREAT)) == -1){
        perror("msgget failed");
        exit(EXIT_FAILURE);
    }
    // 获取共享内存ID
    if ((shmid = shmget(key, 1024, 0666 | IPC_CREAT)) == -1){
        perror("shmget failed");
        exit(EXIT_FAILURE);
    }
    // 将共享内存附加到当前进程的地址空间
    shm = (char *)shmat(shmid, 0, 0);
    if (shm == (char *)(-1)){
        perror("shmat failed");
        exit(EXIT_FAILURE);
    }
    
    // 获取信号量ID
    if ((semid = semget(key, 0, 0666 | IPC_CREAT)) == -1){
        perror("semget failed");
        exit(EXIT_FAILURE);
    }

    // 打印菜单
    printf("---------------------------------------\n");
    printf("--                IPC                --\n");
    printf("--  input w :write data send client  --\n");
    printf("--  input q :quit procedure          --\n");
    printf("---------------------------------------\n");

    // 主循环
    while (flag) {
        char c;
        printf("input:");
        scanf("%c", &c); // 读取用户输入
        switch (c) {
            // 写操作
            case 'w':
                getchar(); // 消耗掉scanf后的换行符
                p_handler(semid); // 执行P操作
                memset(str, 0, sizeof(str)); // 清空字符串
                printf("write:\n");
                // 读取用户输入的字符串
                fgets(str, sizeof(str), stdin); // 使用fgets代替gets以避免缓冲区溢出
                strcpy(shm, str); // 将用户输入的字符串复制到共享内存
                v_handler(semid); // 执行V操作

                // 发送消息给读者,告知有新数据写入共享内存
                readmsg.mtype = 888;
                readmsg.mtext = 'r';
                msgsnd(msgid, &readmsg, sizeof(struct msgbuf), 0);
                break;

            // 退出操作
            case 'q':
                printf("quit\n");
                // 清除输入缓冲区直到遇到换行符或文件结束符
                while ((c = getchar()) != '\n' && c != EOF);
                // 发送退出消息
                readmsg.mtype = 888;
                readmsg.mtext = 'q';
                msgsnd(msgid, &readmsg, sizeof(struct msgbuf), 0);
                flag = 0; // 设置退出标志
                break;

            // 输入错误处理
            default:
                printf("%c input error\n", c);
                // 清除输入缓冲区
                while ((c = getchar()) != '\n' && c != EOF);
                break;
        }
    }
    return 0;
}

运行结果:左侧为服务端,接收来自客户端的消息,右侧为客户端,发送指令然后传递消息。

在这里插入图片描述

2、IPC通信方式总结

  1. 管道(Pipes)
    • 优点:
      • 简单易用,适合父子进程间的通信。
      • 可以实现双向通信(双管道)。
    • 缺点:
      • 只能在有亲缘关系的进程间使用。
      • 数据传输为字节流,没有消息边界。
  2. 命名管道(Named Pipes)
    • 优点:
      • 允许无亲缘关系的进程通信。
      • 可以存在多个写入者和读取者。
    • 缺点:
      • 性能开销较大。
      • 缓冲区有限,可能需要额外的同步。
  3. 消息队列(Message Queues)
    • 优点:
      • 允许不同进程发送和接收消息,无需亲缘关系。
      • 支持消息优先级。
    • 缺点:
      • 系统资源有限,过多的消息队列或消息可能导致问题。
      • 消息长度有限制。
  4. 信号(Signals)
    • 优点:
      • 简单快速,用于发送通知。
      • 可以异步地发送给进程。
    • 缺点:
      • 信息量有限,只能传递有限的信号值。
      • 可能引起竞态条件。
  5. 共享内存(Shared Memory)
    • 优点:
      • 通信速度快,因为避免了数据的复制。
      • 可以被多个进程同时访问。
    • 缺点:
      • 需要同步机制来避免竞态条件。
      • 管理复杂,如内存分配和同步。
  6. 信号量(Semaphores)
    • 优点:
      • 用于进程同步,可以控制对共享资源的访问。
      • 可以跨多个进程使用。
    • 缺点:
      • 使用不当可能导致死锁。
      • 操作较为复杂
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值