Skip to content

进程间通信

目录


Linux 进程间通信方式:

  • 来自 UNIX 的进程间通信方式:管道、命名管道、信号
  • 来自 System V 的进程间通信方式:System V 消息队列、System V 信号量、System V 共享内存
  • socket 进程间通信方式
  • POSIX 进程通讯:Posix 消息队列、Posix 信号量、Posix 共享内存

理论部分可以参考笔记:操作系统-进程通信

pipe() | 管道

函数原型

c
#include <unistd.h>
/**
 * 创建管道
 * @param fd 用于返回管道的读写文件描述符
 * @return 成功返回 0,失败返回 -1
 */
int pipe(int fd[2]);

管道是一种半双工的通信方式,数据只能在一个方向上流动,需要双方通信时,需要建立起两个管道。

EG

先用 pipe 创建一个管道,然后再用 fork 创建一个子进程,子进程继承了父进程的管道,然后父进程关闭管道的读端,子进程关闭管道的写端,这样就可以实现父子进程之间的通信。

向管道写入数据时,Linux 将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞。

c
/* pipe.c */
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX_DATA_LEN 256
#define DELAY_TIME 1
int main()
{
    pid_t pid;
    int pipe_fd[2];
    char buf[MAX_DATA_LEN];
    const char data[] = "Pipe Test Program";
    int real_read, real_write;

    memset((void *)buf, 0, sizeof(buf));
    /* 创建管道 */
    if (pipe(pipe_fd) < 0)
    {
        printf("pipe create error\n");
        exit(1);
    }

    /* 创建一子进程 */
    if ((pid = fork()) == 0)
    {
        /* 子进程关闭写描述符,并通过使子进程暂停 1s 等待父进程已关闭相应的读描述符 */
        close(pipe_fd[1]);
        sleep(DELAY_TIME * 3);

        /* 子进程读取管道内容 */
        if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
        {
            printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
        }

        /* 关闭子进程读描述符 */
        close(pipe_fd[0]);
        exit(0);
    }
    else if (pid > 0)
    {
        /* 父进程关闭读描述符,并通过使父进程暂停 1s 等待子进程已关闭相应的写描述符 */
        close(pipe_fd[0]);
        sleep(DELAY_TIME);
        if ((real_write = write(pipe_fd[1], data, strlen(data))) != -1)
        {
            printf("Parent wrote %d bytes : '%s'\n", real_write, data);
        }

        /*关闭父进程写描述符*/
        close(pipe_fd[1]);

        /*收集子进程退出信息*/
        waitpid(pid, NULL, 0);
        exit(0);
    }
}

popen() | 标准流管道

可以获取其他程序的输出,或者向其他程序输入数据。

函数原型

c
#include <stdio.h>
/**
 * 执行command命令,并返回文件指针
 * @param command 用于执行的命令(以null 结束符结尾的字符串)
 * @param type 用于指定管道的读写方式
 * - r:只读
 * - w:只写
 * @return 成功返回文件指针,失败返回 -1
 */
FILE *popen(const char *command, const char *type);
c
#include <stdio.h>
/**
 * 关闭文件指针
 * @param fp 文件指针
 * @return 成功返回 0,失败返回 -1
 */
int pclose(FILE *fp);

EG

c
/* standard_pipe.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#define BUFSIZE 1024
int main()
{
    FILE *fp;
    char *cmd = "ps -ef";
    char buf[BUFSIZE];

    /*调用 popen()函数执行相应的命令*/
    if ((fp = popen(cmd, "r")) == NULL)
    {
        printf("Popen error\n");
        exit(1);
    }

    while ((fgets(buf, BUFSIZE, fp)) != NULL)
    {
        printf("%s", buf);
    }
    pclose(fp);
    exit(0);
}

// $ ./standard_pipe
//  PID TTY Uid Size State Command
//  1 root 1832 S init
//  2 root 0 S [keventd]
//  3 root 0 S [ksoftirqd_CPU0]
//  ……
//  74 root 1284 S ./standard_pipe
//  75 root 1836 S sh -c ps -ef
//  76 root 2020 R ps –ef

mkfifo() | 命名管道

突破了管道只能在具有公共祖先的进程之间使用的限制,可以在不相关的进程之间使用。FIFO 创建了一个管道文件,进程可以打开这个文件来进行通信。

