apue 第15章 进程间通信

引言

进程间相互通信的其他技术-进程间通信

  • 管道
  • FIFO
  • 消息队列
  • 信号量
  • 共享内存
  • POSIX信号量

管道

什么是管道

统操作执行命令的时候,经常有需求要将一个程序的输出交给另一个程序进行处理,这种操作可以使用输入输出重定向加文件。如下面的命令:

ubuntu@VM-188-113-ubuntu:~$ ls -l /etc/ > etc.txt
ubuntu@VM-188-113-ubuntu:~$ wc -l etc.txt
179 etc.txt

但是这样未免显得太麻烦了。所以,管道的概念应运而生。目前在任何一个shell中,都可以使用|连接两个命令,shell会将前后两个进程的输入输出用一个管道相连,以便达到进程间通信的目的:

ubuntu@VM-188-113-ubuntu:~$ ls -l /etc/ | wc -l
179

管道本质上就是一个文件,前面的进程以写方式打开文件,后面的进程以读方式打开。这样前面写完后面读,于是就实现了通信。实际上管道的设计也是遵循UNIX的“一切皆文件”设计原则的,它本质上就是一个文件。Linux系统直接把管道实现成了一种文件系统,借助VFS给应用程序提供操作接口。

虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux上的管道就是一个操作方式为文件的内存缓冲区。

管道是UNIX系统IPC的最古老的形式,所有的UNIX系统都提供这一种通信机制。局限性:

  • 历史上,它们是半双工的
  • 管道只能在具有公共祖先的两个进程之间使用
管道实现机制

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

