Linux——进程间通信

一、概述

a、共享内存

共享内存是一种在多个进程之间共享数据的机制。它允许多个进程访问相同的物理内存区域,从而实现高效的数据交换和通信。

使用共享内存可以避免进程之间频繁地进行数据复制,而是直接读取和写入共享内存区域,从而提高数据传输的效率。

下面是使用共享内存的基本步骤:

  1. 创建共享内存段:

    • 使用 shmget() 函数创建一个新的共享内存段,或者通过指定一个已存在的共享内存的键值来获取共享内存段的标识符。
    • 指定共享内存的大小,通常使用 shmget() 函数的第二个参数进行指定。
    • 设置标志和访问权限。
    • shmget() 函数返回一个共享内存段的标识符,供后续操作使用。
  2. 连接共享内存段:

    • 使用 shmat() 函数将共享内存段连接到当前进程的地址空间中。
    • 通过指定共享内存段的标识符和其他参数来调用 shmat() 函数。
    • shmat() 函数返回连接后的共享内存段的地址,供后续操作使用。
  3. 使用共享内存:

    • 通过访问共享内存的地址,可以直接读取和写入共享内存区域中的数据。
    • 多个进程可以并发地读写共享内存,但需要注意同步和互斥,以避免竞争条件和数据一致性问题。
  4. 分离共享内存:

    • 当进程不再需要访问共享内存时,使用 shmdt() 函数将共享内存从当前进程的地址空间中分离。
    • 通过指定共享内存的地址来调用 shmdt() 函数。
  5. 删除共享内存段:

    • 当不再需要共享内存段时,使用 shmctl() 函数将共享内存段从系统中删除。
    • 通过指定共享内存段的标识符和相应的命令来调用 shmctl() 函数。

需要注意的是,使用共享内存时需要进行适当的同步和互斥,以确保多个进程之间的数据访问的正确性和一致性。常见的同步机制包括信号量、互斥锁等。

使用共享内存需要谨慎处理,确保数据的正确性和安全性。

b、信号量

信号量(Semaphore)是一种用于实现进程间同步和互斥的机制。它可以用来控制对共享资源的访问,以避免多个进程同时对资源进行操作而引发冲突。

在使用信号量时,通常需要以下几个步骤:

  1. 创建信号量:使用 semget 函数创建一个信号量集,指定信号量集的键值和权限等参数。

  2. 初始化信号量:使用 semctl 函数初始化信号量集中的各个信号量。可以使用 SETVAL 命令设置信号量的初始值。

  3. 操作信号量:使用 semop 函数进行信号量的操作。操作包括 P 操作(等待信号量)和 V 操作(释放信号量)。P 操作通过 semopsem_op 参数设置为负值实现,V 操作通过 sem_op 参数设置为正值实现。

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

int main() {
    int sem_id;
    key_t key;
    struct sembuf sb;

    // 创建信号量
    key = ftok(".", 'S');
    sem_id = semget(key, 1, IPC_CREAT | 0666);

    // 初始化信号量
    semctl(sem_id, 0, SETVAL, 1);

    // 操作信号量(P 操作)
    sb.sem_num = 0;
    sb.sem_op = -1;
    sb.sem_flg = 0;
    semop(sem_id, &sb, 1);

    // 临界区代码
    printf("Critical section\n");

    // 操作信号量(V 操作)
    sb.sem_op = 1;
    semop(sem_id, &sb, 1);

    // 销毁信号量
    semctl(sem_id, 0, IPC_RMID);

    return 0;
}

