Linux - 笔记 进程间通信(IPC)

发布于:2024-07-01 ⋅ 阅读:(12) ⋅ 点赞:(0)

目录

一、前言

1.1 什么是进程间通信?

1.2 进程间通信的作用?

1.3 进程间通信的机制?

1.4 IPC机制介绍

二、IPC机制

2.1 无名管道

2.1.1 概念

2.1.2 实现

2.2 有名管道

2.2.1 概念

2.2.2 实现

2.3 无名管道与有名管道对比

2.4 IPC对象

2.4.1 消息队列

2.4.2 共享内存

2.4.3 信号量

2.5 消息队列

2.5.1 操作函数

2.5.1.1 mq_open

2.5.1.2 mq_send

2.5.1.3 mq_receive

2.5.1.4 mq_close

2.5.1.6 mq_getattr

2.5.1.7 mq_setattr

2.5.2 实操

2.6 共享内存

2.6.1 操作函数

2.6.1.1 shm_open

2.6.1.2 ftruncate

2.6.1.3 mmap

2.6.1.4 munmap

2.6.2 实操

2.7 信号量

2.7.1 操作函数

2.7.1.1 sem_open

2.7.1.2 sem_close

2.7.1.4 sem_wait

2.7.1.5 sem_post

2.7.2 实操

总结


一、前言

1.1 什么是进程间通信?

        进程间通信(IPC)是操作系统中的一种机制,用于不同进程之间进行数据和信息的交换与共享。它允许并发执行的进程协调工作,共享资源,或者通过消息传递进行协作。在一个多任务的操作系统中,多个进程可以同时运行,但是这些进程是相互独立的,它们有自己的地址空间和上下文,无法直接访问对方的内存空间。如果多个进程需要协作来完成某项任务,或者需要共享某些数据,就需要使用进程间通信机制来进行通信和协作。

1.2 进程间通信的作用?

(1)实现进程间数据共享:进程间通信可以使不同进程之间共享数据,避免了数据复制的开销,提高了系统的性能。共享内存和消息队列是实现进程间数据共享的常用方法。

(2)提高系统的可靠性:进程间通信可以将多个进程组织成一个整体,使得它们可以协同工作,提高了系统的可靠性。例如,多个进程可以共同访问同一个文件,避免了数据的竞争和冲突。

(3)实现进程间协作:进程间通信可以使进程之间相互协作,共同完成任务。一个进程可以向另一个进程发送请求,请求另一个进程提供某些服务,如打印文件等。管道、消 息队列、信号量、共享内存等机制都可以用来实现进程间协作。

(4)提高系统的安全性:进程间通信可以实现不同进程之间的数据隔离和保护,从而提高了系统的安全性。例如,共享内存和消息队列可以通过权限控制来保护数据的安全

1.3 进程间通信的机制?

        根据 IPC 机制所依赖的资源类型可以划分为 基于系统资源的 IPC 机制和基于文件系统的 IPC 机制。基于文件系统的 IPC 机制主要是通过操作文件系统中的文件和管道来进行进程间的通信,这些通信机制依赖于文件系统提供的特定功能,如普通文件、命名管道、套接字文件等。而基于系统资源的进程间通信机制主要是利用系统内核中提供的一些共享资源来实现进程间的通信,这些共享资源包括共享内存、消息队列、信号量等。这些通信机制依赖于操作系统内核所提供的特定功能,如内存管理、进程调度、信号处理等。

        IPC包括多种技术,如管道、消息队列、共享内存和信号量等。每种IPC方式都有其适用的场景和特性,例如管道适用于父子进程之间的单向通信,消息队列则支持异步的消息传递,共享内存则允许进程直接共享物理内存。通过这些IPC机制,进程可以安全地交换信息,协同完成复杂任务,提高系统的效率和灵活性。

1.4 IPC机制介绍

(1)管道(Pipe):管道是一种基于文件描述符的通信机制,可以实现进程间的单向通信,分为无名管道和有名管道(有时候也被叫命名管道)两种。无名管道是在进程创建时自动创建的,只能在具有亲缘关系的进程间使用;有名管道则是由系统中的一个特殊文件来实现的,可以在任意进程之间进行通信。

