Linux系统编程(三) --进程间通信

本文详细介绍了Linux进程间通信(IPC)的原理、类别,重点讲解了无名管道、有名管道、SystemV共享内存、消息队列和信号量的使用方法,以及生产者消费者模型的应用。通过实例演示了如何在不同进程间进行数据交换和同步协调。
摘要由CSDN通过智能技术生成

1 进程间通信总览

进程间通信,IPC(Inter Process Communication),指不同进程间的交流,就是进程之间发送和接收数据。

1.1 进程间如何通信

进程的 0-3GB 空间是互不相干的,3GB-4GB 是内核空间,所有进程间共带。下图说明了进程空间的独立性,内核空间的共享性。内核的共享特性,给进程的通信带来了可能。

请添加图片描述

进程的用户空间与内核空间

实验:进程 A 从标准输入读取字符,然后“发送给”进程 B,进程 B 接收到数据后,将字符中的小写转换成大写后打印到屏幕。

方案:

  • 进程 A 创建一个文件 tmp,并向 tmp 写入数据。
  • 进程 A 写完数据后关闭 tmp,并向进程 B 发送信号 SIGUSR1。
  • 进程 B 接收到信号后,知道进程 A 已经写完数据,于是打开文件 tmp 读取数据。
  • 进程 B 读取完数据后关闭 tmp 文件,并把 tmp 文件删除。
  • 进程 B 把读取到的数据中的字符全部转换成大写打印到屏幕。
// sender.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {
    // 要想发送信号,必须知道另一个进程的进程 id 号,所以这里通过参数将进程 id 传进来
    if (argc < 2) {
        printf("usage: %s <pid>", argv[0]);
        return 1;
    }

    pid_t pid = atoi(argv[1]);
    char buf[64] = { 0 };
    int n = 0;
    while (1) {
        // 从标准输入中读取数据,并写到文件中
        if ((n = read(STDIN_FILENO, buf, 64)) > 0) {
            int fd = open("tmp", O_WRONLY | O_CREAT | O_EXCL, 0664);
            if (fd < 0) {
                perror("open");
                continue;
            }
            write(fd, buf, n);
            // 写完数据后,向接收进程发送 SIGUSR1 信号
            close(fd);
            if (kill(pid, SIGUSR1) < 0) {
                perror("kill");
            }
            // 如果用户输入 q,就关闭程序
            if (*buf == 'q') return 0;
        }
    }
    return 0;
}
// recver.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

// 信号处理函数,从文件中读取数据,并转换成大写打印到屏幕
void handler(int sig) {
    char buf[64];
    int i;
    int fd = open("tmp", O_RDONLY);
    if (fd < 0) {
        perror("open");
        return;
    }

    int n = 0;

    if ((n = read(fd, buf, 64)) < 0) {
        perror("read");
        close(fd);
        return;
    }
    close(fd);
    unlink("tmp"); // 读取完成后将文件删除
    for (i = 0; i < n; ++i)
        putchar(toupper(buf[i])); // 将数据转换成大写并打印到屏幕。toupper 是 C 库函数,声明于 ctype.h 文件中

    if (*buf == 'q') exit(0); // 如果收到的数据以 q 开头就退出
}

int main() {
    printf("I'm %d\n", getpid());

    // 注册 SIGUSR1 信号
    struct sigaction act;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    if (sigaction(SIGUSR1, &act, NULL) < 0) {
        perror("sigaction");
        return 1;
    }

    // main 函数进入休眠
    while (1) pause();
}

编译,运行

$ gcc sender.c -o sender
$ gcc recver.c -o recver
$ ./sender 34803

再开启另一个终端,执行

$ ./recver

请添加图片描述

1.2 Linux IPC 分类

分为四类:

  1. 最初的 Unix IPC:包括无名管道,有名管道,信号。
  2. System V IPC:包括 System V 共享内存、System V 消息队列、System V 信号量。
  3. 基于 socket IPC:主要使用套接字的方式进行通信。
  4. POSIX IPC:POSIX 共享内存、POSIX 消息队列、POSIX 信号量。

1.3 Linux IPC常用手段

  1. 无名管道(pipe)、有名管道(named pipe):无名管道只能用于有亲缘关系(父子进程)的进程,有名管道用于任意两进程间通信。
  2. 信号(signal)。
  3. 消息(message)队列:包括Posix消息队列system V消息队列。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  4. 共享内存(share memory):多个进程可以访问同一块内存空间,是最快的可用IPC形式,效率高。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
  5. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
  6. 套接口(Socket):用于不同机器之间的进程间通信。

2 无名管道

本质上,pipe 函数会在进程内核空间申请一块内存(比如一个内存页,一般是 4KB),然后把这块内存当成一个先进先出(FIFO)的循环队列来存取数据。管道位于进程内核空间

无名管道和文件确实很像,也有描述符。但它不是普通的本地文件,而是一种抽象的存在。

2.1 pipe 函数

int pipe(int pipefd[2]);

成功返回0,失败返回-1。

pipefd[0] 用于读,而 pipefd[1] 用于写。

用于读写的描述符必须同时打开。

  1. 如果关闭读 (close(pipefd[0])) 端保留写端,继续向写端 (pipefd[1]) 端写数据的进程会收到 SIGPIPE 信号。
  2. 如果关闭写 (close(pipefd[1])) 端保留读端,继续向读端 (pipefd[0]) 端读数据(read 函数),read 函数会返回0。

2.2 用pipe进行进程间通信

用 pipe 打开两个描述符后,fork一个子进程。这样,子进程也会继承这两个描述符,描述符引用计数变成 2。

父进程向子进程发送数据:

父进程关闭pipefd[0](读端),子进程关闭pipefd[1]写端。

步骤一:fork 子进程

请添加图片描述

fork 后的半双工管道

步骤二:父进程读端,子进程关闭写端

请添加图片描述

从父进程到子进程的管道

实验

进程 fork 出一个子进程,通过无名管道向子进程发送字符,子进程收到数据后将字符串中的小写字符转换成大写并输出。

