第 15 章 进程间通信

进程间通信

进程间的通信(InterProcess Communication,IPC)
常见的 IPC 方式有

1、半双工管道、FIFO
2、全双工管道、命名全双工管道
3、消息队列、信号量、共享内存
4、消息队列(实时)、信号量、共享存储(实时)
5、套接字、STREAMS

管道 pipe

管道是一种进程间通信机制,用于在两个进程间传递数据。可以实现一种单向、基于字节流的通信方式,即一个进程将数据写入管道的一段,另一个进程从管道的另一端读取数据

管道的分类

管道有两种类型:无名管道(Anonymous Pipes)和命名管道(Named Pipes)

1、无名管道(PIPE):无名管道是在进程间临时创建的,只能在创建它们的进程及其子进程之间进行通信。无名管道的创建是通过 pipe 系统调用来实现的,调用成功后会返回两个文件描述符,一个用于读取管道中的数据,一个用于写入数据到管道中。无名管道具有较小的缓冲区,一旦缓冲区已满,写入操作将被阻塞直到读取进程读取管道中的数据。

2、命名管道(FIFO):命名管道是在文件系统中创建的,多个进程可以通过打开同一个文件来进行通信。命名管道的创建是通过 mkfifo 系统调用来实现的,调用成功后会在文件系统中创建一个 FIFO 文件,进程可以使用 open 系统调用打开该文件并进行读写操作。命名管道具有较大的缓冲区,写入操作不会阻塞,直到缓冲区已满。

pipe 函数

#include <unistd.h>
/* On Alpha, IA-64, MIPS, SuperH, and SPARC/SPARC64; see NOTES */
struct fd_pair {
    long fd[2];
};
struct fd_pair pipe();
/* On all other architectures */
int pipe(int pipefd[2]);// 创建一个管道,成功返回0,pipefd[0]代表管道读端,pipefd[1]代表管道写端。

pipe2 函数

pipefd: 一个长度为 2 的整型数组,用来存储新创建的管道的读取端和写入端的文件描述符。

flags: 用来设置管道的属性,可以为以下值之一:

O_CLOEXEC: 设置管道文件描述符在执行 exec 时自动关闭。 O_DIRECT: 禁止使用缓存。 O_NONBLOCK: 将管道设置为非阻塞模式。

#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>
int pipe2(int pipefd[2], int flags);

pipe 创建背后流程

pipe 创建背后流程

管道样例

父进程向子进程发送内容的简单样例

#include <iostream>
#include <errno.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

#define BUFFSIZE 256

int main(int argc, char **argv)
{
    int pipefd[2];
    pid_t pid;
    char buffer[BUFFSIZE];
    if (pipe(pipefd) == -1)
    {
        cerr << "pipe error" << endl;
        exit(1);
    }
    pid = fork();
    if (pid == -1)
    {
        cerr << "fork error" << endl;
        exit(1);
    }
    if (pid == 0) // 子进程
    {
        close(pipefd[1]); // 关闭写端
        ssize_t count = read(pipefd[0], buffer, BUFFSIZE);
        if (count == -1)
        {
            cerr << "read error" << endl;
            exit(1);
        }
        cout << "child process read=> " << buffer << endl;
        close(pipefd[0]); // 关闭读端
        exit(0);          // 子进程退出
    }
    else // 父进程
    {
        close(pipefd[0]); // 关闭读端
        char message[] = "hello,child process!";
        ssize_t count = write(pipefd[1], message, strlen(message) + 1);
        if (count == -1)
        {
            cerr << "write error" << endl;
            exit(-1);
        }
        close(pipefd[1]); // 关闭写端
        cout << "father process write=> " << message << endl;
        pid_t retpid = wait(nullptr); // 等待子进程结束
        if (retpid == pid)
        {
            cout << "child process end" << endl;
        }
        cout << "father process end" << endl;
        exit(0);
    }
    return 0;
}
/*
gaowanlu@DESKTOP-QDLGRDB:/$ ./main
father process write=> hello,child process!
child process read=> hello,child process!
child process end
father process end
*/

popen 和 pclose 函数

popen 和 pclose 是 C 标准库中的两个函数,用于在程序中调用外部命令,并进行输入输出的操作。

函数原型

#include <stdio.h>
FILE *popen(const char *command, const char *mode);
int pclose(FILE *stream);

popen,command 参数为要执行的外部命令,mode 参数为打开文件的模式(读或写)。popen 函数会创建一个管道,执行指定的命令,并返回一个 FILE 类型的文件指针,可以使用标准的文件 I/O 函数(如 fread、fwrite、fgets、fputs 等)对该文件进行读写操作。如果 mode 参数为“r”,则该文件指针指向命令的标准输出,如果 mode 参数为“w”,则该文件指针指向命令的标准输入。

pclose,stream 参数为 popen 函数返回的文件指针。pclose 函数会等待命令执行完毕,并关闭管道。如果命令执行成功,pclose 函数返回命令的退出状态,否则返回-1。

简单样例

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <cstdio>
using namespace std;

int main(int argc, char **argv)
{
    FILE *fp;
    char buffer[1024];
    chdir("/");
    fp = popen("ls -l", "r");
    if (fp == nullptr)
    {
        cerr << "popen error" << endl;
        exit(1);
    }
    while (fgets(buffer, sizeof(buffer), fp))
    {
        cout << buffer;
    }
    int ret_state = pclose(fp);
    cout << "\nret_state=" << ret_state << endl;
    return 0;
}