(2)消息队列(Message Queue):消息队列是一种基于内核对象的通信机制,可以实现进程间的异步通信。消息队列中包含多个消息,每个消息都有一个特定的类型和长度,进程可以通过指定消息类型来选择接收特定类型的消息。

(3)共享内存(Shared Memory):共享内存是一种基于内核对象的通信机制,可以让多个进程共享同一块物理内存。多个进程可以同时访问这块内存,从而实现快速、高效的数据共享。共享内存需要考虑数据同步和互斥等问题,以保证数据的一致性和正确性。

(4)信号量(Semaphore):信号量是一种基于内核对象的通信机制,用于实现进程间的同步和互斥。信号量可以用于实现多个进程之间的资源共享和互斥,通过对信号量进行加锁和解锁操作,可以控制多个进程之间的访问顺序和数量。

(5)Socket:Socket 是一种基于网络的通信机制,可以实现不同计算机之间的进程间通信。 Socket 可以实现进程间的数据传输和通信,可以通过网络协议(如 TCP、UDP 等)来进行数据 传输和路由。

总结一下:

机制名称 机制类型 实现原理 数据共享 进程关系 通信方式
无名管道 基于文件描述符 单向通信 有血缘关系进程 无名管道
有名管道 基于文件描述符 单向通信 无关进程 有名管道
消息队列 基于内核对象 异步通信 无关进程 先进先出
共享内存 基于内核对象 共享同一块物理内存 无关进程 直接读写
信号量 基于内核对象 同步与互斥 无关进程 加锁/解锁
Socket 基于网络 跨网络的进程间通信 无关进程 客户端和服务端

二、IPC机制

注:这里主要是POSIX下的IPC机制。

2.1 无名管道

2.1.1 概念

        无名管道是一种单向的、字节流的通信管道,可以在进程之间传递数据。由于是匿名管道, 所以无法通过文件系统来访问它,只能通过文件描述符在具有亲缘关系的进程之间使用,如父 子进程、兄弟进程等。

        无名管道实际上是一种特殊的文件,它存在于内存中,当一个进程创建了一个无名管道之 后,它实际上创建了两个文件描述符,一个用于读取数据,另一个用于写入数据。在进程调用 fork()函数之后,子进程会继承父进程的文件描述符,因此可以通过这两个文件描述符实现进程间通信。

特点:

1)单向传输:无名管道是单向的,数据只能从管道的一端进入,从另一端出去。

2)字节流传输:管道中的数据是以字节流的形式传输的,没有消息边界。

3)亲缘关系:只有具有亲缘关系的进程才能使用无名管道进行通信。

4)阻塞式读写:管道的读写操作是阻塞式的,即如果管道为空(写端没有写入数据), 读操作会一直阻塞等待,直到有数据写入为止。

2.1.2 实现

无名管道创建使用系统调用pipe():

#include <unistd.h>
int pipe(int pipefd[2])
/*
参数 pipefd,是一个有两个元素的数组,分别代表管道的读端和写端。
pipefd[0]代表管道的读端,pipefd[1]代表管道的写端。
调用 pipe()函数后,系统会自动创建一个无名管道,并将读端和写端返回给调用进程。
如果成功创建管道,返回 0;否则返回-1,表示创建失败。
*/

 使用无名管道的步骤一般为:

1)使用 pipe()函数创建无名管道。

2)使用 fork()函数创建子进程,父子进程共享管道的读写端。

3)在父进程中关闭管道的读端,向管道的写端写入数据。

4)在子进程中关闭管道的写端,从管道的读端读取数据。

5)在合适的时机关闭管道的读端和写端,以防止发生资源泄露的情况。

例子一: 单向通信,父进程发信息给子进程

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