支持 read、write、open、close 等操作,但是不支持 lseek 操作。

  • 对于读进程。
    • 若该管道是阻塞打开,且当前 FIFO 内没有数据,则对读进程而言将一直阻塞到有数据写入。
    • 若该管道是非阻塞打开,则不论 FIFO 内是否有数据,读进程都会立即执行读操作。即如果 FIFO 内没有数据,则读函数将立刻返回 0。
  • 对于写进程。
    • 若该管道是阻塞打开,则写操作将一直阻塞到数据可以被写入。
    • 若该管道是非阻塞打开而不能写入全部数据,则读操作进行部分写入或者调用失败。

函数原型

c
#include <sys/types.h>
#include <sys/state.h>
/**
 * 创建命名管道
 * @param pathname 管道文件的路径名
 * @param mode 管道文件的访问权限
 * @return 成功返回 0,失败返回 -1
 */
int mkfifo(const char *pathname, mode_t mode);

mode 参数

  • O_RDONLY:读管道
  • O_WRONLY:写管道
  • O_RDWR:读写管道
  • O_NONBLOCK:非阻塞
  • O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用第三个参数为其设置权限
  • O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息。这一参数可测试文件是否存在

报错类型

ERRCODE说明
EACCESS参数 filename 所指定的目录路径无可执行的权限
EEXIST参数 filename 所指定的文件已存在
ENAMETOOLONG参数 filename 的路径名称太长
ENOENT参数 filename 包含的目录不存在
ENOSPC文件系统的剩余空间不足
ENOTDIR参数 filename 路径中的目录存在但却非真正的目录
EROFS参数 filename 指定的文件存在于只读文件系统内

EG

下面的实例包含了两个程序,一个用于读管道,另一个用于写管道。其中在读管道的程序里创建管道,并且作为 main()函数里的参数由用户输入要写入的内容。读管道的程序会读出用户写入到管道的内容,这两个程序采用的是阻塞式读写管道模式。

c
/* fifo_write.c */
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MYFIFO "/tmp/myfifo"     /* 有名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF /*定义在于 limits.h 中*/
int main(int argc, char *argv[]) /*参数为即将写入的字符串*/
{
    int fd;
    char buff[MAX_BUFFER_SIZE];
    int nwrite;

    if (argc <= 1)
    {
        printf("Usage: ./fifo_write string\n");
        exit(1);
    }
    sscanf(argv[1], "%s", buff);

    /* 以只写阻塞方式打开 FIFO 管道 */
    fd = open(MYFIFO, O_WRONLY);
    if (fd == -1)
    {
        printf("Open fifo file error\n");
        exit(1);
    }
    /*向管道中写入字符串*/
    if ((nwrite = write(fd, buff, MAX_BUFFER_SIZE)) > 0)
    {
        printf("Write '%s' to FIFO\n", buff);
    }
    close(fd);
    exit(0);
}
c
/*fifo_read.c*/
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MYFIFO "/tmp/myfifo"     /* 有名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF /*定义在于 limits.h 中*/
int main()
{
    char buff[MAX_BUFFER_SIZE];
    int fd;
    int nread;
    /* 判断有名管道是否已存在,若尚未创建,则以相应的权限创建*/
    if (access(MYFIFO, F_OK) == -1)
    {
        if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
        {
            printf("Cannot create fifo file\n");
            exit(1);
        }
    }
    /* 以只读阻塞方式打开有名管道 */
    fd = open(MYFIFO, O_RDONLY);
    if (fd == -1)
    {
        printf("Open fifo file error\n");
        exit(1);
    }

    while (1)
    {
        memset(buff, 0, sizeof(buff));
        if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)
        {
            printf("Read '%s' from FIFO\n", buff);
        }
    }
    close(fd);
    exit(0);
}

为了能够较好地观察运行结果,需要把这两个程序分别在两个终端里运行,在这里首先启动读管道程序。读管道进程在建立管道之后就开始循环地从管道里读出内容,如果没有数据可读,则一直阻塞到写管道进程向管道写入数据。在启动了写管道程序后,读进程能够从管道里读出用户的输入内容

信号 | signal

信号生命周期:

  1. 信号产生(内核进程)
  2. 信号注册(用户进程)
  3. 信号注销(用户进程)
  4. 信号处理(用户进程/内核进程)