如何用管道双向通信

当然可以在 fork 前创建两个管道,fork 后对两个管道规定读写的方向为相反,就可以实现父进程向子进程发数据同时也可以由子进程向父进程发送数据

协同进程

协同进程通常指一组进程通过协作的方式共同完成一个任务,出了在 fork 前预分配,也可以使用进程通信方式进程任务数据的分配等

FIFO

FIFO 是一种特殊的文件类型,也称为命名管道(named pipes),允许不同进程之间进行通信。与匿名管道不同,FIFO 在文件系统中有一个特定的文件名,因此可以由多个进程同时访问

创建 FIFO mkfifo

参数 pathname 指定 FIFO 的路径名,mode 指定 FIFO 的访问权限。创建 FIFO 后,可以像打开普通文件一样打开它。

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
//函数调用成功,则返回0,如果出现错误,则返回-1。

简单样例

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstring>
#include <errno.h>

#define PATH "/tmp/myfifo"

int main()
{
    if (mkfifo(PATH, 0666) == -1)
    {
        perror("create mkfifo error\n");
        if (errno == EEXIST)
        {
            printf(strerror(errno), "");
        }
        exit(EXIT_FAILURE);
    }
    return 0;
}

mkfifoat

在指定的文件夹下创建 FIFO

#include <fcntl.h>           /* Definition of AT_* constants */
#include <sys/stat.h>
int mkfifoat(int dirfd, const char *pathname, mode_t mode);

样例

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

int main() {
    int dirfd = open("/tmp", O_RDONLY);
    mkfifoat(dirfd, "myfifo", 0666);
    close(dirfd);
    return 0;
}

FIFO 样例

父子进程通信、当然可以是没有血缘关系的两个进程

#include <iostream>
#include <errno.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

#define BUFFSIZE 256

int main(int argc, char **argv)
{
    char buffer[BUFFSIZE];
    const char *path = "/tmp/tmpfifo";
    int ret = mkfifo(path, 0666);
    int fifo = open(path, O_RDWR);
    int pid = fork();
    if (pid == -1)
    {
        cerr << "fork error" << endl;
        exit(1);
    }
    if (pid == 0) // 子进程
    {
        ssize_t count = read(fifo, buffer, BUFFSIZE);
        if (count == -1)
        {
            cerr << "read error" << endl;
            exit(1);
        }
        cout << "child process read=> " << buffer << endl;
        close(fifo);
        exit(0); // 子进程退出
    }
    else // 父进程
    {
        char message[] = "hello,child process!";
        ssize_t count = write(fifo, message, strlen(message) + 1);
        if (count == -1)
        {
            cerr << "write error" << endl;
            exit(-1);
        }
        close(fifo);
        cout << "father process write=> " << message << endl;
        pid_t retpid = wait(nullptr); // 等待子进程结束
        if (retpid == pid)
        {
            cout << "child process end" << endl;
        }
        cout << "father process end" << endl;
        if (remove(path) == 0)
        {
            cout << "fifo delete success" << endl;
        }
        exit(0);
    }
    return 0;
}
/*
gaowanlu@DESKTOP-QDLGRDB:/$ ./main
father process write=> hello,child process!
child process read=> hello,child process!
child process end
father process end
*/

FIFO 注意事项

1、FIFO 是一种特殊类型的文件,需要像操作普通文件一样打开和关闭它。在打开 FIFO 时,需要指定相应的文件访问模式,例如只读、只写或读写模式。

2、FIFO 是一种半双工通信机制,即一个进程可以向管道写入数据,另一个进程可以从管道读取数据,但不能同时进行。因此,在使用 FIFO 进行通信时,需要分别创建读写进程,并确保它们在适当的时候打开和关闭 FIFO 文件描述符。

3、FIFO 是基于字节流的,它不保留消息的边界,因此在从 FIFO 中读取数据时,需要进行适当的数据处理,以确保正确地解析消息。

4、如果要使用多个 FIFO 进行通信,需要使用不同的 FIFO 文件名来避免冲突。建议使用唯一的、具有描述性的 FIFO 名称来避免混淆。

5、FIFO 是基于文件系统的,因此如果 FIFO 文件被删除或文件系统故障,通信将中断。在使用 FIFO 进行通信时,需要考虑这些情况,并采取适当的恢复措施。

管道的读写阻塞

在 Unix 和类 Unix 系统中,管道和 FIFO 的写操作和读操作都是阻塞的,也就是说,如果没有读进程读取管道中的数据,写进程将一直阻塞等待,直到有读进程读取数据为止。同样地,如果管道已满(即写进程写入数据的速度比读进程读取数据的速度快),写进程将一直阻塞等待,直到有足够的空间写入数据为止。

类似地,如果没有数据可读取,读进程将一直阻塞等待,直到有写进程写入数据为止。如果管道为空(即读进程读取数据的速度比写进程写入数据的速度快),读进程将一直阻塞等待,直到有数据可读取为止。

需要注意的是,管道和 FIFO 都是单向的,如果需要进行双向通信,需要创建两个管道或 FIFO。

XSI IPC

XSI IPC(X/Open System Interface Inter-Process Communication)是 UNIX 系统中一组标准的进程间通信(IPC)机制,由 X/Open 组织制定并发布。

XSI IPC 包括三种主要的 IPC 机制:消息队列(message queues)、信号量(semaphores)和共享内存(shared memory),它们可以在进程之间传递数据、同步进程和共享内存等。这些机制都提供了一些函数和系统调用,以供进程进行访问和操作。