从原理上,管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。最开始的时候,上面的两个箭头都连接在同一个进程Process 1上(连接在Process 1上的两个箭头)。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (两个黑色的箭头被关闭; Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了如上图的PIPE。

实现细节

在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如下图

有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。

管道是通过pipe函数创建的

#include <unistd.h>
int pipe(int filedes[2]);

filedes[0]用于读出数据,读取时必须关闭写入端,即close(filedes[1]);
filedes[1]用于写入数据,写入时必须关闭读取端,即close(filedes[0])。

管道的读写规则

管道读函数pipe_read()及管道写函数pipe_wrtie()。管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。

当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:

  • 内存中有足够的空间可容纳所有要写入的数据;
  • 内存没有被读程序锁定。

如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。

管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。

一个使用pipe的例子:

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

#define MAXLINE 4096

void ERR_EXIT(const char* m)
{
    perror(m);
    exit(EXIT_FAILURE);
}

int main()
{
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];

    if (pipe(fd) < 0)
        ERR_EXIT("pipe error!");

    if ((pid = fork()) < 0)
        ERR_EXIT("fork error!");
    else if (pid > 0)
    {
        close(fd[0]);
        write(fd[1], "hello world\n", 12);
    }
    else
    {
        close(fd[1]);
        n = read(fd[0], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }

    exit(0);
}

函数popen和pclose

常见的操作是创建一个连接到另一个进程的管道,然后读其输出或者向其输入端发送数据。标准I/O库提供了两个函数popen和pclose。这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,然后等待命令终止。

#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
//返回值:若成功,返回文件指针;若出错,返回NULL
int pclose(FILE *fp);
//返回值:若成功,返回cmdstring的终止状态;若出错,返回-1
  • 函数popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O文件指针。如果type是“r”,则文件指针连接到cmdstring的标准输出。 如果type是“w”,则文件指针连接到cmdstring的标准输入
  • pclose函数关闭标准I/O流。等待命令终止,然后返回shell的终止状态。如果shell不能被执行,则pclose返回的终止状态与shell已执行exit一样

协同进程

UNIX系统过滤程序从标准输入读取数据,对其进行适当处理后写到标准输出。几个过滤程序通常在shell管道命令行中线性地连接。当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程(coprocess)。

进程线创建两个管道:一个是协同进程的标准输入,另一个是协同进程的标准输出。如下图所示:

XSI IPC

有3中IPC称为XSI IPC,即消息队列、信号量以及共享存储器,它们之间有很多相似之处。

标识符和键
  • 每个内核中的IPC结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符(identifier)加以引用.
  • 标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上会合,需要提供一个外部名方案。为此使用了键(key),每个IPC对象都与一个键相关联,于是键就用作为该对象的外部名。
  • 创建IPC结构都应指定一个键,键的数据类型是基本系统数据类型key_t,通常在头文件
#include <sys/ipc.h>
key_t ftok(const char *path, int id);
//返回值:若成功则返回键,若出错则返回(key_t)-1
  • path参数必须引用一个现存文件。当产生键时,只是用id参数的低8位。
  • ftok创建的键通常是下列方式构成的:按给定的路径名取得其stat结构,该结构中取出部分st_dev和st_ino字段,然后再与项目ID组合起来
权限结构

XSI IPC结构设置了一个ipc_perm结构。该结构规定了权限和所有者,至少包括以下成员:

struct ipc_perm {
    uid_t    uid;     /* owner's effective user id */
    gid_t    gid;     /* owner's effective group id */
    uid_t    cuid;    /* creator's effective user id */
    gid_t    cgid;    /* creator's effective group id */
    mode_t   mode;    /* access modes */
    ...
};
结构限制

三种形式的XSI IPC都有内置限制(built-in limit)。这些限制的大多数可以通过重新配置内核而加以更改。

消息队列

消息队列是消息的链接表,存放在内核中并由消息队列标识符标识。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。

消息队列的操作

消息队列的操作有3种类型:

  • 打开或创建消息队列:获得一个消息队列的描述子,只需要提供该消息队列的键值
  • 读写操作:消息类型为如下所示的结构体:
struct mymegs
{
    long mtype;//message type
    char mtext[BUFFERSIZE];//the length of the message
}
  • 获得或设置消息队列的属性:消息队列的信息保存在消息队列的数据结构中,因此可以分配一个消息队列数据结构(struct msqid_ds),返回消息队列的属性或者设置消息队列属性。
    结构体如下:
struct msqid_ds {
    struct ipc_perm    msg_perm;      /* */
    msgqnum_t          msg_qnum;      /* # of messages on queue */
    msglen_t           msg_qbytes;    /* max # of bytes no queue */
    pid_t              msg_lspid;     /* pid of last msgsnd() */
    pid_t              msg_lrpid;     /* pid of last msgrcv() */
    time_t             msg_stime;     /* last-msgsnd() time */
    time_t             msg_rtime;     /* last-msgrcv() time */
    time_t             msg_ctime;     /* last-change time */
    ...
};
文件名到键值
#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok (char*pathname, char proj);
  • 返回路径pahtname相对应得一个键值
  • 往往要先调用这个函数,然后得到key
msgget函数
#include <sys/msg.h>
int msgget(key_t key, int flag);
返回值:若成功则返回消息队列ID,若出错则返回-1
  • 打开或创建一个消息队列
  • 当创建一个消息队列时,mysqid_ds结构的成员如下:
    • ipc_perm按前面所述的权限结构初始化
    • msg_qnum、msg_lspid、msg_lrpid、msg_stime和msg_rtime都设置为0。
    • msg_ctime设置为当前时间。
    • msg_qbytes设置为系统限制值。
  • 若执行成功,msgget返回非负队列ID。此后,该值就可被用于其他三个消息队列函数(msgsnd、msgrcv和msgctl)。
  • flag为IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者的或结果:
    • 当只有IPC_CREAT选项打开时,若不存在则创建信号量集返回该ID,若存在,则都返回该信号量集的ID,
    • 当只有IPC_EXCL选项打开时,不管有没有该信号量集,shmget()都返回-1
    • 所以当IPC_CREAT | IPC_EXCL时, 如果没有该信号量,则创建,并返回ID。若已有该信号量集,则返回-1;
msgsnd函数
#include <sys/msg.h>
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
返回值:若成功则返回0,若出错则返回-1
  • 每个消息都由三部分组成,它们是:正长整型类型字段、非负长度(nbytes)以及实际数据字节(对应于长度)。消息总是放在队列尾端。
  • ptr就是一个指向mymesg结构的指针。接收者可以使用消息类型以非先进先出的次序取消息。
  • flag:可以指定为IPC_NOWAIT,类似于文件I/O的非阻塞I/O标志;没有指定IPC_NOWAIT,则进程阻塞直到下述情况出现为止:有空间可以容纳要发送的消息;从系统中删除了此队列;或捕捉到一个信号,并从信号处理程序返回。
  • 当msgsnd成功返回,与消息队列相关的msqid_ds结构得到更新,以表明发出该调用的进程ID(msg_lspid)、进行该调用的时间(msg_stime),并指示队列中增加了一条消息(msg_qnum)
msgrcv函数
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
返回值:若成功则返回消息的数据部分的长度,若出错则返回-1
  • 如同msgsnd中一样,ptr参数指向一个长整型数(返回的消息类型存放在其中),跟随其后的是存放实际消息数据的缓冲区。
  • nbytes说明数据缓冲区的长度。若返回的消息大于nbytes,而且在flag中设置了MSG_NOERROR,则该消息被截短。
  • type:
    • type == 0 返回队列中的第一个消息。
    • type > 0 返回队列中消息类型为type的第一个消息。
    • type < 0 返回队列中消息类型值小于或等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
  • flag和msgsnd函数中相同
  • msgrcv成功执行时,内核更新与该消息队列相关联的msqid_ds结构,以指示调用者的进程ID(msg_lrpid)和调用时间(msg_rtime),并将队列中的消息数(msg_qnum)减1。
msgctl函数

msgctl对队列执行多种操作

#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
返回值:若成功则返回0,若出错则返回-1

cmd参数说明对由msqid指定的队列要执行的命令:

  • IPC_STAT 取此队列的msqid_ds结构,并将它存放在buf指向的结构中。
  • IPC_SET 按由buf指定结构中的值,设置与此队列相关结构中的下列四个字段:msg_perm.uid、msg_perm.gid,msg_perm.mode和msg_qbytes。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid;另一种是具有超级用户特权的进程。只有超级用户才能增加msg_qbytes的值。
  • IPC_RMID 从系统中删除该消息队列以及仍在该队列中的所有数据。这种删除立即生效。仍在使用这一消息队列的其他进程在它们下一次试图对此队列进行操作时,将出错返回EIDRM。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid;另一种是具有超级用户特权的进程。

例子

msgreceive.c接受消息,类型为0,表示接受第一个可用消息。msgsend.c发送消息,发送类型为1.

msgreceive.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>

#define BUFSIZE 512

struct msg_st
{
    long int msg_type;
    char text[BUFSIZE];
};

void err_exit(const char* m)
{
    perror(m);
    exit(EXIT_FAILURE);
}

int main()
{
    int running = 1;
    int msgid = -1;
    struct msg_st data;
    long int msgtype = 0;

    //create message queue
    msgid = msgget((key_t)1212, 0666 | IPC_CREAT);
    if (msgid == -1)
        err_exit("create message queue error");
    while (running)
    {
        //receive  message
        if (msgrcv(msgid, (void*)(&data), BUFSIZE, msgtype, 0) == -1)
            err_exit("recieve message error.");
        printf("You wrote:%s\n", data.text);

        if (strncmp(data.text, "end", 3) == 0)
            running = 0;
    }
    //delete message
    if (msgctl(msgid, IPC_RMID, 0) == -1)
        err_exit("delete message error.");

    exit(0);
};

msgsend.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <error.h>
#include <sys/msg.h>

#define MAX_TEXT 512

struct msg_st
{
    long int msg_type;
    char text[MAX_TEXT];
};

void err_exit(const char* m)
{
    perror(m);
    exit(EXIT_FAILURE);
}

int main()
{
    int running = 1;
    struct msg_st data;
    char buffer[MAX_TEXT];
    int msgid = -1;

    //create message queue
    msgid = msgget((key_t)1212, 0666|IPC_CREAT);
    if (msgid == -1)
        err_exit("create message queue error.");
    while (running)
    {
        printf("Enter some text:\n");
        fgets(buffer, MAX_TEXT, stdin);
        data.msg_type = 1;
        strcpy(data.text, buffer);
        if (msgsnd(msgid, (void *)(&data), MAX_TEXT, 0) == -1)
            err_exit("send message error.");
        if (strncmp(buffer, "end", 3) == 0)
            running = 0;
        sleep(1);
    }

    exit(0);
}

运行结果

ubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$ ./msgreveive &
[1] 18484
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$ ./msgsend 
Enter some text:
test
You wrote:test

Enter some text:
yanke
You wrote:yanke

Enter some text:
end
You wrote:end

[1]+  Done                    ./msgreveive

信号量

信号量是一个计数器,用于多个进程对数据对象共享访问。为了获得共享资源,进程需要执行的操作为:

  • 测试控制该资源的信号量。
  • 若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示它使用了一个资源单位。
  • 若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0.进程被唤醒后,它返回至第(1)步。

当进程不再使用由一个信号量控制的共享资源时,该信号量值增1。如果有进程正在休眠等待此信号量,则唤醒它们。为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。

内核为每个信号量维护的数据结构semid_ds

struct semid_ds {
    struct ipc_perm    sem_perm;    
    unsigned short     sem_nsems;    /* # of semaphores in set */
    time_t             sem_otime;    /* last-semop() time */
    time_t             sem_ctime;    /* last-change time */
    ...
};

每个信号量都有一个无名的表示,它至少包含一下成员:

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 */
};