int main(int argc , char *argv[]) 
{
    int fd[2];
    char buffer[] = "hello world!";
    pid_t pid;

    if (0 > pipe(fd) ) {
        perror("pipe error");
        exit(-1);
    }

    pid = fork();
    if (0 > pid) {
        perror("fork error");
        exit(-1);
    } else if (0 < pid) {
        close(fd[0]);
        printf("this is father process,pid = %d \r\n", getpid());
        write(fd[1], buffer, sizeof(buffer));
        close(fd[1]);  
    } else {
        close(fd[1]); 
        printf("this is child process,pid = %d\n", getpid());
        read(fd[0], buffer, sizeof(buffer)); 
        printf("Message from parent process: %s\n", buffer); 
        close(fd[0]); 
    }

    return 0;
}

                                

例子二:使用无名管道实现双向通信,父进程与子进程之间,创建两个管道。

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

int main(int argc , char *argv[]) 
{
    int fd1[2], fd2[2];
    char buffer1[] = "hello child process\r\n";
    char buffer2[] = "hello parent process\r\n";
    char buf[50] = {0};
    pid_t pid;

    if (0 > pipe(fd1) ) {
        perror("pipe error");
        exit(-1);
    }

    if (pipe(fd2) == -1) {
        perror("pipe2 error");
        exit(EXIT_FAILURE);
    }

    
    pid = fork();
    if (0 > pid) {
        perror("fork error");
        exit(-1);
    } else if (0 < pid) {
        close(fd1[0]);
        close(fd2[1]);
        printf("this is father process,pid = %d \r\n", getpid());

        write(fd1[1], buffer1, sizeof(buffer1));
        read(fd2[0], buf, sizeof(buf)); 
        printf("Message from child process: %s\n", buf);

        close(fd1[1]);
        close(fd2[0]);   
    } else {
        close(fd1[1]); 
        close(fd2[0]);
        printf("this is child process,pid = %d\n", getpid());

        read(fd1[0], buf, sizeof(buf)); 
        printf("Message from parent process: %s\n", buf);
        write(fd2[1], buffer2, sizeof(buffer2));
        
        close(fd1[0]); 
        close(fd2[1]);
    }

    return 0;
}

2.2 有名管道

2.2.1 概念

        有名管道是一种特殊类型的 Unix/Linux 文件,也被称为 FIFO(First-In-First-Out)管道,用 来在进程之间传输数据的,与无名管道不同,有名管道是通过文件系统路径命名的管道,可以在进程之间进行通信。有名管道的操作方式类似于打开文件,即进程可以打开有名管道来读取或写入其中的数据。

        使用有名管道的方法有两种,一种是使用命令 “mkfifo pipe_name”命令创建,pipe_name 是有名管道的名称,可以是任何有效的文件名。创建有名管道后,它就会在文件系统中以文件的形式存在, 但是它的内容为空,且文件类型为 p 表示管道,如下图所示:

                        

另一种是使用mkfifo()函数进行创建:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

pathname:有名管道的路径(文件名)。
mode:指定管道的权限,这与 open 或 creat 函数中的权限参数相同。

返回值
成功时返回 0。
失败时返回 -1,并设置 errno 来指示错误。
2.2.2 实现

        编写两个应用程序来测试有名管道,一个用于向管道写入数据,另一个用来读取数据。

写数据:

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

int main() 
{
    int fd = 0;
    char buf[] = "hello";
    mkfifo("test_fifo", 0644);

    fd = open("test_fifo", O_RDWR);
    if (-1 == fd) {
        perror("open error");
        exit(1);
    } 

    write(fd, buf, sizeof(buf));

    close(fd);

    return 0;
}

读数据:

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

int main() 
{
    int fd = 0;
    char buf[] = "hello";
    mkfifo("test_fifo", 0644); //创建一个命名管道,如果已经存在就不会创建

    fd = open("test_fifo", O_RDONLY);
    if (-1 == fd) {
        perror("open error");
        exit(1);
    } 

    read(fd, buf, 256);
    printf("Received message: %s\n", buf);

    close(fd);

    return 0;
}

开两个终端,一个运行读程序一个运行写程序。先运行读数据会发现阻塞了,这是因为管道内没数据,然后再运行写数据程序,就会发现读到数据了。

        

        

        

2.3 无名管道与有名管道对比