相比于传统的 IPC 机制(如管道、套接字等),XSI IPC 提供了更高级、更强大和更灵活的 IPC 功能,可以实现更复杂的进程间通信和同步。它们还提供了一些可靠性和性能方面的优势,如消息队列可以在进程之间异步传递数据、信号量可以实现进程之间的同步、共享内存可以实现进程之间高效地共享数据等。

需要链接库

gcc -o program program.c -lrt -lipc
gaowanlu@DESKTOP-QDLGRDB:/$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems

XSI IPC 权限结构

在头文件、sys/ipc.h 中

ipc_perm 结构体中的这些信息可以用于控制对 IPC 对象的访问权限,从而保护 IPC 对象的安全性和可靠性。其中,key、uid、gid、cuid、cgid 和seq 字段是只读的,不能通过应用程序修改。mode 字段可以使用 IPC 对象创建函数的 mode 参数来指定,或者使用系统调用 chmod 修改 IPC 对象的权限。

struct ipc_perm{
    uid_t uid;//IPC对象所有者的有效用户ID
    gid_t gid;//IPC对象所有者的有效组ID
    uid_t cuid;//IPC对象创建者的有效用户ID
    gid_t cgid;//IPC对象创建者的有效组ID
    mode_t mode;//IPC对象的权限模式,由创建者指定
    key_t __key;//IPC对象的键值,由IPC对象创建函数(如msgget、semget、shmget)指定
    unsigned short __seq;//IPC对象的序列号,用于防止IPC对象的重复使用。
};

消息队列

消息队列(message queue)是一种 IPC 机制,用于在进程之间异步传递消息。消息队列提供了一种可靠的、异步的、有序的进程间通信方式,适用于需要传递大量数据或需要独立的进程间通信的应用程序。

Linux 系统中的消息队列通过 System V IPC 机制实现,包括三个主要的系统调用:msgget、msgsnd 和 msgrcv。

msqid_ds 结构

每一个队列都有一个 msqid_ds 结构与其关联 在头文件 sys/msg.h

struct msqid_ds {
    //消息队列的权限信息,包括UID、GID、权限位等
    struct ipc_perm msg_perm;   /* Ownership and permissions */
    //消息队列的最后一次发送时间
    time_t          msg_stime;  /* Time of last msgsnd(2) */
    //消息队列的最后一次接收时间
    time_t          msg_rtime;  /* Time of last msgrcv(2) */
    //消息队列的最后一次修改时间
    time_t          msg_ctime;  /* Time of last change */
    //消息队列当前的字节数
    unsigned long   __msg_cbytes;/* Current number of bytes in queue */
    //消息队列中当前的消息数量
    msgqnum_t       msg_qnum;   /* Current number of messages in queue */
    //消息队列的最大字节数
    msglen_t        msg_qbytes; /* Maximum number of bytes allowed in queue */
    //最后一个发送消息的进程ID
    pid_t           msg_lspid;  /* PID of last msgsnd(2) */
    //最后一个接收消息的进程ID
    pid_t           msg_lrpid;  /* PID of last msgrcv(2) */
};

ftok 函数

convert a pathname and a project identifier to a System V IPC key

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id)

特殊的 key_t

IPC_PRIVATE 用于创建一个新的 IPC 对象,例如新的共享内存区域、新的消息队列或新的信号量集等。通过指定 IPC_PRIVATE 标志符,可以保证创建的新 IPC 对象是唯一的,并且只能由当前进程及其子进程访问。

消息队列收发内容格式

mtype 是消息的类型,通常为正整数,用于在发送和接收消息时标识消息的种类。
mtext 是消息的内容,可以是任何数据类型的数据,实际大小可以超过 1 个字节,需要根据具体情况动态分配存储空间。

struct msgbuf {
    long mtype;      /* 消息类型 */
    char mtext[1];   /* 消息内容,实际长度可以超过 1 个字节 */
};

msgget 函数

get a System V message queue identifier,用于创建或获取一个消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
// If  msgflg  specifies both IPC_CREAT and IPC_EXCL and a message queue already exists for key, then msgget() fails with errno set to EEXIST.
// IPC_EXCL是System V IPC(包括消息队列、信号量、共享内存等)的一个标志位,用于创建IPC资源时控制是否允许创建已经存在的IPC对象

msgctl 函数

用于对消息队列进行控制操作

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

其中:

msqid 是消息队列标识符(ID),用于指定要操作的消息队列。
cmd 是操作命令,用于指定要执行的操作。常用的命令有:
IPC_STAT:获取消息队列的状态信息,将其存储在 buf 中。
IPC_SET:设置消息队列的状态信息,使用 buf 中的内容进行设置。
IPC_RMID:删除消息队列,释放其占用的资源。
buf 是 struct msqid_ds 结构体类型的指针,用于存储消息队列的状态信息。

msgctl() 函数的返回值为执行成功或失败的标志。若函数执行成功,返回值为 0;若函数执行失败,返回值为 -1,并设置相应的错误码。

msgsnd 函数

用于向消息队列发送消息

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

其中:

msqid 是消息队列标识符(ID),用于指定要发送消息的消息队列。
msgp 是指向要发送的消息的指针,类型为 struct msgbuf,包含了消息的类型和内容。
msgsz 是消息的大小,以字节为单位。
msgflg 是发送消息的选项,用于指定消息发送的行为。常用的选项有:
IPC_NOWAIT:如果消息队列已满,不阻塞,立即返回,并设置错误码 EAGAIN。
MSG_NOERROR:如果消息的大小大于消息队列的最大大小,截断消息并发送成功,不阻塞,不设置错误码。