可靠信号

一个不可靠信号的处理过程是这样的:如果发现该信号已经在进程中注册,那么就忽略该信号。因此,若前一个信号还未注销又产生了相同的信号就会产生信号丢失。而当可靠信号发送给一个进 程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此信号就不会丢失。所有可靠信号都支持排队,而所有不可靠信号都不支持排队。

响应信号

用户进程对信号的响应可以有 3 种方式:

  • 忽略信号:即对信号不做任何处理,但是有两个信号不能忽略,即 SIGKILL 及 SIGSTOP
  • 捕捉信号:定义信号处理函数,当信号发生时,执行相应的自定义处理函数
  • 执行缺省操作:Linux 对每种信号都规定了默认操作

常见信号的含义及其默认操作

信 号 名含 义默 认 操 作
SIGHUP该信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一会话内的各个作业与控制终端不再关联终止
SIGINT该信号在用户键入 INTR 字符(通常是 Ctrl-C)时发出,终端驱动程序发送此信号并送到前台进程中的每一个进程终止
SIGQUIT该信号和 SIGINT 类似,但由 QUIT 字符(通常是 Ctrl-\)来控制终止
SIGILL该信号在一个进程企图执行一条非法指令时(可执行文件本身出现错误,或者试图执行数据段、堆栈溢出时)发出终止
SIGFPE该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出及除数为 0 等其他所有的算术错误终止
SIGKILL该信号用来立即结束程序的运行,并且不能被阻塞、处理或忽略终止
SIGALRM该信号当一个定时器到时的时候发出终止
SIGSTOP该信号用于暂停一个进程,且不能被阻塞、处理或忽略暂停进程
SIGTSTP该信号用于交互停止进程,用户键入 SUSP 字符时(通常是 Ctrl+Z)发出这个信号停止进程
SIGCHLD子进程改变状态时,父进程会收到这个信号忽略
SIGABORT进程异常终止时发出

kill()&raise()&alarm() | 信号发送

kill 函数可以向指定进程发送信号

raise 函数可以向自身进程发送信号

alarm()也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它就向进程发送 SIGALARM 信号。要注意的是,一个进程只能有一个闹钟时间,如果在调用 alarm()之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替

函数原型

c
#include <sys/types.h>
#include <signal.h>
/**
 * 向指定进程发送信号
 * @param pid 指定进程的进程号
 * - pid > 0:发送给进程号为 pid 的进程
 * - pid = 0:发送给与发送进程同一进程组的所有进程
 * - pid = -1:发送给除 1 号进程(init 进程)外的所有进程
 * - pid < -1:发送给进程组号为 pid 绝对值的所有进程
 * @param sig 发送的信号
 * @return 成功返回 0,失败返回 -1
 */
int kill(pid_t pid, int sig);
c
#include <signal.h>
#include <sys/types.h>
/**
 * 向自身进程发送信号
 * @param sig 发送的信号
 * @return 成功返回 0,失败返回 -1
 */
int raise(int sig);
c
#include <unistd.h>
/**
 * 设置定时器
 * @param seconds 定时器的时间
 * @return 返回上一次闹钟时间剩余的秒数
 */
unsigned int alarm(unsigned int seconds);

EG

下面这个示例首先使用 fork()创建了一个子进程,接着为了保证子进程不在父进程调用 kill()之前退出,在 子进程中使用 raise()函数向自身发送 SIGSTOP 信号,使子进程暂停。接下来再在父进程中调用 kill()向子 进程发送信号,在该示例中使用的是 SIGKILL,读者可以使用其他信号进行练习

c
/* kill_raise.c */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t pid;
    int ret;

    /* 创建一子进程 */
    if ((pid = fork()) < 0)
    {
        printf("Fork error\n");
        exit(1);
    }

    if (pid == 0)
    {
        /* 在子进程中使用 raise()函数发出 SIGSTOP 信号,使子进程暂停 */
        printf("Child(pid : %d) is waiting for any signal\n", getpid());
        raise(SIGSTOP);
        exit(0);
    }
    else
    {
        /* 在父进程中收集子进程发出的信号,并调用 kill()函数进行相应的操作 */
        if ((waitpid(pid, NULL, WNOHANG)) == 0)
        {
            if ((ret = kill(pid, SIGKILL)) == 0)
            {
                printf("Parent kill %d\n", pid);
            }
        }

        waitpid(pid, NULL, 0);
        exit(0);
    }
}