2.4 IPC对象

        在 POSIX 标准中,IPC(进程间通信)对象是用于在不同进程之间进行数据交换和同步的机制。POSIX IPC对象有三种主要类型:消息队列、共享内存和信号量。

2.4.1 消息队列

        消息队列允许进程以消息的形式传递数据。消息队列中的消息按照先进先出的顺序进行处理。消息队列可以用来实现多个进程之间的异步通信。

主要操作:

  • 创建/打开消息队列:mq_open
  • 发送消息:mq_send
  • 接收消息:mq_receive
  • 关闭消息队列:mq_close
  • 删除消息队列:mq_unlink
2.4.2 共享内存

        共享内存允许多个进程访问同一块内存区域。共享内存是实现进程间通信的一种高效方式,因为它避免了在进程间复制数据。

主要操作:

  • 创建/打开共享内存对象:shm_open
  • 调整共享内存大小:ftruncate
  • 映射共享内存到进程地址空间:mmap
  • 取消映射共享内存:munmap
  • 关闭共享内存对象:close
  • 删除共享内存对象:shm_unlink
2.4.3 信号量

        信号量用于在多个进程之间同步对共享资源的访问。信号量可以用于实现进程间的互斥和同步。  

主要操作:

  • 创建/打开信号量:sem_open
  • 等待信号量(减少信号量值):sem_wait(P)
  • 增加信号量值:sem_post(V)
  • 关闭信号量:sem_close
  • 删除信号量:sem_unlink

2.5 消息队列

2.5.1 操作函数
2.5.1.1 mq_open

创建或打开一个消息队列。

#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>

mqd_t mq_open(const char *name, int oflag, ... /* mode_t mode, struct mq_attr *attr */);

/*
name: 消息队列的名字,必须以 '/' 开头。
oflag: 打开方式(如 O_CREAT, O_RDONLY, O_WRONLY, O_RDWR)。
mode: 权限位(如 0644,可选参数)。
attr: 指向 mq_attr 结构体的指针,用于设置消息队列的属性(可选参数)。

返回值:

成功时返回消息队列描述符(类型为 mqd_t)。
失败时返回 (mqd_t) -1,并设置 errno 以指示错误。
*/

消息队列属性结构体:

struct mq_attr {
    long mq_flags;   // 队列的标志
    long mq_maxmsg;  // 队列中最大消息数
    long mq_msgsize; // 每条消息的最大大小
    long mq_curmsgs; // 当前消息队列中的消息数
};
2.5.1.2 mq_send

向消息队列发送消息。

#include <mqueue.h>

int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);

/*
mqdes: 消息队列描述符。
msg_ptr: 指向要发送的消息的指针。
msg_len: 消息的长度。
msg_prio: 消息的优先级。

返回值:

成功时返回 0。
失败时返回 -1,并设置 errno 以指示错误。
*/
2.5.1.3 mq_receive

从消息队列接收消息。

#include <mqueue.h>

ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);

/*
mqdes: 消息队列描述符。
msg_ptr: 指向接收缓冲区的指针。
msg_len: 缓冲区的长度。
msg_prio: 指向存储接收消息优先级的指针。
返回值:

成功时返回接收的字节数。
失败时返回 -1,并设置 errno 以指示错误。
*/
2.5.1.4 mq_close

关闭消息队列。

#include <mqueue.h>

int mq_close(mqd_t mqdes);
/*
mqdes: 消息队列描述符。
返回值:

成功时返回 0。
失败时返回 -1,并设置 errno 以指示错误。
*/

删除消息队列。

#include <mqueue.h>

int mq_unlink(const char *name);
/*
name: 消息队列的名字。
返回值:

成功时返回 0。
失败时返回 -1,并设置 errno 以指示错误。
*/
2.5.1.6 mq_getattr

获取消息队列属性。

#include <mqueue.h>

int mq_getattr(mqd_t mqdes, struct mq_attr *attr);
/*
mqdes: 消息队列描述符。
attr: 指向 mq_attr 结构体的指针,用于存储消息队列的属性。
返回值:

成功时返回 0。
失败时返回 -1,并设置 errno 以指示错误。
*/
2.5.1.7 mq_setattr