// hellopipe.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

void child(int* fd) {
    close(fd[1]); // 子进程关闭写端
    char buf[64];
    int n = 0, i;

    while (1) {
        n = read(fd[0], buf, 64); // 如果没有数据可读,read 会阻塞;如果父进程退出,read 返回 0.
        for (i = 0; i < n; ++i) putchar(toupper(buf[i]));
        if (*buf == 'q') {
            close(fd[0]);
            exit(0);
        }
        if (n == 0) {
            puts("no data to read!");
            sleep(1);
        }
    }
    exit(0);
}

int main() {
    int fd[2];
    int n = 0;
    char buf[64] = { 0 };
    if (pipe(fd) < 0) {
        perror("pipe");
        return -1;
    }

    pid_t pid = fork();
    if (pid == 0) {
        child(fd);
    }

    close(fd[0]);// 父进程关闭读端
    while (1) {
        n = read(STDIN_FILENO, buf, 64);
        write(fd[1], buf, n);
        if (*buf == 'q') {
            close(fd[1]);
            exit(0);
        }
    }

    return 0;
}

3 有名管道

有名管道有一个实实在在的FIFO类型的文件。只要不同的进程打开 FIFO 文件,就可以彼此通信。

3.1 创建 FIFO 类型文件

通过命令 mkfifo 创建,如 mkfifo hello

通过函数 mkfifo(3) 创建

int mkfifo(const char *pathname, mode_t mode);

该函数返回 0 表示成功,-1 失败。

比如:mkfifo(“hello”, 0664);

hello文件信息如下:

prw-r--r--  1 skx skx    0 9月  27 07:53 hello|

3.2 FIFO文件特性

  1. 文件类型是p,代表管道
  2. 文件大小是0
  3. fifo 文件需要有读写两端,否则在打开fifo文件时会阻塞。

实验:使用 cat 命令打印 hello 文件内容

$ cat hello

接下来你的 cat 命令被阻塞住。

开启另一个终端,执行:

$ echo "hello world" > hello

然后你会看到被阻塞的 cat 又继续执行完毕,在屏幕打印 “hello world”。如果你反过来执行上面两个命令,会发现先执行的那个总是被阻塞。

实验

下面有两个程序,分别是发送端 send 和接收端面 recv。程序 send 从标准输入接收字符,并发送到程序 recv,同时 recv 将接收到的字符打印到屏幕。

发送端

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

int main() {
    char buf[64];
    int n = 0;
    // 有名管道文件不存在则创建
    if (access("/home/skx/pra/learn_linux/52/hello", 0)) {
        if ((n = mkfifo("hello", 0664)) < 0)
            perror("mkfifo");
    }

    int fd = open("hello", O_WRONLY);
    if (fd < 0) { perror("open fifo"); return -1; }
    puts("has opend fifo");

    while ((n = read(STDIN_FILENO, buf, 64)) > 0) {
        write(fd, buf, n);
        if (buf[0] == 'q') break;
    }

    close(fd);

    return 0;
}

接收端

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

int main() {
    char buf[64];
    int n = 0;
    // 有名管道文件不存在则创建
    if (access("/home/skx/pra/learn_linux/52/hello", 0)) {
        if ((n = mkfifo("hello", 0664)) < 0)
            perror("mkfifo");
    }

    int fd = open("hello", O_RDONLY);
    if (fd < 0) { perror("open fifo"); return -1; }
    puts("has opened fifo");

    while ((n = read(fd, buf, 64)) > 0) {
        write(STDOUT_FILENO, buf, n);
    }

    if (n == 0) {
        puts("remote closed");
    }
    else {
        perror("read fifo");
        return -1;
    }

    close(fd);

    return 0;
}

编译运行

$ gcc send.c -o send
$ gcc recv.c -o recv

运行

$ ./send

因为 recv 端还没打开 hello 文件,这时候 send 是阻塞状态的。

再开启另一个终端:

$ ./recv

这时候 send 端和 recv 端都在终端显示has opend fifo

此时在 send 端输入数据,recv 打印。

请添加图片描述

4 System V共享内存

4.1 共享内存

方法如下:

  1. 根据已知的键(key) 使用 get 函数获取或者创建内核对象,并且返回内核对象的 id 号。
  2. 根据 id 号获取内存地址。
  3. 向内存读写数据。

内核对象,理解为位于内核空间中的结构体。

键值是事先约定好的。get函数是以get为后缀的函数名,共享内存里对应的函数为shmget,shm意思为share memory。

根据key获取的id 号,唯一的标识了内核中的一个对象。对于共享内存,可以用此id将内核对象中的内存挂接到用户空间的线性地址。

实验:该实例有两个程序,程序 a 创建一个共享内存的内核对象,获取内存后,向其写入数据。程序 b 获取此内核对象后挂接内存,读取数据然后打印。

  • 程序 a
// a.c
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>

int main() {
    // shmget 函数通过事先约定的键值 0x8888 创建(IPC_CREAT)一个内核对象并返回其 id。如果 0x8888 对应的内核对象存在,就失败(IPC_EXCL)。0664 是该内核对象的权限。第二个参数表示创建的共享内存大小。
    int id = shmget(0x8888, 4096, IPC_CREAT | IPC_EXCL | 0664);
    // 如果失败就退出
    if (id < 0) {
        perror("shmget");
        return -1;
    }

    // 打印获取到的内核对象 id
    printf("id = %d\n", id);

    // 使用函数 shmat (share memory attach) 将内核对象维护的内存挂接到指定线性地址(第二个参数)
    // 如果第二个参数为 0,则系统帮你选择一个合适的线性地址。
    char* buf = shmat(id, NULL, 0);

    // 如果挂接失败就退出
    if (buf == (char*)-1) {
        perror("shmat");
        return -1;
    }

    // 将数据拷贝到共享内存
    strcpy(buf, "hello, share memory!\n");

    // 使用 shmdt(share memory detach) 将挂接的内存卸载
    if (shmdt(buf) < 0) {
        perror("shmdt");
        return -1;
    }

    return 0;
}
  • 程序b