pause() | 捕捉信号

pause()函数是用于将调用进程挂起直至捕捉到信号为止。这个函数很常用,通常可以用于判断信号是否已到

函数原型

c
#include <unistd.h>
/**
 * 挂起进程,直到捕捉到信号为止
 * @return 成功返回 -1,失败返回 -1
 */
int pause(void);

EG

该实例实际上已完成了一个简单的 sleep()函数的功能,由于 SIGALARM 默认的系统动作为终止该进程, 因此程序在打印信息之前,就会被结束了。代码如下所示:

c
/* alarm_pause.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
    /*调用 alarm 定时器函数*/
    int ret = alarm(5);
    pause();
    printf("I have been waken up.\n", ret); /* 此语句不会被执行 */
}

// $./ alarm_pause
// Alarm clock

signal() | 信号的处理

使用 signal()函数处理时,只需要指出要处理的信号和处理函数即可。它主要是用于前 32 种非实时信号的处理,不支持信号传递信息,但是由于使用简单、易于理解,因此也受到很多程序员的欢迎。

Linux 还支持一个更健壮、更新的信号处理函数 sigaction(),推荐使用该函数。

函数原型

参考资料:

c
#include <signal.h>
/**
 * 信号处理函数
 * @param signum 信号带啊吗
 * @param handler 处理函数
 * - SIG_IGN:忽略信号
 * - SIG_DFL:执行缺省操作
 * - 自定义的信号处理函数指针
 * @return 成功返回以前的信号处理配置,失败返回 -1
 */
void (*signal(int signum, void (*handler)(int)))(int);
c
#include <signal.h>
/**
 * 信号处理函数
 * @param signum 信号代码,可以为除 SIGKILL 及 SIGSTOP 外的任何一个特定有效的信号
 * @param act 指向结构 sigaction 的一个实例的指针,指定对特定信号的处理
 * @param oldact 保存原来对相应信号的处理
 * @return 成功返回 0,失败返回 -1
 */
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction
{
    // 信号处理函数指针 | SIG_DFL(默认操作) | SIG_IGN(忽略信号)
    void (*sa_handler)(int signo);
    // 信号集:指定在信号处理程序执行过程中哪些信号应当被屏蔽
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restore)(void);
}

信号集函数组 | 信号的处理

使用信号集函数组处理信号时涉及一系列的函数

创建信号集合

c
#include <signal.h>
/**
 * 创建信号函数集合
 * @param set 信号集合
 * @param signum 信号代码
 * @return 成功返回 0,失败返回 -1
 */

// 将信号集合初始化为空
int sigemptyset(sigset_t *set)
// 将信号集合初始化为包含所有信号
int sigfillset(sigset_t *set)
// 将指定信号加入到信号集合中去
int sigaddset(sigset_t *set, int signum)
// 将指定信号从信号集合中删除
int sigdelset(sigset_t *set, int signum)
// 判断指定信号是否在信号集合中
// 若在则返回 1,否则返回 0
int sigismember(sigset_t *set, int signum)

sigprocmask() | 设置信号屏蔽字

c
#include <signal.h>
/**
 * 设置信号屏蔽字
 * @param how 信号屏蔽字的操作方式
 * - SIG_BLOCK:
 * - SIG_UNBLOCK:
 * - SIG_SETMASK:
 * @param set 信号集合
 * @param oset 用于保存原来的信号屏蔽字
 * @return 成功返回 0,失败返回 -1
 */
int sigprocmask(int how, const sigset_t *set, sigset_t *oset)

信号量 | semaphore

理论请参考操作系统笔记

semget() | 创建信号量

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

/**
 * 创建信号量
 * @param key 信号量的键值,不同进程可以通过它访问同一个信号量,若为 IPC_PRIVATE,则只能由创建者进程访问
 * @param nsems 信号量的个数
 * @param semflg 信号量的权限
 * @return 成功返回信号量的标识符,失败返回 -1
 */
int semget(key_t key, int nsems, int semflg);

semctl() | 控制信号量

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