PV 操作,也称为 P 操作和 V 操作,是信号量的两种基本操作,用于实现进程间的同步和互斥。

  1. P 操作(等待操作):

    • 当一个进程希望访问某个资源时,它需要执行 P 操作来等待该资源的可用性。
    • P 操作通过减小信号量的值来表示资源的占用情况。如果信号量的值大于 0,则表示资源可用,进程可以继续执行;如果信号量的值等于 0,则表示资源不可用,进程需要等待。
    • P 操作的语义是,如果信号量的值大于 0,将其减 1;如果信号量的值为 0,阻塞等待,直到信号量的值大于 0。
  2. V 操作(释放操作):

    • 当一个进程使用完某个资源后,需要执行 V 操作来释放该资源,使其他进程可以访问。
    • V 操作通过增加信号量的值来表示资源的释放。如果有其他进程正在等待该资源,执行 V 操作后会唤醒其中一个等待的进程,使其可以继续执行。
    • V 操作的语义是,将信号量的值加 1,如果有进程正在等待信号量,则唤醒其中一个进程。

PV 操作是原子操作,即在执行 P 操作或 V 操作时,不会被其他进程的操作中断,保证了操作的完整性和正确性。

使用 PV 操作可以实现资源的互斥访问和进程间的同步。当多个进程需要访问同一资源时,可以通过信号量来控制同时只有一个进程能够访问资源,其他进程需要等待。这样可以避免多个进程同时访问资源而引发冲突和不一致的问题。同时,PV 操作也可以用于进程间的同步,使进程按照特定的顺序执行。

二、基本用法

消息队列、信号量、共享内存都需要有键值

在程序中,共享内存和信号量需要通过键值进行标识和访问。键值是一个整数,用于唯一地标识共享内存段和信号量集。

SHM_KEY 宏定义用于定义共享内存的键值,通常是一个非负整数。在程序中,可以使用 SHM_KEY 宏定义来创建共享内存或获取已经存在的共享内存。

SEM_KEY 宏定义用于定义信号量的键值,也是一个非负整数。在程序中,可以使用 SEM_KEY 宏定义来创建信号量集或获取已经存在的信号量集。

这样做的目的是确保在程序中使用相同的键值来访问共享内存和信号量,以便不同的进程可以正确地进行通信和同步。

 

int sprintf(char *str, const char *format, ...);

参数str是一个指向字符数组的指针,表示要写入的目标缓冲区。

参数format是一个格式化字符串,类似于printf()函数中的格式化字符串,可以包含格式化说明符和对应的参数。

sprintf()函数根据格式化字符串和参数生成一个格式化的字符串,并将其写入目标缓冲区中。

在上述代码中,sprintf(msg.mtext, "hello from Process %d", getpid());的作用是将格式化的字符串写入消息结构体的消息文本字段(mtext)中。该字符串的格式是"hello from Process <进程ID>",使用getpid()函数获取当前进程的进程ID,并将其插入到格式化字符串中。

这样,消息结构体的消息文本字段将包含形如"hello from Process <进程ID>"的消息内容,用于发送给消息队列或其他进程进行通信。

a、管道

1、int pipe(int pipefd[2]);

创建管道

参数pipefd是一个长度为2的整型数组,用于返回管道的读端和写端文件描述符。pipefd[0]表示管道的读端,pipefd[1]表示管道的写端。

当调用pipe(pipefd)成功时,返回值为0;否则,返回值为-1,表示创建管道失败。

在使用管道进行进程通信时,通常需要在fork()之前调用pipe()来创建管道,然后将管道的文件描述符传递给子进程,以实现进程之间的数据交换。父进程可以使用管道的读端进行读取,而子进程可以使用管道的写端进行写入。这样,父进程和子进程之间就可以通过管道进行通信了。

2、ssize_t write(int fd, const void *buf, size_t count);

管道写

参数fd是文件描述符,表示要写入的文件或管道的写端。在这里,pipefd[1]表示管道的写端。

参数buf是要写入的数据的指针。在这里,message是要写入管道的字符串。

参数count是要写入的数据的字节数。在这里,strlen(message) + 1表示要写入的字符串的长度加上终止符\0的字节数。

write()函数返回写入的字节数,如果写入失败,返回-1。

因此,write(pipefd[1], message, strlen(message) + 1)的作用是将字符串message写入到管道的写端。注意要确保管道的写端在此之前已经打开。

3、ssize_t read(int fd, void *buf, size_t count);