// b.c
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>

int main() {
    // 根据事先约定后的键值获取内核对象 id,这时候后面两个参数都可以为 0.
    int id = shmget(0x8888, 0, 0);
    if (id < 0) {
        perror("shmget");
        return -1;
    }

    printf("id = %d\n", id);

    // 挂接内存
    char* buf = shmat(id, NULL, 0);

    if (buf == (char*)-1) {
        perror("shmat");
        return -1;
    }

    // 打印数据
    printf("%s", buf);

    // 卸载
    if (shmdt(buf) < 0) {
        perror("shmdt");
        return -1;
    }

    // 删除内核对象
    if (shmctl(id, IPC_RMID, NULL) < 0) {
        perror("shmctl");
        return -1;
    }

    return 0;
}

编译

$ gcc a.c -o a
$ gcc b.c -o b

运行

$ ./a

如果你的程序运行成功,会在屏幕打印内核对象的 id 号。另外通过命令ipcs -m 可以看到刚刚创建的共享内存内核对象。

请添加图片描述

  • 运行程序 b
$ ./b

屏幕打印如下结果:

请添加图片描述

4.2 IPC 内核对象

IPC 内核对象都是位于内核空间中的一个结构体。使用get函数创建内核对象后,内核开辟一块内存。只要你不删除,就永远在内核中。

请添加图片描述

IPC 内核对象示意图

4.3 获取内核对象的id号

为了能够得到内核对象的 id 号,用户程序需要提供键值——key,它的类型是 key_t (int 整型)。系统调用函数(shmget, msgget 和 semget)根据 key,就可以查找到你需要的内核 id 号。在内核创建完成后,就已经有一个唯一的 key 值和它绑定起来了,也就是说 key 和内核对象是一一对应的关系。(key = 0 为特殊的键,它不能用来查找内核对象)

请添加图片描述

根据 key 获取内核对象 id 号

相同的key值,使用不用的get函数就能获取是内存内核对象的 id,还是消息队列的或者信号量的内核对象 id。

int id = shmget(0x8888, 0, 0); // 返回 0
int id = msgget(0x8888, 0); // 返回 1
int id = semget(0x8888, 0, 0); // 返回 4

用 key = 0 的键调用 get 后缀函数,将导致创建一个匿名内核对象而不是获取内核对象,这样的内核对象不绑定任何键值,这意味着你将无法通过 get 后缀函数来获取其 id。

4.4 创建IPC内核对象

在创建 IPC 内核对象时,要提供 key 值。

// 在 0x8888 这个键上创建内核对象,权限为 0644,如果已经存在就返回错误。
int id = shmget(0x8888, 4096, IPC_CREAT | IPC_EXCL | 0644);
int id = msgget(0x8888, IPC_CREAT | IPC_EXCL | 0644);
int id = semget(0x8888, 1, IPC_CREAT | IPC_EXCL | 0644); //第二个参数表示创建几个信号量

4.5 shmget函数

int shmget(key_t key, size_t size, int flags);

参数 key:约定好的键值。

  • 为 IPC_PRIVATE(这个宏被定义为 0),则表示创建一个新的内核对象并返回其 id 号。
  • 如果该值不等于 0,表示创建或者获取 IPC 内核对象的 id 号(具体是创建还是获取需要依据 shmflg 参数)。

参数 size

  • 创建内核对象时才有效,表示创建共享内存的大小(一般设置为一页内存大小的整数倍,页面内存大小通过 getpagesize() 函数获取)。

参数 flags:可选项

  • IPC_CREAT:创建内核对象。如果内核对象已存在,且未指定 IPC_EXCL,就返回该内核对象的 id 号。

  • IPC_EXCL:总是搭配 IPC_CREAT 一起使用。如果设定该选项,当内核对象已存在就返回错误,同时 errno 设定为 EEXIST。

  • 权限位:如果是创建新的内核对象,flags 还需要位或内核对象的权限位,比如 0664。

三个 System V IPC(shmget, msgget, semget) 都有参数 key 和 flags,用法都是一样的。

返回值:

  • 成功返回内核对象id,失败返回-1。

如果要获取已存在的内核对象id,除了key,其它参数都为0。

实验:创建 ipc 内核对象

程序 ipccreate 用于在指定的键值上创建 ipc 内核对象。使用格式为 ./ipccreate <ipc type> <key>,比如 ./ipccreate 0 0x8888 表示在键值 0x8888 上创建共享内存。具体运行方式看后面的示例。

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

int main(int argc, char* argv[]) {
    if (argc < 3) {
        printf("%s <ipc type> <key>\n", argv[0]);
        return -1;
    }

    key_t key = strtoll(argv[2], NULL, 16);
    char type = argv[1][0];
    char buf[64];
    int id;

    if (type == '0') {
        id = shmget(key, getpagesize(), IPC_CREAT | IPC_EXCL | 0644);
        strcpy(buf, "share memory");
    }
    else if (type == '1') {
        id = msgget(key, IPC_CREAT | IPC_EXCL | 0644);
        strcpy(buf, "message queue");
    }
    else if (type == '2') {
        id = semget(key, 5, IPC_CREAT | IPC_EXCL | 0644);
        strcpy(buf, "semaphore");
    }
    else {
        printf("type must be 0, 1, or 2\n");
        return -1;
    }

    if (id < 0) {
        perror("get error");
        return -1;
    }

    printf("create %s at 0x%x, id = %d\n", buf, key, id);

    return 0;
}
  • 编译和运行
$ gcc ipccreate.c -o ipccreate1
$ ./ipccreate 0 0x1234// 创建共享内存
$ ./ipccreate 1 0x1234 // 创建消息队列
$ ./ipccreate 2 0x1234 // 创建信号量
  • 运行结果

请添加图片描述

实验:获取 ipc 内核对象

程序 ipcget 用于在指定的键值上获取 ipc 内核对象的 id 号。使用格式为 ./ipcget <ipc type> <key>,比如 ./ipcget 0 0x8888 表示获取键值 0x8888 上的共享内存 id 号。

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