/**
 * 控制信号量
 * @param semid 信号量的标识符
 * @param semnum 信号量编号,当使用信号量集时才会被用到。通常取值为 0,就是使用单个信号量(也是第一个信号量)
 * @param cmd 指定对信号量的各种操作,当使用单个信号量(而不是信号量集)时,常用的有以下几种:
    * - IPC_RMID:删除信号量
    * - IPC_STAT:把 semid_ds 结构中的数据设置为信号量的当前值
    * - IPC_SETVAL:将信号量值设置为 arg 的 val 值
    * - IPC_GETVAL:返回信号量的当前值
 * @param arg 可选参数
 * @return 成功根据 cmd 的不同返回不同的值,失败返回 -1
    * - IPC_RMID,IPC_SETVAL,IPC_STAT:成功返回 0,失败返回 -1
    * - IPC_GETVAL:返回信号量的当前值
 */
int semctl(int semid, int semnum, int cmd, ...);

union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
}

semop() | 操作信号量

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

/**
 * 操作信号量
 * @param semid 信号量的标识符
 * @param sops 信号量操作结构体数组
 * @param nsops 信号量操作结构体数组的大小
 * @return 成功返回 0,失败返回 -1
 */
int semop(int semid, struct sembuf *sops, unsigned nsops);

struct sembuf
{
    short sem_num; /* 信号量编号 */
    short sem_op;  /* 信号量操作 */
    short sem_flg; /* 操作标志 */
}

EG

本实例说明信号量的概念以及基本用法。在实例程序中,首先创建一个子进程,接下来使用信号量来控制两个进程(父子进程)之间的执行顺序。

因为信号量相关的函数调用接口比较复杂,我们可以将它们封装成二维单个信号量的几个基本函数。它们分别为信号量初始化函数(或者信号量赋值函数)init_sem()、P 操作函数 sem_p()、V 操作函数 sem_v()以及删除信号量的函数 del_sem()等,具体实现如下所示:

c
/* sem_com.c */
#include "sem_com.h"
/* 信号量初始化(赋值)函数*/
int init_sem(int sem_id, int init_value)
{
    union semun sem_union;
    sem_union.val = init_value; /* init_value 为初始值 */
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
    {
        perror("Initialize semaphore");
        return -1;
    }
    return 0;
}
/* 从系统中删除信号量的函数 */
int del_sem(int sem_id)
{
    union semun sem_union;
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
    {
        perror("Delete semaphore");
        return -1;
    }
}
/* P 操作函数 */
int sem_p(int sem_id)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0;        /* 单个信号量的编号应该为 0 */
    sem_b.sem_op = -1;        /* 表示 P 操作 */
    sem_b.sem_flg = SEM_UNDO; /* 系统自动释放将会在系统中残留的信号量*/
    if (semop(sem_id, &sem_b, 1) == -1)
    {
        perror("P operation");
        return -1;
    }
    return 0;
}
/* V 操作函数*/
int sem_v(int sem_id)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0;        /* 单个信号量的编号应该为 0 */
    sem_b.sem_op = 1;         /* 表示 V 操作 */
    sem_b.sem_flg = SEM_UNDO; /* 系统自动释放将会在系统中残留的信号量*/
    if (semop(sem_id, &sem_b, 1) == -1)
    {
        perror("V operation");
        return -1;
    }
    return 0;
}
c
/* fork.c */
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define DELAY_TIME 3 /* 为了突出演示效果,等待几秒钟,*/
int main(void)
{
    pid_t result;
    int sem_id;
    sem_id = semget(ftok(".", 'a'), 1, 0666 | IPC_CREAT); /* 创建一个信号量*/
    init_sem(sem_id, 0);
    /*调用 fork()函数*/
    result = fork();
    if (result == -1)
    {
        perror("Fork\n");
    }
    else if (result == 0) /*返回值为 0 代表子进程*/
    {
        printf("Child process will wait for some seconds...\n");
        sleep(DELAY_TIME);
        printf("The returned value is %d in the child process(PID = %d)\n",
               result, getpid());
        sem_v(sem_id);
    }
    else /*返回值大于 0 代表父进程*/
    {
        sem_p(sem_id);
        printf("The returned value is %d in the father process(PID = %d)\n",
               result, getpid());
        sem_v(sem_id);
        del_sem(sem_id);
    }
    exit(0);
}