在Linux系统中使用信号量的四个步骤

  • 创建信号量或获得在系统中已存在的信号量,此时需要调用 semget() 函数。不同进程通过使用同一个信号量键值来获得同一个信号量。
  • 初始化信号量,此时使用 semctl() 函数的SETVAL操作。当使用二维信号量时,通常将信号量初始化为1。
  • 进行信号量的PV操作,此时,调用 semop()函数。这一步是实现进程间的同步和互斥的核心工作部分。
  • 如果不需要信号量,则从系统中删除它,此时使用semctl()函数的 IPC_RMID操作。需要注意的是,在程序中不应该出现对已经被删除的信号量的操作。

创建或使用一个信号量

#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
返回值:若成功则返回信号量ID,若出错则返回-1
  • key为信号量的键值
  • nsems为信号量的数目
  • flag和open()函数的权限位类似。当使用IPC_CREAT时表示创建一个信号量,当使用IPC_EXCL标志创建唯一的信号量

semctl函数包含了多种信号量操作

#include < sys/sem.h>
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
  • semid:semget()函数返回的信号标识符
  • semnum:信号量编号,当使用信号量集才用到,通常取值为0
  • cmd:

    • IPC_STAT 对此集合取semid_ds结构,并存放在由arg.buf指向的结构中。
    • IPC_SET 按由arg.buf指向结构中的值设置与此集合相关结构中的下列三个字段值:sem_perm.uid、sem_perm.gid和sem_perm.mode。此命令只能由下列两种进程执行:一种是其有效用户ID等于sem_perm.cuid或sem_perm.uid的进程;另一种是具有超级用户特权的进程。
    • IPC_RMID 从系统中删除该信号量集合。这种删除是立即发生的。
    • GETVAL 返回成员semnum的semval值。
    • SETVAL 设置成员semnum的semval值。该值由arg.val指定。
    • GETPID 返回成员semnum的sempid值。
    • GETNCNT 返回成员semnum的semncnt值。
    • GETZCNT 返回成员semnum的semzcnt值。
    • GETALL 取该集合中所有信号量的值,并将它们存放在由arg.array指向的数组中。
    • SETALL 按arg.array指向的数组中的值,设置该集合中所有信号量的值。
  • arg是一个union,这是一个联合,而非指向联合的指针。