int main(int argc, char* argv[]) {
    if (argc < 3) {
        printf("%s <ipc type> <key>\n", argv[0]);
        return -1;
    }

    key_t key = strtoll(argv[2], NULL, 16);
    char type = argv[1][0];
    char buf[64];
    int id;

    if (type == '0') {
        id = shmget(key, 0, 0);
        strcpy(buf, "share memory");
    }
    else if (type == '1') {
        id = msgget(key, 0);
        strcpy(buf, "message queue");
    }
    else if (type == '2') {
        id = semget(key, 0, 0);
        strcpy(buf, "semaphore");
    }
    else {
        printf("type must be 0, 1, or 2\n");
        return -1;
    }

    if (id < 0) {
        perror("get error");
        return -1;
    }

    printf("get %s at 0x%x, id = %d\n", buf, key, id);

    return 0;
}
  • 编译和运行
$ gcc ipcget.c -o ipcget1
$ ./ipcget 0 0x1234// 创建共享内存
$ ./ipcget 1 0x1234 // 创建消息队列
$ ./ipcget 2 0x1234 // 创建信号量
  • 运行结果

请添加图片描述

创建的共享内存,消息队列,信号量可以通过命令ipcrm命令删除

4.6 键值与ftok

函数 ftok 可以根据路径和一个整数生成 key 值。如此你就可以约定好一个路径以及一个整数来取得相同的 key 了。

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

参数 pathname 可以是目录路径,也可以是文件路径(随便什么类型的文件都可以)。

参数 proj_id 可以取任意整数。

实验:使用 ftok 生成 key 并将其打印到屏幕。

// ftok.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>


int main(int argc, char* argv[]) {
    if (argc < 3) {
        printf("usage: %s <path> <id>\n", argv[0]);
        return -1;
    }

    int id = atoi(argv[2]);

    key_t key = ftok(argv[1], id);

    if (key == -1) {
        perror("ftok");
        return -1;
    }

    printf("key = 0x%08x\n", key);
    return 0;
}
  • 编译和运行
$ gcc ftok.c -o ftok1
$ touch tmp // 生成一个文件
$ ./ftok tmp 10

ftok 的算法

通过 stat 函数读取 pathname 的设备号和 inode 号,取设备号的低8位,inode 号的 低 16 位,以及 proj_id 的低 8 位组合成 key。

<proj_id 8 bit>-<dev 8 bit>-<inode 16 bit>

tmp 信息如下

请添加图片描述

可以看到 tmp 的设备号为 2049(十六进制为 0x801),inode 号为 930564(十六进制为 0xe3304),再根据 proj_id,图1 中使用的是 10,(十进制 0xa),分别取:

proj_id 的低 8 位—— 0x0a
设备号低 8 位 —— 0x01
inode 号低 16 位—— 0x3304

最后组合成成 key——0x0a013304。

4.7 shmget函数

黑色部分表示的是未分配的线性地址。

请添加图片描述

进程空间与物理页

shmget创建出共享内存,系统分配一个(或多个)物理页,具体分配多少看shmget 第二个参数。

shmat 与 shmdt 函数

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

shmadd为0,系统自动选合适的线性地址。shmflg:读写权限,为0可读写,为 SHM_RDONLY,只读。

shmat返回挂接的线性地址,

shmat 全称是 share memory attach,中译为共享内存挂接。

shmat 函数原理

shmat从进程空间中选择一个合适的或者指定的线性地址,挂接到共享内存物理页上。

请添加图片描述

shmat 函数将线性地址挂接到物理页

4.8 shmctl

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid:ipc:内核对象id。

cmd :命令

为IPC_STAT时, 第三个buf接收返回值,为IPC_SET时,buf传递值并设置内核对象。为cmd = IPC_RMID时,buf为NULL,删除内核对象。

shmid_ds 结构体

通将此结构体访问内核空间的ipc内核对象。

struct shmid_ds {
    struct ipc_perm shm_perm;    /* 所有权和权限位 */
    size_t          shm_segsz;   /* 段大小 */
    time_t          shm_atime;   /* 最后挂接时间 */
    time_t          shm_dtime;   /* 最后卸载时间 */
    time_t          shm_ctime;   /* 最后改变当前结构体的时间(由IPC_SET命令改变) */
    pid_t           shm_cpid;    /* 创建 ipc 内核对象的进程 pid */
    pid_t           shm_lpid;    /* 最后执行 shmat/shmdt 的进程 pid */
    shmatt_t        shm_nattch;  /* 挂接进程个数 */
    ...
};

其中成员 shm_perm 是所有 System V IPC 内核对象都包含的,它的结构如下:

struct ipc_perm {
    uid_t          uid;      /* 所有者有效用户 id */
    gid_t          gid;      /* 所有者有效用户组 id */
    uid_t          cuid;     /* 创建者有效用户 id */
    gid_t          cgid;     /* 创建者有效用户组 id */
    unsigned short mode;     /* 权限位*/
};

实验

下面的程序 shmctl 可以用来创建、删除内核对象,也可以挂接、卸载共享内存,还可以打印、设置内核对象信息。具体使用方法具体见下面的说明:

  • ./shmctl -c : 创建内核对象。
  • ./shmctl -d : 删除内核对象。
  • ./shmctl -v : 显示内核对象信息。
  • ./shmctl -s : 设置内核对象(将权限设置为 0600)。
  • ./shmctl -a : 挂接和卸载共享内存(挂接 5 秒后,再执行 shmdt,然后退出)。
// shmctl.c
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>

#define ASSERT(res) if((res)<0){perror(__FUNCTION__);exit(-1);}

// 打印 ipc_perm
void printPerm(struct ipc_perm* perm) {
    printf("euid of owner = %d\n", perm->uid);
    printf("egid of owner = %d\n", perm->gid);
    printf("euid of creator = %d\n", perm->cuid);
    printf("egid of creator = %d\n", perm->cgid);
    printf("mode = 0%o\n", perm->mode);
}