共享内存 | shared memory

相关原理可见操作系统笔记

shmget() | 创建共享内存

c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

/**
 * 创建共享内存
 * @param key 共享内存的键值,不同进程可以通过它访问同一个共享内存,若为 IPC_PRIVATE,则只能由创建者进程访问
 * @param size 共享内存的大小
 * @param shmflg 共享内存的权限
 * @return 成功返回共享内存的标识符,失败返回 -1
 */
int shmget(key_t key, size_t size, int shmflg);

shmat() | 连接共享内存

c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

/**
 * 连接共享内存
 * @param shmid 共享内存的标识符
 * @param shmaddr 指定共享内存连接到当前进程的地址位置,通常为空,表示让系统来选择共享内存的地址
 * @param shmflg 共享内存的权限
 * @return 成功返回共享内存的地址,失败返回 -1
 */
void *shmat(int shmid, const void *shmaddr, int shmflg);

shmdt() | 断开共享内存

c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

/**
 * 断开共享内存
 * @param shmaddr 共享内存的地址
 * @return 成功返回 0,失败返回 -1
 */
int shmdt(const void *shmaddr);

EG

该实例说明如何使用基本的共享内存函数。首先是创建一个共享内存区(采用的共享内存的键值为 IPC_PRIVATE,是因为本实例中创建的共享内存是父子进程之间的共用部分),之后创建子进程,在父子两个进程中将共享内存分别映射到各自的进程地址空间之中。

父进程先等待用户输入,然后将用户输入的字符串写入到共享内存,之后往共享内存的头部写入“ WROTE”字符串表示父进程已成功写入数据。子进程一直等到共享内存的头部字符串为“WROTE”,然后将共享内存的有效数据(在父进程中用户输入的字符串)在屏幕上打印。父子两个进程在完成以上工作之后,分别解除与共享内存的映射关系。

最后在子进程中删除共享内存。因为共享内存自身并不提供同步机制,所以应该额外实现不同进程之间的同步(例如:信号量)。为了简单起见,在本实例中用标志字符串来实现非常简单的父子进程之间的同步。

这里要介绍的一个命令是 ipcs,这是用于报告进程间通信机制状态的命令。它可以查看共享内存、消息队列等各种进程间通信机制的情况,这里使用了 system()函数用于调用 shell 命令“ipcs”。程序源代码如下所示:

c
/* shmem.c */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER_SIZE 2048
int main()
{
 pid_t pid;
 int shmid;
 char *shm_addr;
 char flag[] = "WROTE";
 char *buff;

 /* 创建共享内存 */
 if ((shmid = shmget(IPC_PRIVATE, BUFFER_SIZE, 0666)) < 0)
 {
 perror("shmget");
 exit(1);
 }
 else
 {
 printf("Create shared-memory: %d\n",shmid);
 }

 /* 显示共享内存情况 */
 system("ipcs -m");

 pid = fork();
 if (pid == -1)
 {
 perror("fork");
 exit(1);
 }
 else if (pid == 0) /* 子进程处理 */
 {
 /*映射共享内存*/
 if ((shm_addr = shmat(shmid, 0, 0)) == (void*)-1)
 {
 perror("Child: shmat");
 exit(1);
 }
 else
 {
 printf("Child: Attach shared-memory: %p\n", shm_addr);
 }
 system("ipcs -m");

 /* 通过检查在共享内存的头部是否标志字符串"WROTE"来确认
父进程已经向共享内存写入有效数据 */
 while (strncmp(shm_addr, flag, strlen(flag)))
 {
 printf("Child: Wait for enable data...\n");
 sleep(5);
 }

 /* 获取共享内存的有效数据并显示 */
 strcpy(buff, shm_addr + strlen(flag));
 printf("Child: Shared-memory :%s\n", buff);

 /* 解除共享内存映射 */
 if ((shmdt(shm_addr)) < 0)
 {
 perror("shmdt");
 exit(1);
 }
 else
 {
 printf("Child: Deattach shared-memory\n");
 }
 system("ipcs -m");

 /* 删除共享内存 */
 if (shmctl(shmid, IPC_RMID, NULL) == -1)
 {
 perror("Child: shmctl(IPC_RMID)\n");
 exit(1);
 }
 else
 {
 printf("Delete shared-memory\n");
 }

 system("ipcs -m");
 }
 else /* 父进程处理 */
 {
 /*映射共享内存*/
 if ((shm_addr = shmat(shmid, 0, 0)) == (void*)-1)
 {
 perror("Parent: shmat");
 exit(1);
 }
 else
 {
 printf("Parent: Attach shared-memory: %p\n", shm_addr);
 }

 sleep(1);
 printf("\nInput some string:\n");
 fgets(buff, BUFFER_SIZE, stdin);
 strncpy(shm_addr + strlen(flag), buff, strlen(buff));
 strncpy(shm_addr, flag, strlen(flag));

 /* 解除共享内存映射 */
 if ((shmdt(shm_addr)) < 0)
 {
 perror("Parent: shmdt");
 exit(1);
 }
 else
 {
 printf("Parent: Deattach shared-memory\n");
 }
 system("ipcs -m");

 waitpid(pid, NULL, 0);
 printf("Finished\n");
 }
 exit(0);
}


