"高级进程间通信"

  "进程间通信"

Posted by Xu on August 30, 2017

高级进程间通信

这部分主要介绍两部分内容:基于Stream的管道及UNIX域套接字,使用这些IPC可以在进程间传送打开的文件描述符。

基于Stream的管道

基于Stream的管道是一个双向(全双工)的管道,单个Steram管道就能给父子进程提供双向数据流。

基于Stream的管道创建:

int s_pipe(fd);

命名的Streams管道

通常管道仅在相关进程之间使用,子进程继承父进程管道,之前我们曾使用FIFO进行通信,但这仅仅提供单向通信。Stream机制提供一种途径,使得进程可以给予管道一个文件系统的名字,用于无关进程间通信,避免了单向FIFO的问题。我们使用

StreamPipe

  int fattach(int fd,const char *path);//path必须引用一个现存的文件,一旦STREAMS管道连接到文件系统名字空间,则原来使用该名字的底层文件就不再可访问,打开该名字的任一进程将能访问相应管道,而不是访问原先的文件
  

一般只有管道的一端连接到文件系统的名字上,另一端用来与打开该连接文件名的进程通信。 撤销管道端与文件名的连接:

  int fdetach(const char *path);
  

调用fdetach后,先前依靠打开path而能访问STREAMS管道的进程人可继续访问该管道,但此后打开path的进程将访问该名字在文件系统中的底层文件。

唯一连接

由于当服务器进程创建一个Streams命名管道时,多个客户端进程想要通过用命名streams管道的另一端与服务器通信,当多个进程对该管道进行写数据时,这些数据可能会发生混淆。而当服务器发送数据到指定进程时,我们也无法进行合适的调度让消息被指定的客户端进程读取。所以我们压入connld模块到要被客户端连接的管道一端来解决这种问题。

这和socket通信的机制类似,当客户端进程打开命名管道连接时,操作系统会新建一个Streams管道,给客户端返回一端作为其打开连接/tmp/pipe的结果,再把新管道的另一端的文件描述符通过已有的连接管道发送给服务器端构建出该客户进程与服务器进程之间通信的唯一连接。

对比socket通信,实现对应的三个函数:

int serv_listen(const char *name);//建立一个Streams管道,将该管道的另一端与指定name连接,并返回一个文件描述符,表示服务器进程将对该文件描述符进行监听

int serv_accept(int listenfd,uid_t *uidptr);//等待客户进程连接请求的到来,当请求到来时,系统自动创建一个新的Streams管道,并向服务器进程返回该管道的一端

int cli_conn(const char *name);//客户进程调用该函数连接服务器进程,函数会返回新建的Stream管道的另一端文件描述符用于与服务器进程通信。

StreamPipeCS

UNIX域套接字

UNIX域套接字用于在同一台机器上进程的通信,相对于网络套接字,其效率更高,UNIX套接字仅仅复制数据,不进行协议处理,不需要添加或删除网络报头,无需检验和,无需产生顺序号,也无须发送确认报文。

UNIX域套接字提供流和数据报两种接口。

创建一对非命名的相互连接的UNIX域套接字函数:

int socketpair(int domain,int type,int protocol,int socketfd[2]);

socketpair函数创建的套接字没有名字,我们可以通过bind操作给UNIX域套接字命名,但需要注意的是,UNIX域套接字与使用的地址格式不同于英特网套接字。

同样UNIX域的唯一连接的创建过程因特网socket的创建过程一致,通过三个函数listen,accept,connect来实现

传送文件描述符

文件共享

内核有三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响

  • (1) 每一个进程都有一个进程表项,每个描述符占用一项。一个文件描述符由两部分构成:a.文件描述符标志,b.指向一个文件表项的指针
  • (2) 内核为所有的被打开的文件维持一张文件表,一个文件表包括:a.文件状态标志,b.当前文件偏移量,c.指向该文件v节点表项的指针(v节点的创建是为对在一个计算机系统上的多文件系统类型提供支持)
  • (3) 每个打开文件或设备都有一个v节点结构。v节点包括文件类型和对此文件进行各种操作的函数指针.对大多数文件,v节点还包括该文件的i节点(包含文件所有者,文件长度,文件所在设备,指向文件实际数据库所在磁盘的位置指针)

当两个不同的进程打开同一文件时,在各自进程表项对应各自的文件描述符,每个文件描述符中的文件指针指向不同的文件表,每个进程拥有不同的文件表可以使得进程都有它自己的对该文件的偏移量。