union semun {
    int                 val;      /* for SETVAL */
    struct semid_ds    *buf;      /* for IPC_STAT and IPC_SET */
    unsigned short     *array;    /* for GETALL and SETALL */
};
  • 对于除GETALL以外的所有GET命令,semctl函数都返回相应的值。其他命令的返回值为0.

函数semop自动执行信号量集合上的操作数组,这是个原子操作。

#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
返回值:若成功则返回0,若出错则返回-1
  • 参数semoparray是一个指针,它指向一个信号量操作数组,信号量操作由sembuf结构表示:
struct sembuf {
    unsigned short    sem_num;         /* member # in set ( 0, 1, ..., nsems-1) */
    short             sem_op;          /* operation (negtive, 0, or positive) */
    short             sem_flag;        /* IPC_NOWAIT, SEM_UNDO */
};
  • nops规定该数组中操作的数量
  • 对集合中每个成员的操作由相应的sem_op值规定。此值可以是负值、0或正值。
    • sem_op为正。这对应于进程释放占用的资源数。sem_op值加到信号量的值上。如果指定了undo标志(此标志对应于相应sem_flag成员的SEM_UNDO位),则也从该进程的此信号量调整值中减去sem_op。
    • 若sem_op为负,则表示要获取由该信号量控制的资源。
    • 若sem_op为0,这表示调用进程希望等待到该信号量值变成0。