管道读

参数fd是文件描述符,表示要读取的文件或管道的描述符。在这里,pipefd[0]表示管道的读端。

参数buf是用于接收读取数据的缓冲区的指针。

参数count是要读取的数据的最大字节数。在这里,BUFFER_SIZE表示缓冲区的大小,即最多读取的字节数。

read()函数返回实际读取的字节数,如果读取失败或已到达文件末尾,返回值为0;如果发生错误,返回值为-1。

因此,read(pipefd[0], buffer, BUFFER_SIZE)的作用是从管道的读端读取数据,并将其存储到buffer缓冲区中。注意要确保管道的读端在此之前已经打开,并且缓冲区足够大以容纳要读取的数据。

4、int close(int fd);

关闭管道

参数fd是文件描述符,表示要关闭的文件或管道的描述符。在这里,pipefd[0]表示管道的读端。

当调用close(pipefd[0])成功关闭管道的读端时,返回值为0;否则,返回值为-1,表示关闭失败。

在进程通信中,关闭不需要的文件描述符是一种良好的实践。在这里,子进程关闭管道的读端是因为它不需要从管道中读取数据,而只需要向管道写入数据。关闭不需要的文件描述符可以避免资源的浪费,并且有助于保持代码的清晰性和正确性。

b、消息队列

1、key_t ftok(const char *pathname, int proj_id);

2、int msgget(key_t key, int msgflg)

int msgget(key_t key, int msgflg)是一个系统调用函数,用于创建或获取消息队列。

函数原型如下:

int msgget(key_t key, int msgflg);

参数key是一个类型为key_t的键值,用于定位和访问消息队列。

参数msgflg是一个整数,表示创建或获取消息队列的标志和权限。可以使用以下标志位进行逻辑或运算:

  • IPC_CREAT:如果消息队列不存在,则创建一个新的消息队列。如果消息队列已经存在,则直接获取现有的消息队列。
  • IPC_EXCL:与IPC_CREAT一起使用时,如果消息队列已经存在,则创建失败,返回错误。
  • 权限位:使用八进制表示的权限位,用于设置消息队列的权限。比如0666表示读写权限。

函数返回一个类型为int的消息队列标识符(msgid),用于后续对消息队列的操作。

在使用msgget()函数时,需要确保使用相同的键值和权限位来创建或获取消息队列,以便不同的进程可以定位和访问同一个消息队列。如果创建新的消息队列,它将被分配一个新的消息队列标识符(msgid),可以使用该标识符进行后续的消息队列操作。如果获取现有的消息队列,则会返回该消息队列的标识符。

需要注意的是,对于同一个键值和权限位的组合,只有一个消息队列可以被创建,因此要确保在所有相关进程之间共享相同的键值和权限位。

3、ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

 系统调用函数,用于从消息队列中接收消息。

参数msqid是消息队列的标识符,通过msgget()函数获得。

参数msgp是指向接收消息的缓冲区的指针,它指向一个存储消息的结构体或缓冲区的内存空间。

参数msgsz是接收缓冲区的大小,表示可接收的最大消息大小。

参数msgtyp是指定要接收的消息类型。如果值为0,则表示接收队列中的第一个消息;如果值大于0,则表示接收类型等于msgtyp的消息;如果值小于0,则表示接收类型等于或小于msgtyp绝对值的消息。

参数msgflg是接收消息的标志,用于控制接收行为。常见的标志包括:

  • IPC_NOWAIT:如果没有匹配的消息可用,则立即返回-1,并将errno设置为ENOMSG
  • MSG_NOERROR:如果接收到的消息比msgsz指定的大小要大,则将消息截断为msgsz大小,但不会引发错误。
  • MSG_EXCEPT:接收类型不匹配msgtyp的第一个消息。

函数返回实际接收到的消息的字节数。如果出现错误,返回值为-1,并设置errno变量来指示具体的错误原因。

