Apue.15 IPC

对于CS的课程,除了离散数学和编译原理这种抽象程度较高的理论课,其他的基本都要自己动手写代码、观察和调试的,这里的读书笔记其实没啥大用,只是作为一个督促自我学习的方式,同时也算是以后方便查询的index吧。马上就要开学,华为的习题还没做,今天开始添加此项任务吧。

IPC方式,POSIX.1规定的有pipe和FIFO;XSI扩展中又添加了消息队列、信号量和共享内存,以及网络编程中的套接字;XSI可选部分规定了STREAMS流机制。

管道:

最古老的IPC方式,POSIX.1规定的是半双工,某些系统提供全双工实现。

只能在具有公共祖先的进程间使用,创建:

#include <unistd.h>

int pipe(int fields[2]);    //fields[0]为读端口,fields[1]为写端口;

管道是文件,可以用S_ISFIFO来判断,fstat对管道的每一端都返回一个FIFO类型的文件描述符;

常见用法:pipe创建管道,fork创建子进程;在父进程中关闭fd[0],在子进程中关闭fd[1];父进程用于写,子进程用于读;或者反着来,子进程写父进程读;

更常见的用法:直接复制文件描述符为对应的标准输入/输出;

若管道的一端关闭,规则:

写读端已关闭的管道,产生信号SIGPIPE,返回-1,errno=EPIPE;

读写端已关闭的管道,正常;若读取完毕,read返回0,表示到达文件尾端;

常量PIPE_BUF规定了管道缓冲区的大小,若write的字节数大于该常量,那么,可能会造成多个同时写该管道(或FIFO)的进程之间出现穿插。

管道可用于父子进程之间的同步,简单来说,创建两条管道(TELL_WAIT),然后等待(read);完成操作后发送(write);

由于复制为标准I/O的用法太过常见,因此标准库直接提供了更简单的函数:

#include <stdio.h>

FILE *popen(const char *cmdstring, const char *type);    //type=r,w,表示为读|写而打开管道;分别将其标准输出|输入连接到文件指针;

int pclose(FILE *fp);        //关闭I/O流,返回shell结束状态

popen会先fork然后exec参数中的程序,返回打开文件的指针。

popen绝不应该由setuid | setgid的程序调用;

popen特别适用于构造简单的过滤器程序。

协同进程

当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出,则称之为该过滤程序的协同程序。

通常采用两个管道实现协同程序。注意标准I/O对于管道是全缓冲机制的。

FIFO

又称为命名管道,与管道的最大不同在于可以由非公共祖先的进程共同使用。

FIFO类似于普通文件,不同进程之间通过援引文件名来使用FIFO。创建:

#include <sys/stat.h>

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

mode的参数和创建文件时权限的参数相同;一旦创建FIFO完毕,其使用方法类似于pipe和普通文件。

常见用法:FIFO用于客户进程—服务器进程之间的通信,客户进程通过众所周知的FIFO向服务器进程发送read请求,服务器进程通过各自的FIFO响应客户进程;

XSI IPC

XSI IPC都通过"标识符"加以引用,IPC标识符一般是long类型的整数,其外部名被称为key,共享IPC数据结构的方案:

  1. 服务器进程在使用创建IPC的函数时,指定key=IPC_PRIVATE用于创建一个新IPC结构,将返回的标识符存放在一个文件中以便客户进程取用,
  2. 在一个公用的头文件中指定key,然后服务器用此key创建结构。缺点是可能已经被占用了。
  3. 指定一个路径名和项目ID(0~255),使用ftok生成key(注意路径名必须现实存在)。注意:如果使用同一ID,那么key可能重复;

创建IPC的函数有相同的结构(msgget, semget, shmget),第一个参数是key,第二个则是flag;

创建新IPC的方法:key=IPC_PRIVATE,或flag中指定IPC_CREAT(最好同时指定IPC_EXCL,防止打开了一个已创建的同名IPC结构)。

权限结构,所有XSI IPC中都有一个结构成员来限制IPC的权限:

struct ipc_perm{

    uid_t uid;    //owner's uid

    gid_t gid;    //owner's gid

    uid_t cuid;    //creator's uid

    gid_t cgid;    //creator's gid

    mode_t mode;//access mode

};

除了不可执行外,mode与普通文件的访问权限一致;

XSI IPC的主要缺点是没有访问计数,必须显式删除;其二是这些IPC结构在文件系统中没有名字,这意味着必须创建一系列的系统调用来完成对IPC对象的处理;

消息队列

每则消息由三部分组成:type(unsigned long),length(unsigned, bytes)和实际的消息。

struct msqid_ds{

    struct ipc_perm msg_perm;    //权限控制

    msgnum_t msg_qnum;        //队列中消息

    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/msg.h>

int msgget(key_t key,int flag);//返回消息ID或-1

int msgctl(int msqid, int cmd, struct msqid_ds *buf);//垃圾桶函数,cmd=IPC_STAT(取队列的msqid_ds结构,存放在buf中),IPC_SET(设置该队列的),IPC_RMID(删除该消息队列和数据)

int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);//flag可指定IPC_NOWAIT(无阻塞)

int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);//type指定消息类型,type=0返回第一条消息;type<0则返回队列中消息类型值小于或等于type绝对值的消息(取最小的一个)

信号量

semaphore 是一种计数器,用来对可用资源进行计数。理论上说,很简单:

如果资源可用,那么信号量的值>0,每使用一个资源,信号量-1;

如果资源不可用,那么信号量=0,进程休眠等待;当信号量>0时,进程被唤醒;

如果进程不再使用资源,那么释放,信号量+1.

信号量集:

struct semid_ds{

    struct ipc_perm sem_perm;

    unsigned short sem_nsems;

    time_t sem_otime;

    time_t sem_ctime;

};

信号量:

struct{

    unsigned short semval;

    pid_t sempid;

    unsigned short semncnt;

    unsigned short semzcnt;

};

#include <sys/sem.h>

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

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

union semun{

    int val;

    struct semid_ds *buf;

    unsigned short *array;

};

除了和msg类似的3个cmd外,还有一系列用于获取参数的cmd(GETVAL,GETPID等);

int semop(int semid, struct sembuf semoparray[], size_t nops);

struct sembuf{

    unsigned short sem_num;    //信号集中的信号序数

    short sem_op;            //操作(加减)

    short sem_flag;        //IPC_NOWAIT, SEM_UNDO

};

SEM_UNDO存在的意义在于可以在exit后内核可以自己处理信号量,进行收尾工作。

一般而言,使用记录锁可以完成的任务,最好不要使用更为复杂的信号量;

共享内存

最快的IPC,回忆进程的内存空间结构,共享内存在stack的地址前,heap的地址后(向上增长)。内核实际链接的共享存储段放在进程的什么位置和系统明确相关。

使用共享存储需要注意多个进程之间的访问同步问题。

各种处理函数和flag与前面两种IPC大致相同。

注意使用shmat链接到段,使用shmdt脱离段,使用shmctl删除段。

文中给出了两个特殊实例:

将/dev/zero映射用mmap映射,并指定flag为MAP_SHARED,那么多个进程可以共享此文件映射存储区。好处是无须存在一个实际文件(如果仅仅是为了使用共享存储),而且比较简单,但是只能在相关进程中起作用;除此之外,匿名存储映射也可以完成类似的功能(更加简单)。

原文地址:https://www.cnblogs.com/livewithnorest/p/2914459.html