使用信号量的例子:

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

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

static int sem_id = 0;

void err_exit(const char* m)
{
    perror(m);
    exit(EXIT_FAILURE);
}


int set_semvalue()
{
    //init semaphores
    union semun sem_union;
    sem_union.val = 1;
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
        //err_exit("init sempaores error!");
        return 0;

    return 1;
}

void del_semvalue()
{
    union semun sem_union;

    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
        err_exit("delete semaphores error!");
}

int sempahore_p()
{
    //p operator
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = -1;
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1)
       // err_exit("p operator error!");
       return 0;

    return 1;
}

int semaphore_v()
{
    //v operator
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = 1;
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1)
        //err_exit("v operator error!");
        return 0;

    return 1;
}

int main(int argc, char** argv)
{
    char message = 'x';
    int i = 0;

    //create semaphore
    sem_id = semget((key_t)1212, 1, 0666 | IPC_CREAT );

    if (argc > 1)
    {
        //first use semaphore
        if (!set_semvalue())
            err_exit("init semaphore error!");
        message = argv[1][0];
        sleep(2);
    }

    for (i = 0; i < 10; ++i)
    {
        //进入临界区
        if(!sempahore_p())
            err_exit("p operator error!");
        printf("%c ", message);
        fflush(stdout);
        sleep(rand() % 3);
        printf("%c ", message);
        fflush(stdout);
        if (!semaphore_v())
            err_exit("p operator error!");
        sleep(rand() % 2);
    }

    sleep(10);
    printf("\n %d - finished", getpid());

    if (argc > 1)
    {
        sleep(3);
        del_semvalue();
    }

    exit(0);
}

输出:

ubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$ ./seml y & ./seml [1] 22522
x x x x y y x x y y x x y y x x x x y y x x y y x x y y x x x x y y y y y y y y 
 22523 - finishedubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$ 
 22522 - finished
 [1]+  Done                    ./seml y

共享存储

XSI共享存储和内存映射的文件不同之处在于,前者没有相关的文件,XSI共享存储段的内存的匿名段。内核维护的共享段的数据结构:

struct shmid_ds {
    struct ipc_perm    shm_perm;    
    size_t             shm_segsz;       /* size of segment in bytes */
    pid_t              shm_lpid;        /* pid of last shmop() */
    pid_t              shm_cpid;        /* pid of creator */
    shmatt_t           shm_nattch;      /* number of current attaches */
    time_t             shm_atime;       /* last-attach time */
    time_t             shm_dtime;       /* last-detach tiime */
    time_t             shm_ctime;       /* last-change time */
    ...
};

shmatt_t类型定义为不带符号整型,它至少与unsigned short一样大。

共享内存的使用

获得一个共享内存标志符

#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
返回值:若成功则返回共享存储ID,若出错则返回-1
  • size为共享内存的长度

对共享内存段执行多种操作

#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回值:若成功则返回0,若出错则返回-1
  • cmd参数:
    • IPC_STAT 取此段的shmid_ds结构,并将它存放在由buf指向的结构中。
      * IPC_SET 按buf指向结构中的值设置与此段相关结构中的下列三个字段:shm_perm.uid、shm_perm.gid以及shm_perm.mode。
      * IPC_RMID 从系统中删除该共享存储段。
  • buf是一个结构指针,它指向共享内存模式和访问权限的结构。

一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中。