// $ ./shmem
// Create shared-memory: 753665
// /* 在刚创建共享内存时(尚未有任何地址映射)共享内存的情况 */
// ------ Shared Memory Segments --------
// key shmid owner perms bytes nattch status
// 0x00000000 753665 david 666 2048 0
// Child: Attach shared-memory: 0xb7f59000 /* 共享内存的映射地址 */
// Parent: Attach shared-memory: 0xb7f59000
// /* 在父子进程中进行共享内存的地址映射之后共享内存的情况*/
// ------ Shared Memory Segments --------
// key shmid owner perms bytes nattch status
// 0x00000000 753665 david 666 2048 2
// Child: Wait for enable data...
// Input some string:
// Hello /* 用户输入字符串“Hello” */
// Parent: Deattach shared-memory
// /* 在父进程中解除共享内存的映射关系之后共享内存的情况 */
// ------ Shared Memory Segments --------
// key shmid owner perms bytes nattch status
// 0x00000000 753665 david 666 2048 1
// /*在子进程中读取共享内存的有效数据并打印*/
// Child: Shared-memory :hello
// Child: Deattach shared-memory
// /* 在子进程中解除共享内存的映射关系之后共享内存的情况 */
// ------ Shared Memory Segments --------
// key shmid owner perms bytes nattch status
// 0x00000000 753665 david 666 2048 0
// Delete shared-memory
// /* 在删除共享内存之后共享内存的情况 */
// ------ Shared Memory Segments --------
// key shmid owner perms bytes nattch status
// Finished

消息队列 | message queue

顾名思义,消息队列就是一些消息的列表。用户可以从消息队列中添加消息和读取消息等。从这点上看,消息队列具有一定的 FIFO 特性,但是它可以实现消息的随机查询,比 FIFO 具有更大的优势。同时,这些消息又是存在于内核中的,由“队列 ID”来标识。

可以取走指定的消息

msgget() | 创建消息队列

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

/**
 * 创建消息队列
 * @param key 消息队列的键值,不同进程可以通过它访问同一个消息队列,若为 IPC_PRIVATE,则只能由创建者进程访问
 * @param msgflg 消息队列的权限
 * @return 成功返回消息队列的标识符,失败返回 -1
 */
int msgget(key_t key, int msgflg);

msgsnd() | 发送消息

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

/**
 * 发送消息
 * @param msqid 消息队列的标识符
 * @param msgptr 指向要发送的消息的指针
 * @param msgsz 消息的大小
 * @param msgflg 发送消息的标志
 * @return 成功返回 0,失败返回 -1
 */
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

msgrcv() | 接收消息

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

/**
 * 接收消息
 * @param msqid 消息队列的标识符
 * @param msgptr 指向接收消息缓冲区的指针
 * @param msgsz 消息的大小
 * @param msgtyp 指定接收消息的类型
 * @param msgflg 接收消息的标志
 * @return 成功返回接收到消息的大小,失败返回 -1
 */
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgctl() | 控制消息队列

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