msgsnd() 函数的返回值为执行成功或失败的标志。若函数执行成功,返回值为 0;若函数执行失败,返回值为 -1,并设置相应的错误码。

msgrcv 函数

用于从消息队列中接收消息

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

其中:

msqid 是消息队列标识符(ID),用于指定要接收消息的消息队列。
msgp 是指向接收消息的缓冲区的指针,类型为 struct msgbuf,用于存储接收到的消息的类型和内容。
msgsz 是缓冲区的大小,以字节为单位,表示可以接收的最大消息大小。
msgtyp 是要接收的消息类型。如果值为 0,则接收队列中的第一条消息;如果值大于 0,则接收队列中类型为该值的第一条消息;如果值小于 0,则接收队列中类型值小于或等于该值的绝对值的第一条消息。
msgflg 是接收消息的选项,用于指定接收消息的行为。常用的选项有:
IPC_NOWAIT:如果消息队列为空,不阻塞,立即返回,并设置错误码 ENOMSG。
MSG_NOERROR:如果消息的大小大于缓冲区的大小,截断消息并接收成功,不阻塞,不设置错误码。

msgrcv() 函数的返回值为接收到的消息的长度,即消息的实际大小(字节数)。若函数执行失败,返回值为 -1,并设置相应的错误码。

消息队列样例

父子进程通信样例

root@drecbb4udzdboiei-0626900:/mes/temp# ./main
Message queue created successfully.
child process send end
child process end
father process end
IPC_RMID
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <cstring>
#include <unistd.h>
using namespace std;

struct mymsgbuf
{
    long mtype;       /* 消息类型 */
    char mtext[1024]; /* 消息内容,实际长度可以超过 1 个字节 */
};

int main(int argc, char **argv)
{
    // 创建IPC键值
    key_t key = ftok("/tmp", 'a');
    if (key < 0)
    {
        perror("ftok");
        exit(EXIT_FAILURE);
    }
    // 创建消息队列
    int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msgid == -1) // 出错
    {
        if (errno == EEXIST)
        {
            printf("Message queue already exists.\n");
        }
        else
        {
            printf("Failed to create message queue: %s\n", strerror(errno));
            exit(1);
        }
    }
    printf("Message queue created successfully.\n");
    int pid = fork();
    if (pid == -1)
    {
        cerr << "fork error" << endl;
        exit(1);
    }
    // 子进程
    if (pid == 0)
    {
        struct mymsgbuf buffer;
        buffer.mtype = 0;
        strcpy(buffer.mtext, "hello world\n");
        int ret = msgsnd(msgid, &buffer, sizeof(buffer), IPC_NOWAIT);
        while (ret == -1 && errno == EAGAIN)
        {
            ret = msgsnd(msgid, &buffer, sizeof(buffer), IPC_NOWAIT);
        }
        cout << "child process send end" << endl;
        exit(0); // 子进程退出
    }
    else // 父进程
    {
        struct mymsgbuf buffer;
        ssize_t len = msgrcv(msgid, &buffer, sizeof(buffer) - sizeof(long), 0, IPC_NOWAIT);
        if (len == sizeof(buffer))
        {
            cout << "father process read :" << buffer.mtext << endl;
        }
        pid_t retpid = wait(nullptr); // 等待子进程结束
        if (retpid == pid)
        {
            cout << "child process end" << endl;
        }
        cout << "father process end" << endl;
        // 销毁消息队列
        struct msqid_ds msgds;
        if (0 == msgctl(msgid, IPC_RMID, &msgds))
        {
            cout << "IPC_RMID" << endl;
        }
        exit(0);
    }
    return 0;
}

信号量

信号量(Semaphore)是一种用于进程同步和互斥的机制,可以用来解决多个进程同时访问共享资源的问题,保证进程之间的互斥和同步。

在 Linux 中,信号量是由内核维护的一种计数器,它的值通常为非负整数。进程可以通过信号量来协调对共享资源的访问,以保证同一时刻只有一个进程能够访问该资源。进程可以通过原子操作来增加或减少信号量的值,并且可以阻塞等待信号量的值达到某个特定的值。

每个信号量由一个无名结构表示,至少包含下列成员

struct {
    unsigned short semval;//semaphore value,always >=0
    pid_t sempid;//pid for last operation
    unsigned short semncnt;//processes awaiting semval>curval
    unsigned short semzcnt;//processes awaiting semval==0
}

semid_ds 结构

该结构体定义在<sys/sem.h>头文件中

struct semid_ds {
    struct ipc_perm sem_perm;  /* Ownership and permissions */
    //最近一次semop操作的时间
    time_t sem_otime;          /* Last semop time */
    //最近一次修改信号量集的时间
    time_t sem_ctime;          /* Last change time */
    //信号量集中包含的信号量数量
    unsigned short sem_nsems;  /* No. of semaphores in set */
    ...
};

semget 函数

semget()是一个用于创建或获取一个信号量集的函数。该函数可以用于获取一个已经存在的信号量集或创建一个新的信号量集。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

其中,key 是信号量集的键值,用于标识信号量集的唯一性;nsems 是信号量集中包含的信号量个数;semflg 是用于指定信号量集的创建标志,它可以是 IPC_CREAT 表示创建信号量集,IPC_EXCL 表示创建时如果信号量已存在则返回错误(EEXIST),IPC_NOWAIT 表示不等待,直接返回。