在使用msgrcv()函数时,需要确保传入有效的消息队列标识符(msqid),合适大小的接收缓冲区(msgp)以及正确的消息类型和标志。

4、int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

系统调用函数,用于向消息队列发送消息

参数msqid是消息队列的标识符,通过msgget()函数获得。

参数msgp是指向消息内容的指针,它指向一个消息结构体或其他形式的消息数据。

参数msgsz是消息的大小,表示要发送的消息的字节数。

参数msgflg是发送消息的标志,用于控制发送行为。常见的标志包括:

  • IPC_NOWAIT:如果消息队列已满,则立即返回-1,并将errno设置为EAGAIN
  • MSG_NOERROR:如果消息的大小大于消息队列的最大字节数,则截断消息而不引发错误。

函数返回值为0表示发送成功,返回-1表示发送失败,并设置errno变量来指示具体的错误原因。

在使用msgsnd()函数时,需要确保传入有效的消息队列标识符(msqid),正确填充消息内容(msgp)和消息大小(msgsz),以及适当的发送标志(msgflg)。

5、int msgctl(int msqid, int cmd, struct msqid_ds *buf);

系统调用函数,用于对消息队列进行控制操作,例如获取队列信息、设置队列属性、删除队列等

参数msqid是消息队列的标识符,通过msgget()函数获得。

参数cmd是控制命令,指定要执行的操作。常见的命令有:

  • IPC_STAT:获取消息队列的状态信息,将其存储在buf指向的结构体struct msqid_ds中。
  • IPC_SET:设置消息队列的状态,根据buf指向的结构体struct msqid_ds中的值进行设置。
  • IPC_RMID:删除消息队列,释放相关资源。

参数buf是一个指向struct msqid_ds结构体的指针,用于存储或传递消息队列的状态信息。

函数返回值为0表示操作成功,返回-1表示操作失败,并设置errno变量来指示具体的错误原因。

在使用msgctl()函数时,需要根据需要选择合适的命令(cmd),并提供有效的消息队列标识符(msqid)和相应的参数(如状态结构体buf)。例如,使用IPC_STAT命令可以获取消息队列的状态信息,使用IPC_RMID命令可以删除消息队列。

c、信号量

1、union semun

union semun 是一个联合体类型,用于信号量的操作。它在 <sys/sem.h> 头文件中定义。

联合体 semun 有以下成员:

  • int val:用于设置信号量的初始值或获取信号量的当前值。
  • struct semid_ds *buf:用于获取和设置信号量的状态信息。
  • unsigned short *array:用于获取和设置信号量集的每个信号量的值。

在使用 semctl() 系统调用时,需要使用 union semun 类型的变量来指定参数的值。具体使用哪个成员取决于要执行的操作和需要传递的数据。

 

在新版本的 Linux 中,union semun 结构已被移除,因此无法直接使用该结构。要解决这个问题,您可以自己定义一个类似的结构,如下所示:

 

 

2、struct sembuf

struct sembuf 是用于信号量操作的结构体,定义在 <sys/sem.h> 头文件中。

struct sembuf 结构体有以下成员:

  • short sem_num:表示要操作的信号量在信号量集中的索引,从 0 开始计数。
  • short sem_op:表示要执行的操作。它可以是正整数、负整数或零。
    • 如果 sem_op 大于 0,表示对信号量执行 V 操作(增加信号量的值)。
    • 如果 sem_op 小于 0,表示对信号量执行 P 操作(减少信号量的值)。
    • 如果 sem_op 等于 0,表示对信号量执行 Z 操作(等待信号量的值为 0)。
  • short sem_flg:用于指定操作的标志。常用的标志有:
    • SEM_UNDO:在进程退出时自动撤销操作,防止死锁。
    • IPC_NOWAIT:非阻塞操作,立即返回而不等待。

在进行信号量操作时,通常会使用一个 struct sembuf 结构体数组,可以对多个信号量进行批量操作。

 

 3、int semget(key_t key, int nsems, int semflg);

一个系统调用函数,用于创建或访问一个信号量集。

