zoukankan      html  css  js  c++  java
  • UNIX环境高级编程(15-进程间通信)

    本章主要介绍一些进程间通信的方式,如管道、消息队列、信号量和共享存储等。

    管道

    一般来说,管道是半双工的(即数据只能在一个方向上流动),并且只能在具有公共祖先的两个进程之间使用。通常,父进程创建管道后会接着调用fork,从而利用管道在父子进程之间通信。

    Half-duplex pipe after a fork

    之后,父子进程可以分别关闭管道的读/写端,以利用管道在父子进程中传递信息。例如,如果想要创建从父进程到子进程的管道,则可以关闭父进程的读端和子进程的写端

    由于管道半双工的特性,想要在父子进程间双向传递信息需要建立2个管道。

    #include <unistd.h>
    // Returns: 0 if OK, −1 on error
    int pipe(int fd[2]);
    

    利用pipe函数可以创建管道,fd参数返回两个文件描述符,fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入。

    在上面的例子中,父进程关闭fd[0],子进程关闭fd[1],那么最后的示意图如下:

    Pipe from parent to child

    注意:

    • 当读一个写端被关闭的管道,在所有数据被读取后,read返回0
    • 当写一个读端被关闭的管道,会产生SIGPIPE信号。如果忽略该信号或从信号处理程序返回,则write返回-1,且设置errno为EPIPE
    • 写入不超过PIPE_ BUF 字节的操作是原子的,如果写入数据的大小超过该值,在多个进程同时写一个管道时,所写的数据可能交叉

    连接到另一个进程

    管道的通常用法是创建一个连接到另一个进程的管道,然后读取其输出或者向其输入端发送数据。可以使用popenpclose实现这一功能。这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭未使用的管道,执行shell运行命令,然后等待命令终止。

    #include <stdio.h>
    // Returns: file pointer if OK, NULL on error
    FILE *popen(const char *cmdstring, const char *type);
    // Returns: termination status of cmdstring, or −1 on error
    int pclose(FILE *fp);
    

    popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O文件指针,如果type是"r",则文件指针连接到cmdstring的标准输出,如果是"w"则连接到标准输入,如下图所示:

    popen

    cmdstring会以sh -c cmdstring的方式执行。

    pclose函数关闭标准I/O流,等待命令终止,然后返回shell的终止状态。(注意不要使用fclose函数,它不会等待子进程结束)

    协同进程

    UNIX系统过滤程序从标准输入读取数据,向标准输出写数据。几个过滤程序通常在shell管道中线性连接。当一个过滤程序既产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了协同进程(coprocess)。

    要实现协同进程,需要创建两个管道,分别作为协同进程的标准输入和输出,示意图如下:

    Driving a coprocess by writing its standard input and reading its standard output

    子进程的参考代码如下:

    close(fd1[1]);
    close(fd2[0]);
    if (fd1[0] != STDIN_FILENO) {
        if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
            err_sys("dup2 error to stdin");
        close(fd1[0]);
    }
    if (fd2[1] != STDOUT_FILENO) {
        if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
            err_sys("dup2 error to stdout");
        close(fd2[1]);
    }
    if (execl("./add2", "add2", (char *)0) < 0)
        err_sys("execl error");
    

    dup2函数用于复制指定的文件描述符,它将两个管道描述符分别连接到标准输入和输出。

    注意:

    在协同进程中如果需要使用标准I/O(如fgets),则要额外注意其缓冲机制。对于管道,其默认是全缓冲的,可以通过调用fflush或者设置缓冲模式(setvbuf/setbuf)来解决。

    FIFO

    FIFO也被称为命名管道,它使得不相关的进程间也能交换数据。

    FIFO也是一种文件类型,创建FIFO与创建文件类似,需要指定其路径。

    #include <sys/stat.h>
    // Both return: 0 if OK, −1 on error
    int mkfifo(const char *path, mode_t mode);
    int mkfifoat(int dirfd, const char *path, mode_t mode);
    

    mode参数指明FIFO的文件权限,与open函数中的mode相同。

    mkfifoat函数的path参数有如下几种情况:

    • 如果指定为绝对路径,则会忽略dirfd参数,行为与mkfifo类似
    • 如果指定为相对路径,则该路径与dirfd打开的目录有关
    • 如果指定为相对路径,且dirfd有参数AT_FDCWD,那么路径以当前目录开始

    创建完成后,就可以使用open打开FIFO。

    在打开时如果没有设置非阻塞标志O_NONBLOCK,那么如果以只读方式打开(O_RDONLY),进程会被阻塞直到其他进程为写而打开这个FIFO,同理,只写方式(O_ WRONL )打开也会阻塞。

    但是,不应该使用O_RDWR的方式来绕过这种阻塞行为,而应该使用非阻塞标志。使用读写方式打开FIFO,会导致读取数据时永远看不到文件结束,因为至少会有一个写描述符是打开着的。

    实例

    可以使用FIFO进行客户进程与服务器进程之间的通信。每个客户进程可以将自己的请求写到一个公共的FIFO文件中(请求长度需要小于PIPE_BUF以避免客户进程之间的数据交叉),服务器进程针对每个客户进程创建FIFO,用于向客户进程发送数据。客户进程的FIFO的路径名可以使用客户进程的PID号作为基础,如/tmp/servv1.PID,这样客户进程就直到该从哪个FIFO读取服务器进程返回的数据了。

    Client–server communication using FIFOs

    XSI IPC

    这一部分主要包含3种IPC方式:消息队列、信号量和共享存储器。

    每个IPC对象与键(key)相关联,以使得多个进程可以通过它进行联系。在创建IPC结构时,必须指定一个键。而在系统内部,则使用标识符引用IPC结构。

    关于键的创建方式,主要有如下几种:

    • 指定为IPC_PRIVATE,这会创建一个新的IPC结构,可以将返回的标识符存入文件供其他进程使用,也可直接给fork后的子进程使用

    • 在公共头文件中定义一个键,然后由一个进程(通常是服务器进程)根据这个键来创建新的IPC结构。但是这种方式可能会与已经存在的键冲突,需要进程删除原有的IPC结构再重新创建。

    • 使用ftok函数,将路径名和某个数字(0-255)变换为一个键。

      #include <sys/ipc.h>
      // Returns: key if OK, (key_t)−1 on error
      key_t ftok(const char *path, int id);
      

      path参数必须引用的是现有的文件,id参数只使用其低8位。

    另外,在创建IPC结构时还需要指定其权限,与文件权限类似,但是不存在执行权限。

    XSI IPC permissions

    注意:

    • IPC_PRIVATE只能用于创建新的IPC结构,而不能用来引用一个现有的IPC结构。
    • 如果希望确保新创建的IPC结构没有引用具有同一标识符的现有IPC结构,则可以在flag中同时指定IPC_CREATIPC_EXCL。这样,如果已经存在则会返回EEXIST。

    消息队列

    消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。以下简称队列。

    相关的数据结构很少用到,再次不再列出,后面的信号量和共享存储同理。需要的话可以到对应的头文件中查看。

    示例代码参考https://gitee.com/maxiaowei/Linux/blob/master/apue/ch15/ipc_msg.c

    msgget用于创建或打开一个队列。

    #include <sys/msg.h>
    // Returns: message queue ID if OK, −1 on error
    int msgget(key_t key, int flag);
    

    key参数可以是通过ftok函数生成的,也可以是IPC_PRIVATE。flag用于设定读写权限,如果是新建该IPC结构则可以添加IPC_CREAT

    // Returns: 0 if OK, −1 on error
    int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
    

    msgsnd将新消息添加到队列尾端。

    msqid是get函数返回的队列ID,nbytes是消息数据的长度。

    ptr指向一个结构,其包含一个正的消息类型,和消息数据(nbytes为0则无消息数据),可以定义其结构如下:

    struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[1];    /* message data, of length nbytes */
    };
    

    flag可以指定为IPC_NOWAIT,当消息队列满时(或达到系统限制),会立即出错返回EAGAIN。否则,进程会一直阻塞直到:有空间容纳消息;队列被删除(返回EIDRM);或捕捉到信号并从处理程序返回(返回EINTR)。

    // Returns: size of data portion of message if OK, −1 on error
    ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
    

    msgrcv用于从队列中取出消息,可以指定获取某些类型的数据,而不是必须按照先进先出的次序。

    ptr指向的结构与snd函数一样,而nbytes则指定了消息长度,如果返回的消息长度>nbytes,而flag中设置了MSG_NOERROR,则消息被截断。如果没有设置则出错返回E2BIG,而消息仍然留在队列中。

    type可以指定想要获取的消息:

    • type==0:返回队列中的第一个消息
    • type>0:返回消息类型为type的第一个消息
    • type<0:返回消息类型≤type绝对值的消息,如果有若干个满足则取类型最小的。

    flag参数同样可以指定为非阻塞。

    // Returns: 0 if OK, −1 on error
    int msgctl(int msqid, int cmd, struct msqid_ds *buf );
    

    msgctl函数对队列执行多种操作。

    cmd参数指定队列需要执行的操作:

    • IPC_STAT:获取队列的msqid_ds结构信息,存放于buf指向的结构中
    • IPC_SET:将msg_perm.uid,msg_perm.gid,msg_perm.mode和msg_qbytes通过buf复制到队列的msqid_ds结构中。该命令只能由超级用户或者有效用户ID等于msg_perm.cuid或msg_perm.uid的用户执行。
    • IPC_RMID:删除队列及其中的数据。也只能由上述的两类用户执行。

    这3条命令也适用与信号量(semctl)和共享存储(shmctl)。

    信号量

    信号量是一个计数器,用于为多个进程提供对共享数据对象的访问。

    示例代码:https://gitee.com/maxiaowei/Linux/blob/master/apue/ch15/ipc_sem.c

    XSI信号量需要定义为一个或多个信号量的合集,因此在创建的时候需要指明信号量的个数,在使用的时候也要指明用的是哪个信号量。

    #include <sys/sem.h>
    // Returns: semaphore ID if OK, −1 on error// 
    int semget(key_t key, int nsems, int flag);
    

    semget用于创建或打开一个信号量合集。相关参数的与上一节的队列相似,多出来的nsems用于指定该集合中的信号量数。如果是创建新集合,则需要指定数量;如果是引用现有的集合,则将其设置为0。

    int semctl(int semid, int semnum, int cmd, ... /* union semun arg */ );
    

    semctl包含多种信号量操作。

    第4个参数argcmd的实际值来决定是否使用,注意该参数并不是指针。如果需要使用该参数,其类型需要自己定义,一般定义为如下形式:

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

    参数semnum用于指定信号量集合中的某个成员,该值在0 ~ nsmes-1之间。

    cmd由如下10个可选项:

    • IPC_STAT,IPC_SET,IPC_RMID:与队列类似
    • GETVAL,SETVAL:返回/设置(通过arg.val)semnum指定的成员的信号量值(semval)
    • GETPID,GETNCNT,GETZCNT:返回指定成员的sempid,semncnt,semzcnt
    • GETALL,SETALL:取/设置所有的信号量值(通过arg.array)

    除GETALL以外所有的GET命令都由函数的返回值返回,其他命令则是成功返回0,失败返回-1并设置errno。

    // Returns: 0 if OK, −1 on error
    int semop(int semid, struct sembuf semoparray[], size_t nops);
    

    semop函数自动执行信号量集合上的操作数组。

    nops是数组semoparray的元素个数。

    semoparray是一个信号量操作数组,其中存放每个信号量的操作,其结构如下:

    struct sembuf {
      unsigned short sem_num; /* member # in set (0, 1, ..., nsems-1) */
      short          sem_op;  /* operation (negative, 0, or positive) */
      short          sem_flg; /* IPC_NOWAIT, SEM_UNDO */
    };
    

    sem_flg的SEM_UNDO标志标识当进程终止时,该操作修改的信号量值会被恢复,即重新设置为调用该操作之前的数值。

    sem_op可以指定如下3种值:

    • 正值,表示进程释放的占用的资源数,sem_op值会加到对应的信号量的值上。
    • 0,表示进程希望等待该信号量值变为0。IPC_NOWAIT标志可以控制进程是否阻塞,相关的出错返回信息可以查阅手册,此处省略。
    • 负值,表示进程想要获取的资源数。如果信号量值≥sem_op的绝对值(满足需求),则会从当前的信号量值上减去对应的值,否则由IPC_NOWAIT标志决定进程是否阻塞。

    semop函数具有原子性,即要么执行数组中所有的操作,要么什么也不做。

    共享存储

    共享存储允许两个或多个进程共享一个给定的存储区。但是,需要注意存储区访问的同步问题,当进程在写入数据时其他进程不应该去读取这些数据。一般使用信号量来解决这一同步问题。

    相比与通过文件映射的方式来共享存储区的方式,XSI共享存储没有相关的文件,它共享的是内存的匿名段。

    示例代码:https://gitee.com/maxiaowei/Linux/blob/master/apue/ch15/ipc_shm.c

    #include <sys/shm.h>
    // Returns: shared memory ID if OK, −1 on error
    int shmget(key_t key, size_t size, int flag);
    

    shmget函数用于创建或引用一个共享存储段,在创建时size指定段的大小(单位是字节),若要引用一个现存的段,则应该设置为0。实现一般将大小向上取整为系统页长的整数倍,若指定的size不是整数倍,则余下的空间是不可使用的。

    // Returns: 0 if OK, −1 on error
    int shmctl(int shmid, int cmd, struct shmid_ds *buf );
    

    shmctl函数对共享存储段执行多种操作。主要有IPC_STAT,IPC_SET和IPC_RMID,相关解释可以参考消息队列部分。

    另外,Linux中还额外提供额外的命令支持,可以参考手册shmctl(2) 。

    // Returns: pointer to shared memory segment if OK, −1 on error
    void *shmat(int shmid, const void *addr, int flag);
    

    shmat用于将共享存储段连接到进程的地址空间。具体连接到地址空间的什么位置由2、3两个参数决定。

    • addr=0,则连接到内核选择的第一个可用地址上。(推荐)
    • addr≠0,且flag没有指定SHM_RND,那么连接到addr指定的地址。
    • addr≠0,且指定了SHM_RND,那么系统会按照公式(addr-(addr % SHMLBA))决定连接地址。该公式作用是将地址向下取最近的SHMLBA的倍数,而常数SHMLBA表示“低边界地址倍数”。

    flag还可以指定SHM_RDONLY以只读方式连接共享段。

    // Returns: 0 if OK, −1 on error
    int shmdt(const void *addr);
    

    shmdt用于分离共享存储段。这一操作不会删除系统中共享存储段的标识符及其数据结构。想要删除对应的数据结构,需要调用shmctl的IPC_RMID命令。

    Memory layout on an Intel-based Linux system

    POSIX信号量

    POSIX信号量与XSI信号量最大的不同就是没有信号量集的概念,一次只能操作一个信号量。还有就是在删除信号量时,正在使用XSI信号量的操作会失败;而POSIX信号量的操作会正常执行,直到该信号量的最有一个引用被释放。

    POSIX信号量有两种形式:命名的和未命名的。两者的差异在于创建和销毁的形式上,使用的方式是一样的。未命名的信号量只存在于内存中,因此想要使用这些信号量的进程需要有对应的访问权限,如同一进程中的线程,或者是不同进程中映射相同的内存内容到自己的地址空间的线程。而命名信号量可以被任何直到它们名字的进程访问。

    示例代码:https://gitee.com/maxiaowei/Linux/blob/master/apue/ch15/ipc_psem.c

    创建与销毁

    命名信号量

    给信号量命名需要遵守一定的规则:

    • 名字的第一个字符应该是/。因为一般POSIX信号量的实现要使用文件系统。
    • 名字不应该包含其他斜杠。
    • 名字长度是实现定义的,不应长于_POSIX_NAME_MAX。
    #include <semaphore.h>
    // Returns: Pointer to semaphore if OK, SEM_FAILED on error
    sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode,
                  unsigned int value */ );
    

    sem_open用于创建一个新的信号量或使用一个现有的信号量。

    当想要使用一个现有的信号量时,只需指定其名字,并将oflag设为0。

    oflag包含O_CREAT标志时,如果信号量不存在则会创建新的,如果存在则会被使用,但不会重新初始化。指定此标志时,还需要提供后面的2个参数。mode指定访问权限,这与打开文件的权限相同;value指定信号量的初值。

    如果oflag同时指定了O_EXCL标志,则在创建信号量时,如果信号量已经存在就会出错。

    // Both return: 0 if OK, −1 on error
    int sem_close(sem_t *sem);
    int sem_unlink(const char *name);
    

    sem_close用于关闭一个信号量,释放相关资源。进程退出时如果没有调用该函数,系统也会自动关闭打开的信号量。POSIX信号量没有UNDO机制,所以信号量的值不会受到影响。

    sem_unlink用于销毁信号量,删除信号量的名字。如果没有打开的信号量引用,信号量会被立即销毁,否则会延迟到最后一个打开的引用关闭。

    未命名信号量

    这种形式的信号量主要用于单个进程。

    // Both return: 0 if OK, −1 on error
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    int sem_destroy(sem_t *sem);
    

    sem_init用于创建一个未命名信号量。

    • value指定其初值。

    • pshared值为0时,信号量仅在进程的线程之间共享;不为0则表明会在进程之间共享。

    sem_destroy用于销毁未命名信号量。销毁之后不能使用任何带有sem的信号量函数,除非通过sem_init重新初始化它。

    信号量操作

    与XSI信号量不同,POSIX信号量一次操作只能+1或者-1。

    #include <time.h>
    // All return: 0 if OK, −1 on error
    int sem_trywait(sem_t *sem);
    int sem_wait(sem_t *sem);
    int sem_timedwait(sem_t *restrict sem,
                      const struct timespec *restrict tsptr);
    

    这3个函数实现信号量的-1操作。

    当信号量计数为0时,使用sem_wait函数会阻塞,直到成功使信号量-1或者被信号中断;而sem_trywait会返回-1且设置errno为EAGAIN。

    使用sem_timedwait可以设定等待时间,超时后会返回-1且设置errno为ETIMEOUT。

    // Returns: 0 if OK, −1 on error
    int sem_post(sem_t *sem);
    

    调用sem_post会使信号量计数+1。如果有进程被改信号量阻塞,那么进程会被唤醒。

    // Returns: 0 if OK, −1 on error
    int sem_getvalue(sem_t *restrict sem, int *restrict valp);
    

    sem_getvalue函数用于获取信号量值,该数值存储在valp指向的地址处。注意函数返回的数值有可能是过时的。

  • 相关阅读:
    js 一维数组转二维数组
    mongoose 系列设置
    手写系列
    设置未登录的导航守卫
    vue 添加设置别名文件
    移动端视口标签
    小程序跳转页面怎么携带数据
    data数据复杂时怎么setData
    小程序注意的点 text标签中一定 不要换行
    小程序用户登录
  • 原文地址:https://www.cnblogs.com/maxiaowei0216/p/14250334.html
Copyright © 2011-2022 走看看