// 打印 ipc 内核对象信息
void printShmid(struct shmid_ds* shmid) {
    printPerm(&shmid->shm_perm);
    printf("segment size = %d\n", shmid->shm_segsz);
    printf("last attach time = %s", ctime(&shmid->shm_atime));
    printf("last detach time = %s", ctime(&shmid->shm_dtime));
    printf("last change time = %s", ctime(&shmid->shm_ctime));
    printf("pid of creator = %d\n", shmid->shm_cpid);
    printf("pid of last shmat/shmdt = %d\n", shmid->shm_lpid);
    printf("No. of current attaches = %ld\n", shmid->shm_nattch);
}

// 创建 ipc 内核对象
void create() {
    int id = shmget(0x8888, 123, IPC_CREAT | IPC_EXCL | 0664);
    printf("create %d\n", id);
    ASSERT(id);
}

// IPC_STAT 命令使用,用来获取 ipc 内核对象信息
void show() {
    int id = shmget(0x8888, 0, 0);
    ASSERT(id);
    struct shmid_ds shmid;
    ASSERT(shmctl(id, IPC_STAT, &shmid));
    printShmid(&shmid);
}

// IPC_SET 命令使用,用来设置 ipc 内核对象信息
void set() {
    int id = shmget(0x8888, 123, IPC_CREAT | 0664);
    ASSERT(id);
    struct shmid_ds shmid;
    ASSERT(shmctl(id, IPC_STAT, &shmid));
    shmid.shm_perm.mode = 0600;
    ASSERT(shmctl(id, IPC_SET, &shmid));
    printf("set %d\n", id);
}

// IPC_RMID 命令使用,用来删除 ipc 内核对象
void rm() {
    int id = shmget(0x8888, 123, IPC_CREAT | 0664);
    ASSERT(id);
    ASSERT(shmctl(id, IPC_RMID, NULL));
    printf("remove %d\n", id);
}

// 挂接和卸载
void at_dt() {
    int id = shmget(0x8888, 123, IPC_CREAT | 0664);
    ASSERT(id);
    char* buf = shmat(id, NULL, 0);
    if (buf == (char*)-1) ASSERT(-1);
    printf("shmat %p\n", buf);
    sleep(5); // 等待 5 秒后,执行 shmdt
    ASSERT(shmdt(buf));
    printf("shmdt %p\n", buf);
}



int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("usage: %s <option -c -v -s -d -a>\n", argv[0]);
        return -1;
    }

    printf("I'm %d\n", getpid());

    if (!strcmp(argv[1], "-c")) {
        create();
    }
    else if (!strcmp(argv[1], "-v")) {
        show();
    }
    else if (!strcmp(argv[1], "-s")) {
        set();
    }
    else if (!strcmp(argv[1], "-d")) {
        rm();
    }
    else if (!strcmp(argv[1], "-a")) {
        at_dt();
    }

    return 0;
}

创建内核对象

$ ./shmctl -c
I'm 36732
create 3801089
$ ./shmctl -v
I'm 36734
euid of owner = 1000
egid of owner = 1000
euid of creator = 1000
egid of creator = 1000
mode = 0664
segment size = 123
last attach time = Thu Jan  1 07:00:00 1970
last detach time = Thu Jan  1 07:00:00 1970
last change time = Tue Sep 28 07:36:10 2021
pid of creator = 36732
pid of last shmat/shmdt = 0
No. of current attaches = 0

设置内核对象,将内核对象权限设置为 0600

$ ./shmctl -s
I'm 36749
set 3801089
$ ./shmctl -v
I'm 36750
euid of owner = 1000
egid of owner = 1000
euid of creator = 1000
egid of creator = 1000
mode = 0600
segment size = 123
last attach time = Thu Jan  1 07:00:00 1970
last detach time = Thu Jan  1 07:00:00 1970
last change time = Tue Sep 28 07:49:32 2021
pid of creator = 36732
pid of last shmat/shmdt = 0
No. of current attaches = 0

先在另一个终端执行 ./shmctl -a,然后在当前终端执行./shmctl -v(注意手速,5秒内要搞定)。

请添加图片描述

上面的 ./shmctl -a 结束后,再执行一次 ./shmctl -v.

请添加图片描述

5 System V消息队列

消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息。每一条消息都有自己的消息类型,消息类型用整数来表示,而且必须大于 0。

请添加图片描述

位于内核空间的消息队列

数字1表示类型为1的消息,数字2、3、4 类似。消息类型为0的链表记录了所有消息加入队列的顺序。

5.1 消息队列相关的函数

// 创建和获取 ipc 内核对象
int msgget(key_t key, int flags);
 
// 将消息发送到消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

// 从消息队列获取消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

// 查看、设置、删除 ipc 内核对象(用法和 shmctl 一样)
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

5.2 消息数据格式

struct Msg{
    long type; // 消息类型。这个是必须的,而且值必须 > 0,这个值被系统使用
    // 消息正文,多少字节随你而定
    // ...
}

只要保证首4字节是一个整数就行了,下面的都可以

struct Msg {
    long type;
    char name[20];
    int age;
} msg;

struct Msg {
    long type;
    int start;
    int end;
} msg;

正文部分是什么数据类型都没关系,因为消息队列传递的是 2 进制数据,不一定非得是文本。

5.3 msgsnd函数

作用:用于将数据发送到消息队列。

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

msqid:ipc 内核对象 id

msgp:消息数据地址

msgsz:消息正文部分的大小(不包含消息类型)

msgflg:可选项

  • 为0:如果消息队列空间不够,msgsnd 会阻塞。

  • 为IPC_NOWAIT:直接返回,如果空间不够,设置errno为EAGIN。

返回值:0表示成功,-1失败

5.4 msgrcv函数

作用:从消息队列取出消息,并从消息队列里删除。

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

msqid:ipc 内核对象 id

msgp:用来接收消息数据地址

msgsz:消息正文部分的大小(不包含消息类型)