设置消息队列属性。

#include <mqueue.h>

int mq_setattr(mqd_t mqdes, const struct mq_attr *newattr, struct mq_attr *oldattr);
/*
mqdes: 消息队列描述符。
newattr: 指向新的 mq_attr 结构体的指针,用于设置新的属性。
oldattr: 指向旧的 mq_attr 结构体的指针,用于存储旧的属性。
返回值:

成功时返回 0。
失败时返回 -1,并设置 errno 以指示错误。
*/
2.5.2 实操

        写两个程序演示如何使用消息队列在两个进程之间进行通信。编译时记得带上 -lrt,这是因为POSIX 消息队列函数属于实时扩展库(librt),在编译和链接时需要明确指定链接此库。

                ​​​​​​​        ​​​​​​​        

发送信息:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>

int main() {
    mqd_t mq;
    struct mq_attr attr;
    char buffer[1024] = "Hello, POSIX IPC";

    // 设置消息队列属性
    attr.mq_flags = 0;
    attr.mq_maxmsg = 10;
    attr.mq_msgsize = 1024;
    attr.mq_curmsgs = 0;

    // 创建或打开消息队列
    mq = mq_open("/test_queue", O_CREAT | O_WRONLY, 0644, &attr);
    if (mq == (mqd_t) -1) {
        perror("mq_open error");
        exit(1);
    }

    // 发送消息
    if (mq_send(mq, buffer, sizeof(buffer), 0) == -1) {
        perror("mq_send error");
        exit(1);
    }

    printf("Message sent: %s\n", buffer);

    // 关闭消息队列
    if (mq_close(mq) == -1) {
        perror("mq_close error");
        exit(1);
    }

    return 0;
}

接收信息:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>

int main() {
    mqd_t mq;
    char buffer[1024];
    ssize_t bytes_read;

    // 打开消息队列
    mq = mq_open("/test_queue", O_RDONLY);
    if (mq == (mqd_t) -1) {
        perror("mq_open error");
        exit(1);
    }

    // 接收消息
    bytes_read = mq_receive(mq, buffer, 1024, NULL);
    if (bytes_read == -1) {
        perror("mq_receive error");
        exit(1);
    }

    printf("Received message: %s\n", buffer);

    // 关闭消息队列
    if (mq_close(mq) == -1) {
        perror("mq_close error");
        exit(1);
    }

    // 删除消息队列
    if (mq_unlink("/test_queue") == -1) {
        perror("mq_unlink error");
        exit(1);
    }

    return 0;
}

        ​​​​​​​        ​​​​​​​        

2.6 共享内存

2.6.1 操作函数

2.6.1.1 shm_open

创建或打开一个 POSIX 共享内存对象。

#include <sys/mman.h>
#include <fcntl.h>
#include <sys/stat.h>

int shm_open(const char *name, int oflag, mode_t mode);

/*
name:共享内存对象的名称,以斜杠 / 开头。
oflag:打开标志,可以是以下值的组合:
O_RDONLY:只读方式打开。
O_RDWR:读写方式打开。
O_CREAT:如果对象不存在,则创建它。
O_EXCL:与 O_CREAT 一起使用,如果对象已经存在,则打开失败。
mode:创建对象时的权限,通常以八进制表示,如 0666。

返回值:

成功时返回共享内存对象的文件描述符(非负整数),失败时返回 -1。
*/
2.6.1.2 ftruncate

设置共享内存对象的大小。

#include <unistd.h>

int ftruncate(int fd, off_t length);

/*
fd:共享内存对象的文件描述符,由 shm_open 返回。
length:新的共享内存大小。
返回值:

成功时返回 0,失败时返回 -1 并设置 errno。
*/
2.6.1.3 mmap

将共享内存对象映射到进程的地址空间。

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