semget()函数的返回值是一个信号量集标识符(semaphore set identifier),用于标识该信号量集。

semop 函数

semop()函数是一个用于对信号量进行操作的函数,它可以用于增加或减少信号量的值,或者等待信号量的值达到特定的条件等。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);

其中,semid 是信号量集标识符,用于标识要操作的信号量集;sops 是一个指向 sembuf 结构体的数组,其中每个 sembuf 结构体描述了一个要进行的信号量操作;nsops 表示 sops 数组中 sembuf 结构体的数量。

struct sembuf {
    unsigned short sem_num;  /* Semaphore number */
    short          sem_op;   /* Semaphore operation */
    short          sem_flg;  /* Operation flags IPC_NOWAIT、SEM_UNDO*/
};

其中

sem_num 表示要操作的信号量的编号,从 0 开始计数;
sem_op 表示要进行的操作,它可以是一个正整数、0、或一个负整数;当 sem_op 为正整数时,表示要将信号量的值增加 sem_op(指定 SEM_UNDO 则减去 sem_op);当 sem_op 为负整数时,表示要获取由该信号量控制的资源;当 sem_op 为 0 时,表示调用进程希望等待到信号量值变为 0; sem_flag 可选 IPC_NOWAIT 与 SEM_UNDO

semctl 函数

semctl()是一个用于控制信号量集的函数。它可以用于获取、设置、删除信号量集,以及对信号量进行操作等。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, .../*union semun arg*/);
union semun{
    int val;/*for SETVAL*/
    struct semid_ds *buf;/*for IPC_STAT and IPC_SET*/
    unsigned short *array;/*for GETALL and SETALL*/
    struct seminfo  *__buf;/* Buffer for IPC_INFO(Linux-specific)*/
}

其中,semid 是信号量集标识符,用于标识要控制的信号量集;semnum 是要操作的信号量编号,它从 0 开始计数;cmd 是要执行的操作命令,它可以是
IPC_RMID 表示删除信号量集
IPC_SET 表示设置信号量集的属性
IPC_STAT 表示获取信号量集的状态信息
GETVAL 返回成员 semnum 的 semval 值
SETVAL 设置成员 semnum 的 semval 值,值由 arg.val 指定;
GETPID 返回成员 semnum 的 sempid 值
GETNCNT 返回成员 semnum 的 semncnt 值
GETZCNT 返回成员 semnum 的 semzcnt 值
GETALL 返回成员 semnum 的 sempid 值,值存储在 arg.array 指向的数组中
SETALL 将集合中所有信号量值设置为 arg.array 指向的数组中的值
第四个参数是根据操作命令不同而不同的。

信号量样例

父进程“P”,子进程“V”

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <cstring>
#include <unistd.h>
using namespace std;

union semun
{
    int val;               /*for SETVAL*/
    struct semid_ds *buf;  /*for IPC_STAT and IPC_SET*/
    unsigned short *array; /*for GETALL and SETALL*/
    struct seminfo *__buf; /* Buffer for IPC_INFO(Linux-specific)*/
};

int main(int argc, char **argv)
{
    // 创建信号量集
    int sems_id = semget(IPC_PRIVATE, 1, IPC_CREAT | IPC_EXCL | 0666);
    if (sems_id == -1)
    {
        if (errno != EEXIST)
        {
            cerr << strerror(errno) << endl;
            exit(1);
        }
    }
    // 初始化信号量
    union semun m_semun;
    m_semun.val = 0;
    if (-1 == semctl(sems_id, 0, SETVAL, m_semun))
    {
        cerr << strerror(errno) << endl;
        exit(1);
    }
    int pid = fork();
    if (pid == -1)
    {
        cerr << "fork error" << endl;
        exit(1);
    }
    // 子进程
    if (pid == 0)
    {
        for (int i = 0; i < 10; i++) // 消耗10个资源
        {
            struct sembuf m_sembuf[1];
            m_sembuf[0].sem_num = 0;
            m_sembuf[0].sem_op = -1; // 要1个 "P"操作
            m_sembuf[0].sem_flg = 0; // 默认等待
            int ret = semop(sems_id, m_sembuf, 1);
            while (ret == -1 && errno == EINTR)
            {
                ret = semop(sems_id, m_sembuf, 1);
            }
            cout << "child " << i << endl;
        }
        exit(0); // 子进程退出
    }
    else // 父进程
    {
        for (int i = 0; i < 10; i++)
        {
            struct sembuf m_sembuf[1];
            m_sembuf[0].sem_num = 0;
            m_sembuf[0].sem_op = 1;  // 加1个 "V"操作
            m_sembuf[0].sem_flg = 0; // 默认等待
            int ret = semop(sems_id, m_sembuf, 1);
            while (ret == -1 && errno == EINTR)
            {
                ret = semop(sems_id, m_sembuf, 1);
            }
            cout << "father " << i << endl;
            sleep(1);
        }
        // 等待子进程
        int ret_pid = wait(nullptr);
        // 删除信号量集
        if (0 == semctl(sems_id, 0, IPC_RMID))
        {
            cout << "delete sem success" << endl;
        }
        exit(0);
    }
    return 0;
}

共享内存

共享内存是一种允许不同进程之间共享内存区域的 IPC 机制。它比其他 IPC 机制(如管道、消息队列)更高效,因为它避免了数据复制的开销。