msgtyp:指定获取哪种类型的消息

  • msgtyp = 0:获取消息队列中的第一条消息

  • msgtyp > 0:获取类型为 msgtyp 的消息,若msgflg 为MSG_EXCEPT,获取除了 msgtyp 类型以外的消息。

  • msgtyp < 0:获取类型≤|msgtyp| 的消息。

msgflg:可选项。

  • 为0:没有消息就阻塞。

  • IPC_NOWAIT:如果指定类型的消息不存在就立即返回,同时设置 errno 为 ENOMSG

  • MSG_EXCEPT:仅用于 msgtyp > 0 的情况。获取类型不为 msgtyp 的消息

  • MSG_NOERROR:如果消息数据正文内容大于 msgsz,就将消息数据截断为 msgsz

实验:程序 msg_send 和 msg_recv 分别用于向消息队列发送数据和接收数据。

  • msg_send.c

msg_send 程序定义了一个结构体 Msg,消息正文部分是结构体 Person。该程序向消息队列发送了 10 条消息。

// msg_send.c
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>

#define ASSERT(prompt,res) if((res)<0){perror(#prompt);exit(-1);}

typedef struct {
    char name[20];
    int age;
}Person;

typedef struct {
    long type;
    Person person;
}Msg;

int main(int argc, char* argv) {
    int id = msgget(0x8888, IPC_CREAT | 0664);
    ASSERT(msgget, id);

    Msg msg[10] = {
      {1, {"Luffy", 17}},
      {1, {"Zoro", 19}},
      {2, {"Nami", 18}},
      {2, {"Usopo", 17}},
      {1, {"Sanji", 19}},
      {3, {"Chopper", 15}},
      {4, {"Robin", 28}},
      {4, {"Franky", 34}},
      {5, {"Brook", 88}},
      {6, {"Sunny", 2}}
    };

    int i;
    for (i = 0; i < 10; ++i) {
        int res = msgsnd(id, &msg[i], sizeof(Person), 0);
        ASSERT(msgsnd, res);
    }

    return 0;
}

请添加图片描述

第一次执行完 msg_send 后的消息队列
  • msg_recv

msg_recv 程序接收一个参数,表示接收哪种类型的消息。比如./msg_recv 4 表示接收类型为 4 的消息,并打印在屏幕。

// msg_recv.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>


#define ASSERT(prompt,res) if((res)<0){perror(#prompt);exit(-1);}

typedef struct {
    char name[20];
    int age;
}Person;

typedef struct {
    long type;
    Person person;
}Msg;

void printMsg(Msg* msg) {
    printf("{ type = %ld, name = %s, age = %d }\n",
        msg->type, msg->person.name, msg->person.age);
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("usage: %s <type>\n", argv[0]);
        return -1;
    }

    // 要获取的消息类型
    long type = atol(argv[1]);

    // 获取 ipc 内核对象 id
    int id = msgget(0x8888, 0);

    // 如果错误就退出
    ASSERT(msgget, id);

    Msg msg;
    int res;

    while (1) {
        // 以非阻塞的方式接收类型为 type 的消息 
        res = msgrcv(id, &msg, sizeof(Person), type, IPC_NOWAIT);
        if (res < 0) {
            // 如果消息接收完毕就退出,否则报错并退出
            if (errno == ENOMSG) {
                printf("No message!\n");
                break;
            }
            else {
                ASSERT(msgrcv, res);
            }
        }
        // 打印消息内容
        printMsg(&msg);
    }
    return 0;
}
  • 编译
$ gcc msg_send.c -o msg_send
$ gcc msg_recv.c -o msg_recv12
  • 运行

先运行 msg_send,再运行 msg_recv。

  • 接收所有消息
$ ./msg_send
$ ./msg_recv 0
{ type = 1, name = Luffy, age = 17 }
{ type = 1, name = Zoro, age = 19 }
{ type = 2, name = Nami, age = 18 }
{ type = 2, name = Usopo, age = 17 }
{ type = 1, name = Sanji, age = 19 }
{ type = 3, name = Chopper, age = 15 }
{ type = 4, name = Robin, age = 28 }
{ type = 4, name = Franky, age = 34 }
{ type = 5, name = Brook, age = 88 }
{ type = 6, name = Sunny, age = 2 }
No message!
  • 接收类型为 4 的消息
$ ./msg_send
$ ./msg_recv 4
  • 接收类型小于等于 3 的所有消息
$ ./msg_recv -3

6 System V信号量

6.1 System V信号量简介

信号量是一种资源数量,你使用资源,信号量的值减少,归还资源,增多。

int semget(key_t key, int nsems, int semflg);// 创建信号量
int semop(int semid, struct sembuf *sops, unsigned nsops);// 请求资源或归还资源
int semctl(int semid, int semnum, int cmd);// 获取一个信号量
int semctl(int semid, int semnum, int cmd, union semun buf);// 设置一个或多个信号量,获取所有信号量的值
  • semget 中的参数 nsems,表示你要创建几个信号量(即几个资源)。创建完成后,以后要操作哪个信号量,只要告诉信号量的索引号就行了。

  • semop 中的 sops 参数,该参数需要传递一个数组,nsops 表示数组的个数。数组元素是 sembuf 结构体,

struct sembuf {
    unsigned short sem_num;  /* 要操作的信号量索引 */
    short          sem_op;   /* > 0 归还资源数,< 0 请求资源数 */
    short          sem_flg;  /* 可选项,操作的行为 */
}
  • semctl 中的 semnum 参数,表示要操作哪个信号量。
  • semctl 中的 union semun buf 参数依赖于 cmd 命令,具体如下:
union semun {
    int              val;    /* cmd = SETVAL */
    struct semid_ds *buf;    /* cmd = IPC_STAT, IPC_SET */
    unsigned short  *array;  /* cmd = GETALL, SETALL */
};

如何使用信号量

  1. 指定要创建的信号量的个数,创建信号量的 ipc 内核对象,获取 ipc 内核对象 id.
  2. 使用 semctl 的 SETVAL 或者 SETALL 命令设置信号量的值(每种资源的个数)。
  3. 使用 semop 对指定若干个信号量进行同时操作(请求资源或归还资源)。
  4. 使用 semctl 的 IPC_RMID 命令删除信号量