#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
返回值:若成功则返回指向共享存储的指针,若出错则返回-1
  • 共享存储段连接到调用进程的哪个地址上与addr参数以及在flag中是否指定SHM_RND位有关。

    • 如果addr为0,则此段连接到由内核选择的第一个可用地址上。这是推荐的使用方式。
    • 如果addr非0,并且没有指定SHM_RND,则此段连接到addr所指定的地址上。
    • 如果addr非0,并且指定了SHM_RND,则此段连接到(addr-(addr mod ulus SHMLBA))所表示的地址上。SHM_RND命令的意思是“取整”。SHMLBA的意思是“低边界地址倍数”,它总是2的乘方。该算式是将地址向下取最近1个SHMLBA的倍数
  • 在flag中指定了SHM_RDONLY位,则以只读方式连接此段。否则以读写方式连接此段。

当对共享存储段的操作已经结束时,则调用shmdt脱接该段。

#include <sys/shm.h>
int shmdt(void *addr);
返回值:若成功则返回0,若出错则返回-1
  • addr参数是以前调用shmat时的返回值。如果成功,shmdt将使相关shmid_ds结构中的shm_nattach计数器值减1。
例子

shmdata.h

#ifndef __SHM_DATA_H__
#define __SHM_DATA_H__

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

#define TEXT_SZ 2048

struct shared_use_st
{
    int written;//flag:0 writeable, others readable
    char text[TEXT_SZ];//record text
};

void err_exit(const char* m)
{
    perror(m);
    exit(EXIT_FAILURE);
}

#endif

shmread.c

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/shm.h>
#include "shmdata.h"