/*
addr:映射的起始地址,通常为 NULL(由系统选择)。
length:映射区域的长度(字节数)。
prot:映射区域的保护方式,可以是以下组合:
PROT_READ:可读。
PROT_WRITE:可写。
PROT_EXEC:可执行。
PROT_NONE:不可访问。
flags:映射的标志,可以是以下组合:
MAP_SHARED:映射区域可被其他进程共享。
MAP_PRIVATE:创建一个私有的拷贝。
fd:共享内存对象的文件描述符,由 shm_open 返回。
offset:从文件的偏移量处开始映射。
返回值:

成功时返回映射区域的起始地址(通常是 addr 或系统选择的地址),失败时返回 MAP_FAILED。
*/
2.6.1.4 munmap

解除共享内存对象在进程地址空间中的映射。

#include <sys/mman.h>

int munmap(void *addr, size_t length);

/*
addr:要解除映射的起始地址。
length:解除映射的长度(字节数)。
返回值:

成功时返回 0,失败时返回 -1 并设置 errno。
*/

删除共享内存对象。

#include <fcntl.h>

int shm_unlink(const char *name);

/*
name:要删除的共享内存对象的名称。
返回值:

成功时返回 0,失败时返回 -1 并设置 errno。
*/
2.6.2 实操

        一个简单的例子,演示了如何使用共享内存在两个进程之间共享数据。该例子分为两个程序,一个写入数据到共享内存,另一个从共享内存读取数据。编译记得加-lrt。

写数据:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>

#define SHM_NAME "/my_shm"
#define SHM_SIZE 4096

