最近在看libevent源码,发现libevent中会用到socketpair来进行线程间的通信,如下,当有信号触发时就会通过这个socketpair去通知主线程。
int
evsig_init(struct event_base *base)
{
/*
* Our signal handler is going to write to one end of the socket
* pair to wake up our event loop. The event loop then scans for
* signals that got delivered.
*/
if (evutil_socketpair(
AF_UNIX, SOCK_STREAM, 0, base->sig.ev_signal_pair) == -1) {
#ifdef WIN32
/* Make this nonfatal on win32, where sometimes people
have localhost firewalled. */
event_sock_warn(-1, "%s: socketpair", __func__);
#else
event_sock_err(1, -1, "%s: socketpair", __func__);
#endif
return -1;
}
Unix域支持服务器、客户端的模式,这种模式的好处就是任意进程都可以和服务器进程通信,这种模式通常需要一个文件路径作为地址,使得任意进程都能通过该文件路径标识找到服务器地址。而socketpair虽然也类似,但它不需要地址的概念,因为它用于有继承关系的进程间通信,通常是主进程调用socketpair拿到两个fd,然后fork出子进程,这样两个进程就可以通信了,不需要寻址的过程,也就不需要地址的概念了。
先看下socketpair()函数的声明:
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int d, int type, int protocol, int sv[2]);
socketpair()函数用于创建一对无名的、相互连接的套接字。
如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1];否则返回-1,错误码保存于errno中。
这里解释下socketpair会创建两个描述符,但改描述符不属于任何的实际文件系统,而是网络文件系统,虚拟的.同时内核会将这两个描述符彼此设为自己的peer即对端(这里即解决了如何标识读写端,可以想象,两个描述符互为读写缓冲区,即解决了这个问题).然后应用相应socket家族里的read/write函数执行读写操作.
有了这个基础,即可明白为什么试用fork产生的两个子进程都不关闭读端的时候会竞争,如上所述,他们共享相同的文件表项,有相同的inode和偏移量,两个进程的操作当然是相互影响的.
基本用法:
1. 这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。例如,可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读;
2. 如果往一个套接字(如sv[0])中写入后,再从该套接字读时会阻塞,只能在另一个套接字中(sv[1])上读成功;
3. 读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程。如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写。因为文件描述副sv[0]和sv[1]是进程共享的,所以读的进程要关闭写描述符, 反之,写的进程关闭读描述符。
下面我从内核角度看看socketpair的实现。
// 分配两个关联的socket
int __sys_socketpair(int family, int type, int protocol, int __user *usockvec)
{
struct socket *sock1, *sock2;
int fd1, fd2, err;
struct file *newfile1, *newfile2;
int flags;
// 参数校验
flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
// 获取两个fd
fd1 = get_unused_fd_flags(flags);
fd2 = get_unused_fd_flags(flags);
// 获取两个socket
err = sock_create(family, type, protocol, &sock1);
err = sock_create(family, type, protocol, &sock2);
// 调用钩子函数
err = sock1->ops->socketpair(sock1, sock2);
// 分配两个file,每个file和socket关联
newfile1 = sock_alloc_file(sock1, flags, NULL);
newfile2 = sock_alloc_file(sock2, flags, NULL);
// 关联fd和file
fd_install(fd1, newfile1);
fd_install(fd2, newfile2);
return 0;
}
我们知道在网络的实现中,为了符合虚拟文件系统的设计,socket的设计中,也遵循fd->file->inode的结构,__sys_socketpair就是申请了两份这样的数据结构,然后调用钩子函数socketpair。再调用socketpair之前,架构如下。
我们接下来看socketpair钩子函数的实现。
static int unix_socketpair(struct socket *socka, struct socket *sockb)
{
struct sock *ska = socka->sk, *skb = sockb->sk;
sock_hold(ska);
sock_hold(skb);
// 互相关联
unix_peer(ska) = skb;
unix_peer(skb) = ska;
init_peercred(ska);
init_peercred(skb);
return 0;
}
我们看到socketpair的实现非常简单,就是把两个socket关联起来,这时候的架构如下。
先说说我的理解:socketpair创建了一对无名的套接字描述符(只能在AF_UNIX域中使用),描述符存储于一个二元数组,eg. s[2] .这对套接字可以进行双工通信,每一个描述符既可以读也可以写。这个在同一个进程中也可以进行通信,向s[0]中写入,就可以从s[1]中读取(只能从s[1]中读取),也可以在s[1]中写入,然后从s[0]中读取;但是,若没有在0端写入,而从1端读取,则1端的读取操作会阻塞,即使在1端写入,也不能从1读取,仍然阻塞;反之亦然......
验证所用代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>
#include <sys/socket.h>
#include <stdlib.h>
#define BUF_SIZE 30
int main(){
int s[2];
int w,r;
char * string = "This is a test string";
char * buf = (char*)calloc(1 , BUF_SIZE);
if( socketpair(AF_UNIX,SOCK_STREAM,0,s) == -1 ){
printf("create unnamed socket pair failed:%s\n",strerror(errno) );
exit(-1);
}
/*******test in a single process ********/
if( ( w = write(s[0] , string , strlen(string) ) ) == -1 ){
printf("Write socket error:%s\n",strerror(errno));
exit(-1);
}
/*****read*******/
if( (r = read(s[1], buf , BUF_SIZE )) == -1){
printf("Read from socket error:%s\n",strerror(errno) );
exit(-1);
}
printf("Read string in same process : %s \n",buf);
if( (r = read(s[0], buf , BUF_SIZE )) == -1){
printf("Read from socket s0 error:%s\n",strerror(errno) );
exit(-1);
}
printf("Read from s0 :%s\n",buf);
printf("Test successed\n");
exit(0);
}
若fork子进程,然后在服进程关闭一个描述符eg. s[1] ,在子进程中再关闭另一个 eg. s[0] ,则可以实现父子进程之间的双工通信,两端都可读可写;当然,仍然遵守和在同一个进程之间工作的原则,一端写,在另一端读取;
这和pipe有一定的区别,pipe是单工通信,一端要么是读端要么是写端,而socketpair实现了双工套接字,也就没有所谓的读端和写端的区分
验证代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>
#include <sys/socket.h>
#include <stdlib.h>
#define BUF_SIZE 30
int main(){
int s[2];
int w,r;
char * string = "This is a test string";
char * buf = (char*)calloc(1 , BUF_SIZE);
pid_t pid;
if( socketpair(AF_UNIX,SOCK_STREAM,0,s) == -1 ){
printf("create unnamed socket pair failed:%s\n",strerror(errno) );
exit(-1);
}
/***********Test : fork but don't close any fd in neither parent nor child process***********/
if( ( pid = fork() ) > 0 ){
printf("Parent process's pid is %d\n",getpid());
close(s[1]);
if( ( w = write(s[0] , string , strlen(string) ) ) == -1 ){
printf("Write socket error:%s\n",strerror(errno));
exit(-1);
}
}else if(pid == 0){
printf("Fork child process successed\n");
printf("Child process's pid is :%d\n",getpid());
close(s[0]);
}else{
printf("Fork failed:%s\n",strerror(errno));
exit(-1);
}
/*****read***In parent and child****/
if( (r = read(s[1], buf , BUF_SIZE )) == -1){
printf("Pid %d read from socket error:%s\n",getpid() , strerror(errno) );
exit(-1);
}
printf("Pid %d read string in same process : %s \n",getpid(),buf);
printf("Test successed , %d\n",getpid());
exit(0);
}
以上代码中在父子进程之间各关闭了一个描述符,则在父进程写可从子进程读取,反之若子进程写,父进程同样可以读取;大家可以验证下
另外,我也测试了在父子进程中都不close(s[1]),也就是保持两个读端,则父进程能够读到string串,但子进程读取空串,或者子进程先读了数据,父进程阻塞于read操作!
之所以子进程能读取父进程的string,是因为fork时,子进程继承了父进程的文件描述符的,同时也就得到了一个和父进程指向相同文件表项的指针;若父子进程均不关闭读端,因为指向相同的文件表项,这两个进程就有了竞争关系,争相读取这个字符串.父进程read后将数据转到其应用缓冲区,而子进程就得不到了,只有一份数据拷贝(若将父进程阻塞一段时间,则收到数据的就是子进程了,已经得到验证,让父进程sleep(3),子进程获得string,而父进程获取不到而是阻塞)