在 Linux 中,可以使用 System V 共享内存和 POSIX 共享内存两种机制来实现共享内存。

使用 System V 共享内存的步骤:

1、使用 shmget 函数创建共享内存,该函数返回一个共享内存标识符,需要指定共享内存的大小和访问权限等参数
2、使用 shmat 函数将共享内存映射到进程的地址空间中,该函数返回一个指向共享内存区域的指针
3、在进程间进行共享内存访问时,需要使用同步机制来避免并发访问导致的数据不一致问题,可以使用信号量或互斥锁等机制来实现同步
4、在进程不需要使用共享内存时,需要使用 shmdt 函数将共享内存从进程的地址空间中分离
5、所有进程都不需要共享内存时,需要使用 shmctl 函数删除共享内存

内核为每个共享内存维护着一个结构,该结构至少为每个共享存储段包含以下成员

struct shmid_ds {
    struct ipc_perm shm_perm;   // 共享内存的权限信息
    size_t          shm_segsz; // 共享内存段的大小
    time_t          shm_atime; // 上次访问时间
    time_t          shm_dtime; // 上次分离时间
    time_t          shm_ctime; // 上次变更时间
    pid_t           shm_cpid;  // 创建进程的PID
    pid_t           shm_lpid;  // 最后一个操作进程的PID
    shmatt_t        shm_nattch;// 当前连接到该共享内存的进程数
    ...
};

shmget 函数

shmget 是一个在 Linux 系统下用于创建或获取一个共享内存段的函数,不同的进程可以通过使用相同的键值来访问同一个共享内存段。

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

其中,key 参数是一个用于标识共享内存段的键值,size 参数是共享内存段的大小(以字节为单位),shmflg 参数是一组标志位,可以用于指定一些操作选项。该函数的返回值是一个用于标识共享内存段的 ID,如果出现错误则返回-1。

1、如果指定的键值已经存在对应的共享内存段,则该函数返回该共享内存段的 ID。
2、如果指定的键值不存在对应的共享内存段,并且指定了 IPC_CREAT 标志,则该函数会创建一个新的共享内存段,并返回其 ID。
3、如果指定的键值不存在对应的共享内存段,并且没有指定 IPC_CREAT 标志,则该函数返回-1。

shmctl 函数

shmctl 函数是一个在 Linux 系统下用于控制共享内存段的函数,可以用于删除共享内存段、获取或修改共享内存段的状态等操作

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

其中,shmid 参数是共享内存段的 ID,cmd 参数是一个控制命令,用于指定具体的操作,buf 参数是一个指向 shmid_ds 结构体的指针,用于获取或修改共享内存段的状态信息。该函数的返回值表示操作是否成功,如果出现错误则返回-1。

shmctl 函数支持的控制命令包括以下几种:

IPC_STAT:获取共享内存段的状态信息,并将其保存到 buf 参数指向的结构体中。
IPC_SET:修改共享内存段的状态信息,修改内容存储在 buf 参数指向的结构体中。
IPC_RMID:删除共享内存段。
(Linux 和 Solaris 特有的)
SHM_LOCK 在内存中对共享存储段加锁、此命令只能超级用户执行
SHM_UNLOCK 解锁共享存储段,只能超级用户执行

删除共享内存段会导致所有连接到该共享内存段的进程都与其分离,并且无法再次连接。因此,在删除共享内存段之前,需要确保所有进程都不再需要访问该共享内存段。另外,只有拥有共享内存段的创建者或超级用户才有权限删除共享内存段。

shmat 函数

shmat 函数是一个在 Linux 系统下用于连接到一个共享内存段的函数

#include <sys/types.h>
#include <sys/shm.h>                         //SHM_EXEC SHM_RDONLY SHM_REMAP SHM_RND (Linux-specific)
void *shmat(int shmid, const void *shmaddr, int shmflg);

其中,shmid 参数是共享内存段的 ID,shmaddr 参数通常为 NULL,表示将共享内存段连接到当前进程的地址空间中,shmflg 参数可以指定一些选项标志。该函数的返回值是一个指向共享内存段起始地址的指针,如果出现错误则返回-1。

shmdt 函数

shmdt 函数是一个在 Linux 系统下用于分离一个共享内存段的函数

#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);

其中,shmaddr 参数是共享内存段的起始地址,该函数的返回值为 0 表示成功,返回-1 表示失败。

共享内存样例

父子进程共享内存

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <unistd.h>
using namespace std;

int main(int argc, char **argv)
{
    // 创建共享内存
    int shm_id = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);
    if (shm_id == -1)
    {
        if (errno != EEXIST)
        {
            cerr << strerror(errno) << endl;
            exit(1);
        }
    }
    int pid = fork();
    if (pid == -1)
    {
        cerr << "fork error" << endl;
        exit(1);
    }
    // 子进程
    if (pid == 0)
    {
        // 链接共享内存
        char *ptr = (char *)shmat(shm_id, nullptr, 0);
        if (ptr == (char *)-1)
        {
            if (errno == EINVAL)
            {
                printf("shmat error: invalid argument\n");
            }
            else if (errno == EACCES)
            {
                printf("shmat error: permission denied\n");
            }
            else if (errno == ENOMEM)
            {
                printf("shmat error: no memory available\n");
            }
            exit(1);
        }
        sleep(5);
        cout << ptr << endl;
        shmdt(ptr);
        exit(0); // 子进程退出
    }
    else // 父进程
    {
        // 链接共享内存
        char *ptr = (char *)shmat(shm_id, nullptr, 0);
        if (ptr == (char *)-1)
        {
            if (errno == EINVAL)
            {
                printf("shmat error: invalid argument\n");
            }
            else if (errno == EACCES)
            {
                printf("shmat error: permission denied\n");
            }
            else if (errno == ENOMEM)
            {
                printf("shmat error: no memory available\n");
            }
            exit(1);
        }
        strcpy(ptr, "hello world!");
        // 等待子进程
        int ret_pid = wait(nullptr);
        shmdt(ptr);
        if (0 == shmctl(shm_id, IPC_RMID, nullptr))
        {
            cout << "IPC_RMID" << endl;
        }
        exit(0);
    }
    return 0;
}
/*
gaowanlu@DESKTOP-QDLGRDB:/$ ./main
hello world!
IPC_RMID
*/