int main()
{
    int running = 1;
    void* shm = NULL; //address of the shared memory
    struct shared_use_st *shared;
    int shmid;
    //create shared memory
    shmid = shmget((key_t)1212, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
    if (shmid == -1)
        err_exit("create shared memory error!");

    //attach address to the process
    shm = shmat(shmid, 0, 0);
    if (shm == (void*)-1)
        err_exit("shmat error.");
    printf("\n Memory attached at %X\n", (int)shm);

    //set shared memory
    shared = (struct shared_use_st*)shm;
    shared->written = 0;//fist write
    while (running)
    {
        //read process
        if (shared->written != 0)
        {
            //print the text
            printf("You wrote : %s\n", shared->text);
            sleep(rand() % 3);
            //then set shared writable
            shared->written = 0;
            if (strncmp(shared->text, "end", 3) == 0)
                running = 0;
        }
        else// other process is writing text
            sleep(1);
    }

    if (shmdt(shm) == -1)
        err_exit("shmdt error.");
    if (shmctl(shmid, IPC_RMID, 0) == -1)
        err_exit("shctl failed!");

    exit(0);
}

shmwrite.h

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include "shmdata.h"

int main()
{
    int running = 1;
    void* shm = NULL;
    struct shared_use_st* shared = NULL;
    char buffer[BUFSIZ];//the text
    int shmid;
    //create
    shmid = shmget((key_t)1212, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if (shmid == -1)
        err_exit("shmget error!");
    shm = shmat(shmid, (void*)0,0);
    if (shm == (void*)-1)
        err_exit("shmat error!");
    printf("memory attached at %x\n", (int)shm);

    shared = (struct shared_use_st*)shm;
    while(running)
    {
        while (shared->written == 1)
        {
            sleep(1);
            printf("Waiting ...\n");
        }

        printf("Enter some text:\n");
        fgets(buffer, BUFSIZ, stdin);
        strncpy(shared->text, buffer,TEXT_SZ);
        shared->written = 1;
        if (strncmp(buffer, "end", 3) == 0)
            running = 0;
    }

    if (shmdt(shm) == -1)
        err_exit("shmdt failed!");
    sleep(2);
    exit(0);
}

输出:

ubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$ ./shmread &
[1] 30889
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$ 
 Memory attached at 2EC64000
./shmwrite
memory attached at e66cd000
Enter some text:
test
You wrote : test

Waiting ...
Waiting ...
Enter some text:
yanke
You wrote : yanke

Waiting ...
Waiting ...
Enter some text:
end
You wrote : end

[1]+  Done                    ./shmread

POSIX信号量

POSIX信号量相比于XSI信号量:

  • POSIX信号量接口相比于XSI信号量接口,允许更高性能的实现。
  • POSIX信号量接口简单易用:没有信号量集,其中一些接口模仿了我们熟悉的文件系统操作。
  • POSIX信号量删除时的处理更加合理。XSI信号量被删除后,使用该信号量标识符的操作将会出错返回,并将errno设置为EIDRM。而对于POSIX信号量,操作可以继续正常执行,直到对该信号量的最后一个引用被释放
POSIX 信号量的操作

POSIX信号量有两种:有名信号量和无名信号量,无名信号量也被称作基于内存的信号量。有名信号量通过IPC名字进行进程间的同步,而无名信号量如果不是放在进程间的共享内存区中,是不能用来进行进程间同步的,只能用来进行线程同步。

(1)创建一个信号量:

#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode, unsigned int value */ );
返回值:若成功则返回指向信号量的指针,若出错则返回SEM_FAILED
  • 信号量是通过name参数即信号量的名字来进行标识
  • 如果使用一个现存的有名信号量,我们只需指定两个参数:信号量名和oflag(oflag取0)
  • 把oflag设置为O_CREAT标志时,如果指定的信号量不存在则新建一个有名信号量;如果指定的信号量已经存在,那么打开使用,无其他额外操作发生。
  • 指定O_CREAT标志,那么就需要提供另外两个参数:mode和value.mode用来指定谁可以访问该信号量.value参数用来指定信号量的初始值。它可取值为:0-SEM_VALUE_MAX。
  • oflag参数设置为:O_CREAT|O_EXCL。如果信号量已经存在的话,这会导致sem_open调用失败。
  • sem_open函数返回一个信号量指针,该指针可供其他对该信号量进行操作的函数使用。

(2)释放与信号量相关的资源

#include <semaphore.h>
int sem_close(sem_t *sem);
返回值:若成功则返回0,出错返回-1

(3)销毁一个有名信号量

#include <semaphore.h>
int sem_unlink(const char *name);
返回值:若成功则返回0,出错则返回-1

(4)请求一个信号量:

如果信号量计数为0,这时如果调用sem_wait函数,将会阻塞。直到成功对信号量计数减1或被一个信号中断,sem_wait函数才会返回。我们可以使用sem_trywait函数以避免阻塞。当我们调用sem_trywait函数时,如果信号量计数为0,sem_trywait会返回-1,并将errno设置为EAGAIN。

#include <semaphore.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
两个函数返回值:若成功则返回0,出错则返回-1

使用sem_timedwait函数:tsptr参数指定了希望等待的绝对时间。如果信号量可以被立即减1,那么超时也无所谓,即使你指定了一个已经过去的时间,试图对信号量减1的操作也会成功。如果直到超时,还不能对信号量计数减1,那么sem_timedwait函数将会返回-1,并将errno设置为ETIMEDOUT。

#include <semaphore.h>
#include <time.h>

int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict tsptr);
返回值:若成功则返回0,出错则返回-1

(5)信号量值加1。这类似于对一个二值信号量解锁或释放一个与计数信号量有关的资源。

#include <semaphore.h>
int sem_post(sem_t *sem);
返回值:若成功则返回0,出错则返回-1

(6) 调用sem_init函数创建一个无名信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
返回值:若成功则返回0,出错返回-1

(7) 调用sem_destroy函数来销毁用完的无名信号量

#include <semaphore.h>
int sem_destroy(sem_t *sem);
返回值:若成功则返回0,出错则返回-1

(8) 调用sem_getvalue函数来获取信号量值。

#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *restrict valp);
返回值:若成功则返回0,出错则返回-1

参考

[1] http://liwei.life/2016/07/18/pipe/
[2] http://www.cnblogs.com/biyeymyhjob/archive/2012/11/03/2751593.html
[3] http://blog.csdn.net/jiajiayouba/article/details/8815042
[4] http://blog.csdn.net/ljianhui/article/details/10287879
[5] http://blog.csdn.net/mybelief321/article/details/9086151
[6] http://blog.csdn.net/ljianhui/article/details/10243617
[7] http://blog.csdn.net/ljianhui/article/details/10253345

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值