/**
 * 控制消息队列
 * @param msqid 消息队列的标识符
 * @param cmd 指定对消息队列的操作
 * - IPC_RMID:删除消息队列
 * - IPC_STAT:把 msqid_ds 结构中的数据设置为消息队列的当前值
 * - IPC_SETVAL:将消息队列的值设置为 arg 的 val 值
 * - IPC_GETVAL:返回消息队列的当前值
 * @param buf 可选参数
 * @return 成功返回 0,失败返回 -1
 */
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

struct msqid_ds
{
    struct ipc_perm msg_perm; /* 消息队列的操作权限 */
    time_t msg_stime;         /* 最后一次消息发送的时间 */
    time_t msg_rtime;         /* 最后一次消息接收的时间 */
    time_t msg_ctime;         /* 最后一次消息变更的时间 */
    unsigned long msg_cbytes; /* 消息队列中当前的字节数 */
    msgqnum_t msg_qnum;       /* 消息队列中当前的消息数 */
    msglen_t msg_qbytes;      /* 消息队列中允许的最大字节数 */
    pid_t msg_lspid;          /* 最后一次发送消息的进程 ID */
    pid_t msg_lrpid;          /* 最后一次接收消息的进程 ID */
}

EG

这个实例体现了如何使用消息队列进行两个进程(发送端和接收端)之间的通信,包括消息队列的创建、消息发送与读取、消息队列的撤消和删除等多种操作。

消息发送端进程和消息接收端进程之间不需要额外实现进程之间的同步。在该实例中,发送端发送的消息类型设置为该进程的进程号(可以取其他值),因此接收端根据消息类型确定消息发送者的进程号。注意这里使用了函数 fotk(),它可以根据不同的路径和关键字产生标准的 key。以下是消息队列发送端的代码:

c
/* msgsnd.c */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 512
struct message
{
 long msg_type;
 char msg_text[BUFFER_SIZE];
};
int main()
{
 int qid;
 key_t key;
 struct message msg;

 /*根据不同的路径和关键字产生标准的 key*/
 if ((key = ftok(".", 'a')) == -1)
 {
 perror("ftok");
 exit(1);
 }

 /*创建消息队列*/
 if ((qid = msgget(key, IPC_CREAT|0666)) == -1)
 {
 perror("msgget");
 exit(1);
 }
 printf("Open queue %d\n",qid);

 while(1)
 {
 printf("Enter some message to the queue:");
 if ((fgets(msg.msg_text, BUFFER_SIZE, stdin)) == NULL)
 {
 puts("no message");
 exit(1);
 }

 msg.msg_type = getpid();

 /*添加消息到消息队列*/
 if ((msgsnd(qid, &msg, strlen(msg.msg_text), 0)) < 0)
 {
 perror("message posted");
 exit(1);
 }

 if (strncmp(msg.msg_text, "quit", 4) == 0)
 {
 break;
 }
 }
 exit(0);
}

以下是消息队列接收端的代码

c
/* msgrcv.c */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 512
struct message
{
 long msg_type;
 char msg_text[BUFFER_SIZE];
};
int main()
{
 int qid;
 key_t key;
 struct message msg;

 /*根据不同的路径和关键字产生标准的 key*/
 if ((key = ftok(".", 'a')) == -1)
 {
 perror("ftok");
 exit(1);
 }

 /*创建消息队列*/
 if ((qid = msgget(key, IPC_CREAT|0666)) == -1)
 {
 perror("msgget");
 exit(1);
 }
 printf("Open queue %d\n", qid);

 do
 {
 /*读取消息队列*/
 memset(msg.msg_text, 0, BUFFER_SIZE);
 if (msgrcv(qid, (void*)&msg, BUFFER_SIZE, 0, 0) < 0)
 {
 perror("msgrcv");
 exit(1);
 }
 printf("The message from process %d : %s", msg.msg_type, msg.msg_text);

 } while(strncmp(msg.msg_text, "quit", 4));

 /*从系统内核中移走消息队列 */
 if ((msgctl(qid, IPC_RMID, NULL)) < 0)
 {
 perror("msgctl");
 exit(1);
 }

 exit(0);
}
shell
$ ./msgsnd
Open queue 327680
Enter some message to the queue:first message
Enter some message to the queue:second message
Enter some message to the queue:quit
$ ./msgrcv
Open queue 327680
The message from process 6072 : first message
The message from process 6072 : second message
The message from process 6072 : quit

Copyright © 2022 田园幻想乡 浙ICP备2021038778号-1