6.2 创建和获取信号量

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

nsems:创建几个信号量。

例:创建一个 ipc 内核对象,包含 3 个信号量。

int id = semget(0x8888, 3, IPC_CREAT | IPC_EXCL | 0664);

6.3 设置和获取信号量值

这里主要使用函数 semctl,命令可以是 SETVAL,也可以是 SETALL。前者用来设置某个信号量的值,后者表示设置所有信号量的值。

int semctl(int semid, int semnum, int cmd, union semun);

union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
};

semnum:表示要操作哪个信号量,

cmd

SETVAL:设置某个信号量的值

SETALL:设置所有信号量的值。

例子:设置信号量

semctl(id, 2, SETVAL, 5); //设置第2个信号量的值为5:

设置所有信号量的值

unsigned short vals[3] = {3, 6, 9};//将3个信号量的值分别设置为3,6,9

semctl(id, 0, SETALL, vals); //这时第二个参数被忽略

例子:获取信号量

获取第 1 个信号量的值

int val = semctl(id, 1, GETVAL); // 第4个参数不用写了,获取的值通过返回值返回

获取所有信号量的值

unsigned short vals[3];
semctl(id, 0, GETALL, vals); // 这时第二个参数被忽略,获取的值保存到 vals 数组

6.4 请求和释放信号量

当请求的信号量值 > 0 时,semop 直接返回,否则阻塞,直到信号量值大于 0。

释放资源,semop 直接返回。

int semop(int semid, struct sembuf *sops, unsigned nsops);

该函数第二个参数是一个数组,第三个参数表示数组大小。第二个参数的结构体如下:

struct sembuf {
    unsigned short sem_num;  /* semaphore number */
    short          sem_op;   /* semaphore operation */
    short          sem_flg;  /* operation flags */
}
  • sops是数组,nsops为数组大小。

  • sem_num : 操作第几个信号量

  • sem_op:为一个短整型,操作完成后,这个数值加到信号量上

  • sem_flg:可选项,一般为 0

    • IPC_NOWAIT:无论请求的资源有没有,立即返回。

    • SEM_UNDO:如果设置该值,当进程结束后,该进程执行的所有的操作全部撤销。

例子:请求资源(P操作)

  • 请求 3 个资源(将信号量值减 3)
struct sembuf op;
op.sem_num = 2; // 请求第2个资源(信号量)
op.sem_op = -3;
op.sem_flg = 0;
semop(id, &op, 1);
  • 同时操作多个信号量
struct sembuf ops[3] = {
  {0, -1, 0},
  {1, -5, 0},
  {2, -3, 0}
};

semop(id, ops, 3);

例子:释放(归还)资源(V操作)

  • 释放 2 个资源(将信号量值加 2)
struct sembuf op;
op.sem_num = 1; // 请求第 1 个资源(信号量)
op.sem_op = 2;
op.sem_flg = 0;
semop(id, &op, 1);
  • 同时操作多个信号量
struct sembuf ops[3] = {
  {0, 4, 0}, // 释放 4 个 0 号资源
  {1, 2, 0}, // 释放 2 个 1 号资源
  {2, 5, 0}  // 释放 5 个 2 号资源
};

semop(id, ops, 3);

实验

// semop.c
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

#define R0 0
#define R1 1
#define R2 2

void printSem(int id) {
    unsigned short vals[3] = { 0 };
    semctl(id, 3, GETALL, vals);
    printf("R0 = %d, R1= %d, R2 = %d\n\n", vals[0], vals[1], vals[2]);
}

int main() {
    int id = semget(0x8888, 3, IPC_CREAT | IPC_EXCL | 0664);

    // 打印信号量值
    puts("信号量初始值(默认值)");
    printSem(id);

    // 1. 设置第 2 个信号量值
    puts("1. 设置第 2 个信号量(R2)值为 20");
    semctl(id, 2, SETVAL, 20);
    printSem(id);

    // 2. 同时设置 3 个信号量的值
    puts("2. 同时设置 3 个信号量的值为 12, 5, 9");
    unsigned short vals[3] = { 12, 5, 9 };
    semctl(id, 0, SETALL, vals);
    printSem(id);

    // 3. 请求 2 个 R0 资源
    puts("3. 请求 2 个 R0 资源");
    struct sembuf op1 = { 0, -2, 0 };
    semop(id, &op1, 1);
    printSem(id);

    // 4. 请求 3 个 R1 和 5 个 R2
    puts("4. 请求 3 个 R1 和 5 个 R2");
    struct sembuf ops1[2] = {
      {1, -3, 0},
      {2, -5, 0}
    };
    semop(id, ops1, 2);
    printSem(id);

    // 5. 释放 2 个 R1
    puts("5. 释放 2 个 R1");
    struct sembuf op2 = { 1, 2, 0 };
    semop(id, &op2, 1);
    printSem(id);

    // 6. 释放 1 个 R0, 1 个 R1,3 个 R2
    puts("6. 释放 1 个 R0, 1 个 R1,3 个 R2");
    struct sembuf ops2[3] = {
      {0, 1, 0},
      {1, 1, 0},
      {2, 3, 0}
    };
    semop(id, ops2, 3);
    printSem(id);

    // 7. 删除 ipc 内核对象
    puts("7. 删除 ipc 内核对象");
    semctl(id, 0, IPC_RMID);

    return 0;
}

编译运行

$ gcc semop.c -o semop
$ ./semop 
信号量初始值(默认值)
R0 = 0, R1= 0, R2 = 0

1. 设置第 2 个信号量(R2)值为 20
R0 = 0, R1= 0, R2 = 20

2. 同时设置 3 个信号量的值为 12, 5, 9
R0 = 12, R1= 5, R2 = 9

3. 请求 2 个 R0 资源
R0 = 10, R1= 5, R2 = 9

4. 请求 3 个 R1 和 5 个 R2
R0 = 10, R1= 2, R2 = 4