参数 key 是一个键值,用于唯一标识一个信号量集。通常可以使用 ftok() 函数生成键值。

参数 nsems 是信号量集中信号量的数量,即信号量集中包含的信号量个数。

参数 semflg 是标志,用于指定信号量集的权限和创建方式。可以通过使用位掩码或逻辑或运算符来组合多个标志。常用的标志有:

  • IPC_CREAT:如果信号量集不存在,则创建一个新的信号量集。如果信号量集已存在,则返回现有的标识符。
  • IPC_EXCL:与 IPC_CREAT 结合使用,如果信号量集已存在,则返回错误。
  • 0666:用于指定权限,表示读写权限为所有用户。

函数返回一个标识符,表示成功创建或访问的信号量集。如果发生错误,返回值为 -1,并设置 errno 变量来指示具体的错误原因。

 

4、int semop(int semid, struct sembuf *sops, size_t nsops);

一个系统调用函数,用于执行对信号量集进行操作。

参数 semid 是信号量集的标识符,通过 semget() 函数获得。

参数 sops 是一个指向 struct sembuf 结构体数组的指针,每个结构体描述了对一个信号量的操作。

参数 nsops 是操作的数量,即 sops 数组中结构体的个数。

函数返回值为 0 表示操作成功,返回 -1 表示操作失败,并设置 errno 变量来指示具体的错误原因。

在使用 semop() 函数时,需要根据需要构建适当的 struct sembuf 结构体数组,描述要对信号量集进行的操作。每个结构体指定了要操作的信号量的索引、操作类型和标志。

 

 5、 int semctl(int semid, int semnum, int cmd, ...);

一个系统调用函数,用于对信号量集进行控制操作。

参数 semid 是信号量集的标识符,通过 semget() 函数获得。

参数 semnum 是要操作的信号量在信号量集中的索引,从 0 开始。

参数 cmd 是要执行的控制命令,用于指定对信号量集的具体操作。常用的命令有:

  • SETVAL:设置信号量的初始值。
  • GETVAL:获取信号量的当前值。
  • IPC_RMID:从系统中删除信号量集。

参数 ... 是可选的其他参数,根据不同的命令需要提供不同的参数。

函数返回执行结果,具体取决于所执行的命令。如果发生错误,返回值为 -1,并设置 errno 变量来指示具体的错误原因。

d、共享内存

1、int shmget(key_t key, size_t size, int shmflg);

一个系统调用函数,用于创建或访问一个共享内存段。

参数 key 是一个键值,用于唯一标识一个共享内存段。通常可以使用 ftok() 函数生成键值。

参数 size 是共享内存段的大小,以字节为单位。

参数 shmflg 是标志,用于指定共享内存段的权限和创建方式。可以通过使用位掩码或逻辑或运算符来组合多个标志。常用的标志有:

  • IPC_CREAT:如果共享内存段不存在,则创建一个新的共享内存段。如果共享内存段已存在,则返回现有的标识符。
  • IPC_EXCL:与 IPC_CREAT 结合使用,如果共享内存段已存在,则返回错误。
  • 0666:用于指定权限,表示读写权限为所有用户。

函数返回一个标识符,表示成功创建或访问的共享内存段。如果发生错误,返回值为 -1,并设置 errno 变量来指示具体的错误原因。

 2、void *shmat(int shmid, const void *shmaddr, int shmflg);

一个系统调用函数,用于将共享内存段附加到进程的地址空间。

参数 shmid 是共享内存段的标识符,通过 shmget() 函数获得。

参数 shmaddr 是共享内存段附加到进程地址空间的首选地址。通常将其设置为 NULL,表示由系统选择合适的地址。

参数 shmflg 是标志,用于指定共享内存段的访问方式。常用的标志有:

  • SHM_RDONLY:只读访问共享内存段。
  • 0:读写访问共享内存段。

函数返回一个 void* 指针,指向共享内存段在进程地址空间中的起始地址。如果发生错误,返回值为 (void*)-1

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值