POSIX 信号量

POSIX 信号量是由 IEEE POSIX 标准定义的、而 XSI 信号量则是由 System V IPC(Inter-Process Communication)定义的。POSIX 信号量和 XSI 信号量虽然在一些实现上有所不同,但是它们都可以用于进程间同步和互斥,可以根据实际需要选择使用哪种信号量。

sem_open 函数

用于创建或打开一个命名的 POSIX 信号量的函数

#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,
                mode_t mode, unsigned int value);
Link with -pthread.

参数说明:

name:信号量的名称。该名称应该以斜杠/开头,用于区分该信号量与其他类型的文件或对象。
oflag:打开信号量的方式。可以使用 O_CREAT、O_EXCL、O_RDWR 等标志位,来指定信号量的创建方式或访问权限等。
mode:信号量的访问权限。当 oflag 中包含 O_CREAT 标志位时,需要指定该信号量的权限。这里的权限和文件系统的权限类似,可以用八进制数表示,如 0666 表示所有用户都可以读写该信号量。
value:信号量的初始值。新创建的信号量初始值为 value,已经存在的信号量忽略该值。
该函数返回一个 sem_t 类型的指针,指向创建或打开的信号量。如果创建或打开信号量失败,则返回 SEM_FAILED 宏。

sem_close 函数

用于关闭已经打开的命名的 POSIX 信号量的函数

#include <semaphore.h>
int sem_close(sem_t *sem);
Link with -pthread.

sem:指向已经打开的信号量的指针。
该函数返回一个整型值,表示函数执行的结果。如果函数执行成功,则返回 0,否则返回-1,并设置 errno 变量来指示错误的原因。

用于删除已经创建的命名 POSIX 信号量的函数

#include <semaphore.h>
int sem_unlink(const char *name);
//Link with -pthread.

name:指向信号量名称的指针。该名称应该以斜杠/开头,用于区分该信号量与其他类型的文件或对象。
该函数返回一个整型值,表示函数执行的结果。如果函数执行成功,则返回 0,否则返回-1,并设置 errno 变量来指示错误的原因。

需要注意的是,删除信号量的操作是不可逆的,一旦删除了信号量,就无法再使用该信号量。因此,在调用 sem_unlink()函数之前,应该确保不会再使用该信号量,避免意外删除的问题。

sem_trywait、sem_wait、sem_timedwait 函数

P 操作的函数

#include <semaphore.h>
int sem_wait(sem_t *sem);//阻塞
int sem_trywait(sem_t *sem);//非阻塞
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
//Link with -pthread.

如果函数执行成功,则返回 0,否则返回-1,并设置 errno 变量来指示错误的原因。

sem_post 函数

用于对已经打开的信号量进行 V 操作的函数

#include <semaphore.h>
int sem_post(sem_t *sem);
//Link with -pthread.

sem:指向需要进行 V 操作的信号量的指针。
该函数返回一个整型值,表示函数执行的结果。如果函数执行成功,则返回 0,否则返回-1,并设置 errno 变量来指示错误的原因。

sem_init 函数

用于初始化未命名的信号量的函数,用于设置信号量的初值,sem_init 创建的由 sem_destroy 函数销毁

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
//Link with -pthread.

sem:指向需要进行初始化的信号量的指针。
pshared:一个整型值,指示信号量是否可以在多个进程之间共享。如果该值为 0,则表示信号量仅能在同一进程内的线程之间共享;否则,表示信号量可以在多个进程之间共享。
value:一个无符号整型值,表示信号量的初值。
该函数返回一个整型值,表示函数执行的结果。如果函数执行成功,则返回 0,否则返回-1,并设置 errno 变量来指示错误的原因。

sem_destroy 函数

用于销毁未命名的信号量(sem_init 创建)的函数

#include <semaphore.h>
int sem_destroy(sem_t *sem);
//Link with -pthread.

sem:指向需要进行销毁的信号量的指针。
该函数返回一个整型值,表示函数执行的结果。如果函数执行成功,则返回 0,否则返回-1,并设置 errno 变量来指示错误的原因。

sem_getvalue 函数

用于获取一个信号量的值

#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
//Link with -pthread.

sem:指向要获取值的信号量的指针。
sval:指向整数的指针,用于存储获取到的信号量的值。

POSIX 消息队列

POSIX 消息队列发送的消息可以是任意大小的数据块,而 XSI 消息队列发送的消息必须是一个结构体类型,其中包含一个消息类型和一个数据块。