5. 释放 2 个 R1
R0 = 10, R1= 4, R2 = 4

6. 释放 1 个 R0, 1 个 R1,3 个 R2
R0 = 11, R1= 5, R2 = 7

7. 删除 ipc 内核对象

7 生产者消费者模型

PV 原语:

  • P(S) 将资源S减 1,即 S = S - 1. 如果 S <= 0,该进程进入等待。

  • V(S):将资源S加 1,即 S = S + 1。

请添加图片描述

3个生产者和4个消费者 ,缓冲区为5

信号量MUTEX表示资源cake是否被占用,初始值为1。

信号量FULL表示蛋糕的个数,初始值为0。

信号量EMPTY表示空缓冲区的个数,初始值为5。

生产者进程

while(1) {
  P(EMPTY); // 减少一个空缓冲区个数
  P(MUTEX);
  if (cake < 5) { 
    cake++;
  }
  V(MUTEX);
  V(FULL); // 增加一个蛋糕个数
}

消费者进程

while(1) {
  P(FULL); // 减少一个蛋糕个数
  P(MUTEX);
  if (cake > 0) {
    cake--;
  }
  V(MUTEX);
  V(EMPTY); // 增加一个空缓冲区个数
}

描述蛋糕个数的信号量FULL <= 0时,消费者执行到 P(FULL) 进入等待状态,不再被调度。当FULL > 0,才会被调度。避免CPU浪费。

实验:生产者消费者模型

  • 头文件 semutil.h
// semutil.h
#ifndef __SEMUTIL_H__
#define __SEMUTIL_H__

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

#define ASSERT(prompt,res) if((res)<0){perror(#prompt);exit(-1);}

/*
 * Create
 * 创建和获取信号量 ipc 内核对象 id
 * count > 0 表示创建,count = 0 表示获取
 */
int C(int count);

/*
 * Set
 * 初始化第 semnum 个信号量的值为 val
 */
void S(int id, int semnum, int val);

/*
 * Get
 * 获取第 semnum 个信号量的值
 */
int G(int id, int semnum);

/*
 * Delete
 * 删除信号量内核对象
 */
void D(int id);

/*
 * 请求第 semnum 个信号量,将其值减 1
 */
void P(int id, int semnum);

/*
 * 归还第 semnum 个信号量,将其值加 1
 */
void V(int id, int semnum);

#endif //__SEMUTIL_H__
  • 实现文件 semutil.c
// semutil.c
#include "semutil.h"

int C(int count) {
    int id;
    if (count > 0)
        id = semget(0x8888, count, IPC_CREAT | IPC_EXCL | 0664);
    else
        id = semget(0x8888, 0, 0);
    ASSERT(semget, id);
    return id;
}

void S(int id, int semnum, int val) {
    ASSERT(semctl, semctl(id, semnum, SETVAL, val));
}

void D(int id) {
    ASSERT(semctl, semctl(id, 0, IPC_RMID));
}

void P(int id, int semnum) {
    struct sembuf op;
    op.sem_num = semnum;
    op.sem_op = -1;
    op.sem_flg = 0;
    ASSERT(semop, semop(id, &op, 1));
}

void V(int id, int semnum) {
    struct sembuf op;
    op.sem_num = semnum;
    op.sem_op = 1;
    op.sem_flg = 0;
    ASSERT(semop, semop(id, &op, 1));
}

int G(int id, int semnum) {
    return semctl(id, semnum, GETVAL);
}
  • 程序代码
// pc.c
#include "semutil.h"
#include <string.h>
#include <sys/shm.h>

#define MUTEX 0
#define FULL 1
#define EMPTY 2

static void init() {
  int id = C(3);
  S(id, MUTEX, 1); 
  S(id, FULL, 0); 
  S(id, EMPTY, 5); 
  int shmid = shmget(0x8888, 4, IPC_CREAT | IPC_EXCL | 0664);
  ASSERT(shmget, shmid);
  int *cake= shmat(shmid, NULL, 0); 
  if (cake == (int*)-1) ASSERT(shmat, -1); 
  *cake = 0;
  ASSERT(shmdt, shmdt(cake));
}

static int getsemid() {
  return C(0);
}

static int getshmid() {
  int id = shmget(0x8888, 0, 0); 
  ASSERT(shmget, id);
  return id; 
}

static void release(int id) {
  D(id);
  ASSERT(shmctl, shmctl(getshmid(), IPC_RMID, NULL));
}

static void producer() {
  int id = getsemid();
  int shmid = getshmid();
  int *cake = shmat(shmid, NULL, 0); 
  while(1) {
    P(id, EMPTY);
    P(id, MUTEX);
    printf("current cake = %d, ", *cake);
    (*cake)++;
    printf("produce a cake, ");
    printf("cake = %d\n", *cake);
    V(id, MUTEX);
    V(id, FULL);
    sleep(1);
  }
  shmdt(cake);
}
static void consumer() {
  int id = getsemid();
  int shmid = getshmid();
  int *cake = shmat(shmid, NULL, 0);
  int count = 10;
  while(count--) {
    P(id, FULL);
    P(id, MUTEX);
    printf("current cake = %d, ", *cake);
    (*cake)--;
    printf("consume a cake, ");
    printf("cake = %d\n", *cake);
    V(id, MUTEX);
    V(id, EMPTY);
  }
  shmdt(cake);
}

int main(int argc, char *argv[]) {
  if (argc < 2) {
    printf("usage: %s <option -b -d -p -c>\n", argv[0]);
    return -1;
  }

  if (!strcmp("-b", argv[1])) {
    init();
  }
  else if (!strcmp("-d", argv[1])) {
    release(getsemid());
  }
  else if (!strcmp("-p", argv[1])) {
    producer();
  }
  else if (!strcmp("-c", argv[1])) {
    consumer();
  }
  return 0;
}

编译

$ gcc pc.c semuti.c -o pc

使用如下

./pc -b : 初始化 ipc 内核对象
./pc -d : 删除 ipc 内核对象
./pc -p : 启动生产者进程
./pc -c : 启动消费者进程

请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值