int main() {
    int shm_fd;
    void *ptr;

    // 创建共享内存对象
    shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (shm_fd == -1) {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }

    // 设置共享内存大小
    if (ftruncate(shm_fd, SHM_SIZE) == -1) {
        perror("ftruncate");
        exit(EXIT_FAILURE);
    }

    // 将共享内存对象映射到进程地址空间
    ptr = mmap(0, SHM_SIZE, PROT_WRITE, MAP_SHARED, shm_fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    // 写入数据到共享内存
    const char *message = "Hello from writer!";
    sprintf(ptr, "%s", message);

    // 关闭共享内存对象
    if (munmap(ptr, SHM_SIZE) == -1) {
        perror("munmap");
        exit(EXIT_FAILURE);
    }
    if (close(shm_fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

读数据:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>

#define SHM_NAME "/my_shm"
#define SHM_SIZE 4096

int main() {
    int shm_fd;
    void *ptr;

    // 打开共享内存对象
    shm_fd = shm_open(SHM_NAME, O_RDONLY, 0666);
    if (shm_fd == -1) {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }

    // 将共享内存对象映射到进程地址空间
    ptr = mmap(0, SHM_SIZE, PROT_READ, MAP_SHARED, shm_fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    // 读取共享内存中的数据
    printf("Message from writer: %s\n", (char *)ptr);

    // 解除映射和关闭共享内存对象
    if (munmap(ptr, SHM_SIZE) == -1) {
        perror("munmap");
        exit(EXIT_FAILURE);
    }
    if (close(shm_fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    // 删除共享内存对象
    if (shm_unlink(SHM_NAME) == -1) {
        perror("shm_unlink");
        exit(EXIT_FAILURE);
    }

    return 0;
}

        ​​​​​​​        ​​​​​​​        ​​​​​​​      

                  



2.7 信号量

        信号量用于进程间(IPC)或线程间同步。通过信号量,多个进程或线程可以协调对共享资源的访问。信号量的值表示可用资源的数量,当资源被占用时,信号量的值减少,当资源被释放时,信号量的值增加。

使用信号量进行IPC的步骤

  1. 创建或打开信号量:使用 sem_open 创建或打开一个命名信号量。如果信号量不存在,需要创建一个新的信号量。

  2. 等待信号量:使用 sem_wait 等待信号量。当信号量的值大于0时,立即返回并将信号量的值减1;如果信号量的值为0,则阻塞,直到信号量的值大于0。

  3. 释放信号量:使用 sem_post 释放信号量,将信号量的值加1。如果有其他进程或线程在等待信号量,则唤醒其中一个。

  4. 关闭信号量:使用 sem_close 关闭信号量。

  5. 删除信号量:使用 sem_unlink 删除命名信号量(通常在不再需要时执行)。

2.7.1 操作函数
2.7.1.1 sem_open

创建或打开一个命名信号量。

#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>

sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode, unsigned int value */);
/*
name:信号量的名称,以斜杠 / 开头。
oflag:打开标志,可以是以下值的组合:
O_CREAT:如果信号量不存在,则创建它。
O_EXCL:与 O_CREAT 一起使用,如果信号量已存在,则失败。
mode:创建信号量时的权限(可选)。
value:信号量的初始值(可选)。
返回值:

成功时返回信号量的指针,失败时返回 SEM_FAILED 并设置 errno。
*/
2.7.1.2 sem_close

关闭命名信号量。

#include <semaphore.h>

int sem_close(sem_t *sem);
/*
sem:信号量的指针。
返回值:

成功时返回 0,失败时返回 -1 并设置 errno。
*/

删除命名信号量。

#include <semaphore.h>

int sem_unlink(const char *name);
/*
name:信号量的名称。
返回值:

成功时返回 0,失败时返回 -1 并设置 errno。
*/
2.7.1.4 sem_wait

等待信号量。

#include <semaphore.h>

int sem_wait(sem_t *sem);
/*
sem:信号量的指针。
返回值:

成功时返回 0,失败时返回 -1 并设置 errno。
*/
2.7.1.5 sem_post

释放信号量。

#include <semaphore.h>

int sem_post(sem_t *sem);
/*
sem:信号量的指针。
返回值:

成功时返回 0,失败时返回 -1 并设置 errno。
*/
2.7.2 实操

        使用信号量在生产者和消费者之间进行同步。实操程序有两个一个生产者程序和一个消费者程序。生产者向信号量发送信号,消费者等待信号并在接收到信号后继续执行。

编译带上 -pthread。

生产者:

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    sem_t *sem;

    // 创建或打开命名信号量
    sem = sem_open("/mysem", O_CREAT, 0644, 0);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        exit(EXIT_FAILURE);
    }

    // 生产者向信号量发送信号
    printf("Producer: Sending signal\n");
    if (sem_post(sem) == -1) {
        perror("sem_post");
        exit(EXIT_FAILURE);
    }

    // 关闭信号量
    if (sem_close(sem) == -1) {
        perror("sem_close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

消费者:

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    sem_t *sem;

    // 打开命名信号量
    sem = sem_open("/mysem", 0);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        exit(EXIT_FAILURE);
    }

    // 消费者等待信号量的信号
    printf("Consumer: Waiting for signal\n");
    if (sem_wait(sem) == -1) {
        perror("sem_wait");
        exit(EXIT_FAILURE);
    }

    printf("Consumer: Received signal\n");

    // 关闭信号量
    if (sem_close(sem) == -1) {
        perror("sem_close");
        exit(EXIT_FAILURE);
    }

    // 删除信号量
    if (sem_unlink("/mysem") == -1) {
        perror("sem_unlink");
        exit(EXIT_FAILURE);
    }

    return 0;
}

        ​​​​​​​        ​​​​​​​        ​​​​​​​        

        ​​​​​​​        ​​​​​​​        

总结

IPC 概述

  • IPC(Inter-Process Communication,进程间通信)是操作系统中用于实现进程之间数据交换和同步的机制。
  • 主要的 IPC 方法包括管道、消息队列、信号量、共享内存等。

IPC 方法

  • 管道:单向通信管道,适合具有亲缘关系的进程间通信。
  • 消息队列:消息按照一定顺序发送和接收,可以用于不同进程间通信。
  • 信号量:用于进程间同步和互斥控制,可以防止竞态条件。
  • 共享内存:进程共享同一块物理内存区域,效率高但需要同步机制保证数据一致性。

每种 IPC 方法的特点和适用场景

  • 管道:适合于父子进程之间或者具有共同祖先的进程间通信。
  • 消息队列:适合需要按照特定顺序传递消息的场景,可以实现进程间解耦合。
  • 信号量:适合需要控制并发访问的场景,例如生产者消费者问题。
  • 共享内存:适合频繁交换大量数据的进程间通信,可以提高通信效率。