消息队列由内核维护,可以使用 mq_open() 函数打开或创建一个消息队列。使用 mq_send() 函数向队列中发送消息,使用 mq_receive() 函数从队列中接收消息。可以使用 mq_close() 函数关闭一个已打开的消息队列,使用 mq_unlink() 函数删除一个已创建的消息队列。

mq_open 函数

打开或创建一个消息队列

#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <mqueue.h>
mqd_t mq_open(const char *name, int oflag);
mqd_t mq_open(const char *name, int oflag, mode_t mode,
                     struct mq_attr *attr);
struct mq_attr {
    long mq_flags;       /* Flags (ignored for mq_open()) */
    long mq_maxmsg;      /* Max. # of messages on queue */
    long mq_msgsize;     /* Max. message size (bytes) */
    long mq_curmsgs;     /* # of messages currently in queue
                            (ignored for mq_open()) */
};

参数说明:

name:消息队列的名称,可以以斜杠 / 开头表示绝对路径,也可以不以 / 开头表示相对路径。
oflag:打开标志,如 O_RDONLY、O_WRONLY、O_CREAT、O_EXCL 等。 mode:创建时的访问权限。
attr:队列属性,如队列大小、队列中最大消息数等。如果为 NULL,则使用默认值。

返回值说明:

成功:返回一个消息队列描述符。
失败:返回 -1,并设置相应的错误码。

mq_send 函数

向消息队列中发送一条消息

#include <mqueue.h>
int mq_send(mqd_t mqdes, const char *msg_ptr,
              size_t msg_len, unsigned int msg_prio);
#include <time.h>
#include <mqueue.h>
int mq_timedsend(mqd_t mqdes, const char *msg_ptr,
              size_t msg_len, unsigned int msg_prio,
              const struct timespec *abs_timeout);
//Link with -lrt.

参数说明:

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

返回值说明:

成功:返回 0。
失败:返回 -1,并设置相应的错误码。

mq_receive 函数

从消息队列中接收一条消息

#include <mqueue.h>
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr,
                          size_t msg_len, unsigned int *msg_prio);
#include <time.h>
#include <mqueue.h>
ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr,
                  size_t msg_len, unsigned int *msg_prio,
                  const struct timespec *abs_timeout);
//Link with -lrt.

参数说明:

mqdes:消息队列的描述符。
msg_ptr:指向接收缓冲区的指针。
msg_len:接收缓冲区的大小。
msg_prio:指向存储消息优先级的变量的指针。如果为 NULL,则忽略消息优先级。

返回值说明:

成功:返回接收到的消息的字节数。
失败:返回 -1,并设置相应的错误码。

mq_close 函数

关闭一个已打开的消息队列

#include <mqueue.h>
int mq_close(mqd_t mqdes);
//Link with -lrt.

参数说明:

mqdes:要关闭的消息队列的描述符。

返回值说明:

成功:返回 0。
失败:返回 -1,并设置相应的错误码。

删除一个已经命名的消息队列

#include <mqueue.h>
int mq_unlink(const char *name);
//Link with -lrt.

参数说明:

name:要删除的消息队列的名称。

返回值说明:

成功:返回 0。
失败:返回 -1,并设置相应的错误码。

POSIX 共享内存

POSIX 共享内存是一种高效的进程间通信方式,它允许多个进程共享同一块物理内存,以便在进程之间传递数据。POSIX 共享内存由内核管理,允许多个进程通过将同一内存区域映射到各自的虚拟地址空间来共享数据,这些进程可以同时读取和写入共享内存中的数据,从而达到高效通信的目的。

与传统的 System V 共享内存相比,POSIX 共享内存提供了更加灵活的 API,同时具有更好的可移植性和可靠性。

$man  shm_overview
#include <sys/mman.h>
#include <sys/stat.h>        /* For mode constants */
#include <fcntl.h>           /* For O_* constants */
//打开或创建一个共享内存区域 可使用close关闭
int shm_open(const char *name, int oflag, mode_t mode);
//删除一个共享内存区域
int shm_unlink(const char *name);
//Link with -lrt.

参数说明:

name:共享内存的名称。
oflag:打开标志,如 O_RDONLY、O_RDWR、O_CREAT、O_EXCL 等。
mode:创建时的访问权限。

返回值说明:

成功:返回一个共享内存描述符。
失败:返回 -1,并设置相应的错误码。

ftruncate 函数

调整共享内存区域的大小

#include <unistd.h>
#include <sys/types.h>
int ftruncate(int fd, off_t length);

参数说明:

fd:共享内存的描述符。
length:要调整的共享内存区域的大小。

返回值说明:

成功:返回 0。
失败:返回 -1,并设置相应的错误码。

mmap、munmap 函数

将共享内存区域映射到进程的虚拟地址空间

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
          int fd, off_t offset);
int munmap(void *addr, size_t length);

mmap 参数说明:

addr:指定映射的起始地址。如果为 NULL,则由系统自动分配一个地址。 length:映射的长度。
prot:指定映射区域的保护方式,如 PROT_READ、PROT_WRITE 等。
flags:指定映射区域的特性,如 MAP_SHARED 表示共享内存。
fd:共享内存的描述符。
offset:从文件头开始的偏移量。

mmap 返回值说明:

成功:返回映射区域的起始地址。
失败:返回 MAP_FAILED,并设置相应的错误码。

munmap 参数说明:

addr:要取消映射的起始地址。
length:要取消映射的长度。

munmap 返回值说明:

成功:返回 0。
失败:返回 -1,并设置相应的错误码。