也有可能多个文件描述符中的指针指向同一文件表,比如dup函数或fork的父子进程共享文件表等。所以文件描述符标志fd与文件状态标志在作用域上有一定区别,fd只作用于一个进程,文件状态标志则作用于所有打开它的进程。

ShareFile

传递文件描述符

通常,当一个进程向另一个进程传送一个打开的文件描述符,是想要发送进程和接收进程共享同一文件表项,一般来说,发送进程和接收进程对指向同一文件表项的描述符标志是有所不同的。当发送进程将文件描述符传送给接收进程后,通常会关闭该描述符,但并不意味着该文件被关闭,而是该文件视为被接收进程打开。

协议:为发送一个描述符,send_fd先发送两个0字节,然后发送实际描述符。为发送一个出错信息errmsg,则先发送errmsg,再发送一个0字节,最后发送一个status字节。所以recv_fd读取管道的时候,直到读取null(0)字节,这之前的所有字符都发送给userfunc函数做错误信息处理,而读取null字节之后的字节为status字节,若该字节为0,说明有文件描述符要传送过来,否则没有传送描述符接收。

三个函数实现文件描述符的传递: int send_fd(int fd ,int fd_to_send);//通过fd代表的STREAMS管道或UNIX域套接字发送描述符fd_to_send.

int send_err(int fd,int status,const char *errmsg);//经由fd发送errmsg和随后的status

int recv_fd(int fd ,ssize_t (*userfunc)(int,const void *,size_t));//客户进程接收描述符

基于Streams管道的文件描述符的传递的三个函数的实现由ioctl来控制
int ioctl(int fd ,int flag(I_SENDFD I_RECVFD),fd_to_send);

sendfd

经由UNIX域套接字传送文件描述符

为了使用域套接字传送文件描述符,调用sendmsg和recvmsg函数,两个函数的参数中都有一个msghdr结构指针:

struct msghdr{
  void *msg_name;//可选地址项
  socklen_t msg_namelen;//地址长度
  struct iovec *msg_iov;//传输数据缓冲区
  int msg_iovlen;//缓存数据长度
  void *msg_control;//辅助数据,数据副本
  socklen_t msg_controllen;//数据副本长度
  int msg_flags;//接收消息的标志位
}

该结构体中,前两个用于数据报文发送,其后两个指定多个缓冲区构成的数组,再后面的两个处理控制信息的传送和接收,最后一个msg_flags包含来说明所接受的消息的标志。

其中msg_control字段指向cmsghdr:

struct cmsghdr{
socklen_t cmsg_len;
int cmsg_level;
int cmsg_type;
}

当发送文件描符时,将cmsg_len设置为cmsghdr结构的长度加上一个整型的长度,cmsg_level设置为SOL_SOCKET,cmsg_type字段设置为SCM_RIGHTS用于指明我们再传送访问权,SCM表示套接字级控制访问权(socket_level control message)。访问权仅能经过域套接字传送。描述符紧随cmsg_type字段之后存放。用CMSG_DATA宏获取该整型量的指针。

协议的检测就是通过iovec缓冲区中的buf[1], 检测null字符和随后的第一个字节是否为0,为0说明有描述符传送过来,然后调用CMSG_DATA定位fd

几个宏的定义:

unsigned char *CMSG_DATA(struct cmsghdr *cp);//返回与cmsghdr相关联的数据指针,也就是cmsghdr紧随的数据指针

struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mp);//指向与msghdr关联的第一个cmsghdr指针

struct cmsghdr *CMSG_NEXTHDR(struct msghdr *mp,struct cmsghdr *cp);//返回指向与msgtype相关联的当前cmsghdr *cp(参数)下一个cmsghdr结构的指针

unsigned int CMSG_LENunsigned int nbytes ;//返回为存放nbytes大小数据对象的cmsghdr结构所需要的数据长度

在传送文件描述符方面,UNIX域套接字和STREAMS 管道之间的区别在于。用STREAMS管道时我们可以知道发送进程的身份,UNIX域则需要通过凭证来证明发送进程身份。

发送凭证: Linux:

struct cmsgcred{ uint32_t pid;//发送进程的进程id uint32_t uid;//发送进程的uid uint32_t gid;//发送进程的gid }

一般发送fd时,会将凭证放到fd数据的下一个指针结构,通过CMSG_NXTHDR获取,并且我们指定SCM_CREDENTIALS来传送凭证。