关于unix domain socket的一个文章


原文地址 http://www.thomasstover.com/uds.html

Unix Domain Socket揭密

初稿于2006 二月
最后更新于11月11日, 2011

★"Unix Domain Sockets? - 我以前听说过"★

经常被忽略的Unix domain socket机制是现代Unix最强特性之一。大多数Unix套接字编
程书籍只是从学术上讨论这个话题,甚至没有解释为什么有它或者它用来做什么。它除
了使用到操作系统的某些功能外,还是新接触Linux,BSD,和其他Unix系统的人员必须
认识的领域。本文不是关于套接字的指导手册,而是套接字编程中一个领域的特性及优
点的回顾。

★背景和内容★

UDS有时候称为“本地”套接字。这有些误导,因为它似乎暗示着和本地环回适配器有什
么关系。和UDS最接近的东西应该是管道。Unix管道很大程度上说操作系统的基石。就像
水在一个方向流动的水管,字节流从管道的写端流向读端。一个独立的open文件描述符
维持着对管道读写端的一个引用。管道的另外一端可以是另外一个进程或线程,只要它
们处于同一个本地计算机上。让我们回顾下Unix管道的不同特性

    .对于小于4kb的写操作是原子性的
    .管道可以通过fork()调用来创建和继承,并在线程间共享
    .管道也可以赋予文件系统中的一个名称。这些fifos(或有名管道)存在于进程的生
命周期之外。两个不同进程可以用open()获取对一个管道的引用,但是对文件描述符只能
拥有一个继承。
    .向一个满的缓冲区的管道进行写会导致一个SIGSTOP信号。
    .管道通常认为比UDS更快。
    .使用管道的进程必须用read()和write()和内核进行上下文切换。    
    
管道必须从一端写从另外一端读的例外是,Solaris上管道是全双工的。以Linux和BSD为
例,全双工管道是用了2个不同的管道。有名管道和无名管道本质上是相同的,只是有名
管道用mkfifo()创建而无名管道用pipe()创建。

Windows API的情况并非如此。Windows提供了2种不同的机制,它称为有名和匿名管道。
匿名管道在所有Windows版本中都可用,并且表现非常像Unix管道。除了慢些,还有一些
其他变化,比如可调整的管道缓存大小,其影响到原子写的阀值。微软有名管道比较类
似于UDS。它们只在Windows NT系列版本上可用,并完全不使用Windows网络套接字接口
即winsock。因此,传统的套接字多路策略比如select()不可以用Windows有名管道实现
,而这些策略可以用UDS实现。它们具体通过服务器消息块(SMB)到达多台计算机的优点
。

更新: 值得一提的是OS/2也有一个有名管道, 根据你正在读取的信息, 可能或无法通过S
MB和Windows有名管道进行通讯。这种SMB有名管道也可以存在于运行Samba作为Winbind
机制一部分的*nix盒式设备上。但是,据我所知, 目前无法在Unix环境下通过Samba而不
附加Samba的授权限制来用SMB有名管道和UDS建立连接。其他可以想象到的途径是用Cygw
in,或是socat。

更新:另外一个值得和Unix套接字对比的是Linux特有的netlink套接字。Netlink套接字
用于在用户空间和内核间通讯。比如,用来更新路由或防火墙信息。这就给为什么创建
另外一种套接字类型,而不是简单的使用抽象名字(下面提到)的unix套接字找到一个很
好的解释。

★内部分析★

UDS只存在于一台独立的计算机上。这里的“域”和NIS,LDAP,或Windows没有什么关系
,而是指文件系统.UDS用一个文件名标识,就像有名管道一样。和UDS通讯的程序必须在
同一个计算机上,所以他们之间并不是网络上概念,而是进程间通讯的概念。这就解释
了为什么大多数网络书籍忽略了它。它们使用TCPIP相同的套接字接口实现,就像其他网
络协议支持的那样。这时你应该至少在思考两个问题:“为什么网络层程序会支持UDS来
作为传输?”和“为什么程序会使用UDS来做IPC而不是管道,信号,或共享内存?”,
这里快速解答下

    .UDS在网络协议层面上是安全的,因为:
        它们不会在不可信的网络上被窃听
        没有特定的转发机制,远程计算机不能和它连接
    .它们不需要正确配置好的网络,甚至不需要网络
    .它们是全双工的
    .多个客户端可以用一个相同的命名套接字来连接到相同的服务端
    .同时支持无连接(数据块)和有连接(流)的通讯
    .在IPC层面上UDS是安全的,因为:
        可以在套接字上配置文件权限来限制某些用户或组的访问
        因为所有都是发生在同一个内核控制的计算机上,内核知道关于套接字和参与
方的所有信息。这就意味着需要认证的服务端程序可以查出哪个没有获取到用户名和密
码的用户正在连接它们
    .一个进程中打开的文件描述符可以传输给完全不相关的进程
    .参与者可以知道UDS另外一端的PID
    .套接字的路径名限制为UNIX_PATH_MAX长,UNIX中定义为108。

并非所有这些特性都在每种Unix系统上可用。更坏的情况是他们实现还可能不同。不过
基本操作大多数还是统一的。我们来看些例子。
更新:许多套接字程序使用socketpair()函数。其创建了一个配对的套接字,并非用通
常的、协议上的、内建的过程。典型的,这是用来创建一个线程内部通讯的套接字,或
是继承进程间通讯。PF_UNIX对于socketpair()域参数而言是一个有效参数,是创建UDS
的另外一种方法。

★基于连接的基本客户端和服务器★

我们从最基本的客户端和分支服务端开始。分支服务端会产生一个新进程来处理每个新
来的连接。当连接关闭后,其处理进程就退出了。这类服务端经常名声不好,因为它作
为web服务器性能太差了。它作为web服务器性能差的原因是因为HTTP,每个独立的请求
都创建自己的连接。这样服务器就花了相当不成比例的时间来创建和销毁进程而不是处
理请求。通常不好理解的是,对比于其他类型的在用户使用服务器的整个期间维护一个
独立连接的协议,分支服务端被认为是一个可接受的设计。比如以Open SSH为例。对于
非web服务端应用程序,这种设计的主要问题是它不能直接在所有不同的处理实例间共享
信息。和其他设计一样,多路和多线程设计在这里已经过时了,而简单的分支服务端就
和例子说展示的一样好。把它看作服务器设计的“hello world”。看下面的源代码

更新: 尽管HTTP2使用持久连接确实显著的减轻了上面提到问题,为什么分支服务器会选
择成1:1的对客户端用户处理进程, 看来对HTTP而言仍然不是一个优势.

client1.c 代码

1 #include <stdio.h>
2 #include <sys/socket.h>
3 #include <sys/un.h>
4 #include <unistd.h>
5 #include <string.h>
6
7 int main(void)
8 {
9     struct sockaddr_un address;
10    int  socket_fd, nbytes;
11    char buffer[256];
12
13    socket_fd = socket(PF_UNIX, SOCK_STREAM, 0);
14    if(socket_fd < 0)
15    {
16        printf("socket() failed\n");
17        return 1;
18    }
19
20    /* 从一个干净的地址结构开始 */
21    memset(&address, 0, sizeof(struct sockaddr_un));
22 
23    address.sun_family = AF_UNIX;
24    snprintf(address.sun_path, UNIX_PATH_MAX, "./demo_socket");
25
26    if(connect(socket_fd, 
27            (struct sockaddr *) &address, 
28            sizeof(struct sockaddr_un)) != 0)
29    {
30        printf("connect() failed\n");
31        return 1;
32    }
33
34    nbytes = snprintf(buffer, 256, "hello from a client");
35    write(socket_fd, buffer, nbytes);
36 
37    nbytes = read(socket_fd, buffer, 256);
38    buffer[nbytes] = 0;
39
40    printf("MESSAGE FROM SERVER: %s\n", buffer);
41
42    close(socket_fd);
43
44    return 0;
45}

server1.c代码

1 #include <stdio.h>
2 #include <sys/socket.h>
3 #include <sys/un.h>
4 #include <sys/types.h>
5 #include <unistd.h>
6 #include <string.h>
7
8 int connection_handler(int connection_fd)
9 {
10    int nbytes;
11    char buffer[256];
12
13    nbytes = read(connection_fd, buffer, 256);
14    buffer[nbytes] = 0;
15
16    printf("MESSAGE FROM CLIENT: %s\n", buffer);
17    nbytes = snprintf(buffer, 256, "hello from the server");
18    write(connection_fd, buffer, nbytes);
19 
20    close(connection_fd);
21    return 0;
22}
23
24int main(void)
25{
26    struct sockaddr_un address;
27    int socket_fd, connection_fd;
28    socklen_t address_length;
29    pid_t child;
30 
31    socket_fd = socket(PF_UNIX, SOCK_STREAM, 0);
32    if(socket_fd < 0)
33    {
34        printf("socket() failed\n");
35        return 1;
36    } 
37
38    unlink("./demo_socket");
39
40    /* 从一个干净的地址结构开始 */
41    memset(&address, 0, sizeof(struct sockaddr_un));
42
43    address.sun_family = AF_UNIX;
44    snprintf(address.sun_path, UNIX_PATH_MAX, "./demo_socket");
45
46    if(bind(socket_fd, 
47         (struct sockaddr *) &address, 
48         sizeof(struct sockaddr_un)) != 0)
49    {
50        printf("bind() failed\n");
51        return 1;
52    }
53
55    if(listen(socket_fd, 5) != 0)
56    {
57        printf("listen() failed\n");
58        return 1;
59    }
60
61    while((connection_fd = accept(socket_fd, 
62                               (struct sockaddr *) &address,
63                               &address_length)) > -1)
64    {
65        child = fork();
66        if(child == 0)
67        {
70          /* 现在处于新建的连接处理进程中了 */
71          return connection_handler(connection_fd);
72        }
73
74        /* 还在服务端进程中 */
75        close(connection_fd);
76    }
77
78    close(socket_fd);
79    unlink("./demo_socket");
80    return 0;
81}

有了C的基础知识垫底,初级水平的Unix系统程序员,初级套接字编程,如何查询man页
面,以及Google,上面的例子将会帮你创建一个UDS客户端和服务端。去试试打开一对终
端窗口,一个运行服务端,一个运行客户端。试验后再添加一些类似于sleep(15)的东西
到服务端的处理例程中,在它答复客户端之前。再用多于2个的终端,一个是客户端,一
个执行top或ps -e,或是netstat -ax。试验下,是否学习到什么了?

★实际套接字IO接口★

套接字编程的新手或老手经常没有意识到套接字接口实际上也有自己的IO例程:send(),
 sendto(), sendmsg(), recv(), recvfrom(), 和recvmsg()。这些函数操作对象是套接
字,而不是文件描述符。Unix在套接字创建时自动给其创建一个文件描述符(用相同的整
数值),以便可以对套接字进行IO操作,就像用read,write对普通文件描述符一样操作
。这就是为什么大多数时候不需要直接使用底层的套接字IO函数。某些特性确实要求使
用底层函数(比如UDP)。这也就是为什么在winsock版本2或者更高(其内部使用相同的开
源BSD套接字代码,不像winsock1)的Windows中可以用相同的send/recv套接字IO函数(虽
然不推荐)。同时还注意到,Windows也提供了一种方法来想处理Windows文件一样处理套
接字。

★认证服务器★

让我们想象下一个类似PostgreSQL的数据库服务器。这个服务器会迫使美国连接上的客
户端程序用用户名密码进行认证。它这么做是为了基于所连接上的客户帐号来加强其内
部的安全策略。每次都必须通过用户名密码认证有点过时了,所以也可以使用密钥对验
证。在本地登陆的情形下(客户端和服务端在同一个设备上),可以使用UDS所熟知的特性
之一,也就是证书传递。

可能有部分是不同的,所以确认下你的参考资料,让我们看下Linux上是怎么做的。

Linux用一个底层套接字函数来收集UDS另外一端进程的证书,即getsockopt()的多用途

代码
1 struct ucred credentials;
2 int ucred_length = sizeof(struct ucred);
3
4 /* 填充用户数据结构 */
5 if(getsockopt(connection_fd, SOL_SOCKET, SO_PEERCRED, &credentials, 
&ucred_length))
6 {
7  printf("could obtain credentials from unix domain socket"); <-这里应该是co
uldn't?
8  return 1;
9 }
10
11 /*套接字另外一端的进程id */
12 credentials.pid;
13
14 /* 套接字另外一端进程的有效UID */
15 credentials.uid;
16
17 /* 套接字另外一端进程的有效主GID */
18 credentials.gid;
19
20 /* 为了获取替补组,我们将必须在我们的账户数据库里面查找
21    在根据UID进行递归搜索后来获取账户名
22    我们可以利用这个机会来检查它是否是个合法的帐号
23 */

★传输文件描述符★

可以用2种方法把文件描述符从一个进程传输到另外一个进程。一种方法是继承,另外一
种是通过UDS。我知道有3个可能这么做的原因。首先是平台上没有证书传输机制但却需
要传输一个文件描述符,在基于文件系统特权展示的认证方案中。其次,如果一个进程
有文件系统特权,而另外一个进程没有。第三,在服务器将一个连接的文件描述符传递
给另外一个已经启动的同类协助进程的场景下。再强调一次这种情况和操作系统之间是
不同的。在Linux上,这是通过一种称为配套数据的套接字特性来完成的。

一端可以给另外一端用附带的配置数据来发送信息(至少一个字节)。通常这种机制是用
来针对不同底层网络协议的奇怪特性的,比如TCP/IP的带外数据。它是用底层套接字函
数sendmsg来实现的,其接收了IO向量数组和控制数据消息对象作为其msghdr结构参数的
成员。对应的,作为控制目的,套接字的数据采用cmsghdr的结构形式。该结构的成员可
以基于其使用的套接字类型表示不同的含义。让人更抓狂的是这些结构大部分需要用宏
来修改。这里是两个例子,它们基于文章末尾提到的Warren Gay的书中的例子。套接字
一端用send_fd读取发给它的数据,而不是使用recv_fd,来得到一个大写的F。
 
1 int send_fd(int socket, int fd_to_send)
2 {
3  struct msghdr socket_message;
4  struct iovec io_vector[1];
5  struct cmsghdr *control_message = NULL;
6  char message_buffer[1];
7  /* storage space needed for an ancillary element with a paylod of length 
is CMSG_SPACE(sizeof(length)) */
8  char ancillary_element_buffer[CMSG_SPACE(sizeof(int))];
9  int available_ancillary_element_buffer_space;
10
11  /* 只是传输一个字节的向量 */
12  message_buffer[0] = 'F';
13  io_vector[0].iov_base = message_buffer;
14  io_vector[0].iov_len = 1;
15
16  /* 初始化套接字消息 */
17  memset(&socket_message, 0, sizeof(struct msghdr));
18  socket_message.msg_iov = io_vector;
19  socket_message.msg_iovlen = 1;
20
21  /* 为配套数据提供空间 */
22  available_ancillary_element_buffer_space = CMSG_SPACE(sizeof(int));
23  memset(ancillary_element_buffer, 0, 
available_ancillary_element_buffer_space);
24  socket_message.msg_control = ancillary_element_buffer;
25  socket_message.msg_controllen = 
available_ancillary_element_buffer_space;
26
27  /* 为fd传输而初始化一个独立的配置数据单元 */
28  control_message = CMSG_FIRSTHDR(&socket_message);
29  control_message->cmsg_level = SOL_SOCKET;
30  control_message->cmsg_type = SCM_RIGHTS;
31  control_message->cmsg_len = CMSG_LEN(sizeof(int));
32  *((int *) CMSG_DATA(control_message)) = fd_to_send;
33
34  return sendmsg(socket, &socket_message, 0);
35 }

1 int recv_fd(int socket)
2 {
3  int sent_fd, available_ancillary_element_buffer_space;
4  struct msghdr socket_message;
5  struct iovec io_vector[1];
6  struct cmsghdr *control_message = NULL;
7  char message_buffer[1];
8  char ancillary_element_buffer[CMSG_SPACE(sizeof(int))];
9
10  /* 开始清理 */
11  memset(&socket_message, 0, sizeof(struct msghdr));
12  memset(ancillary_element_buffer, 0, CMSG_SPACE(sizeof(int)));
13
14  /* 创建一块空间来填充消息内容 */
15  io_vector[0].iov_base = message_buffer;
16  io_vector[0].iov_len = 1;
17  socket_message.msg_iov = io_vector;
18  socket_message.msg_iovlen = 1;
19
20  /* 给配套数据提供空间 */
21  socket_message.msg_control = ancillary_element_buffer;
22  socket_message.msg_controllen = CMSG_SPACE(sizeof(int));
23
24  if(recvmsg(socket, &socket_message, MSG_CMSG_CLOEXEC) < 0)
25   return -1;
26
27  if(message_buffer[0] != 'F')
28  {
29   /* this did not originate from the above function */
30   return -1;
31  }
32
33  if((socket_message.msg_flags & MSG_CTRUNC) == MSG_CTRUNC)
34  {
35   /* 没有给配套数据数组提供足够的空间 */
36   return -1;
37  }
38
39  /* 重复配套数据 */
40   for(control_message = CMSG_FIRSTHDR(&socket_message);
41       control_message != NULL;
42       control_message = CMSG_NXTHDR(&socket_message, control_message))
43  {
44   if( (control_message->cmsg_level == SOL_SOCKET) &&
45       (control_message->cmsg_type == SCM_RIGHTS) )
46   {
47    sent_fd = *((int *) CMSG_DATA(control_message));
48    return sent_fd;
49   }
50  }
51
52  return -1;
53 }

★数据块UDS★

大多数时候,通过网络通讯的程序是基于流,或是基于连接的方式。这就是当额外的软
件层,比如TCP的Nagle算法基于底层交换网络上的许多独立的原子报文创建了一个虚拟
通讯回路。有时候,我们只是想用独立的报文进行简单的工作,比如UDP的情况。这种技
术常常成为数据块通讯。该策略允许各种权衡措施。一种是用独立的上下文或处理多个
并发客户端的"主循环"构建一种低负载高性能的服务端。虽然UDS并不是一个使用套接字
接口的网络协议,但还是提供数据块的特性。

数据块通讯在将某类完整原子消息放入单独一个报文的应用程序上运作的最好。这对UDP
可能是一个问题,因为不同的限制会要求报文大小不得超过512字节。在UDS上数据块的
限制会大些。更新:该网页上一个实际的使用数据块UDS的例子是PLC数据代理服务器,d
ndataserver,来自于LAD工具项目。以这种方式设计服务器会是一个不错的策略,其可
以运行多个进程同步共享资源(在这个案例中就是PLC串行网络访问),而不像基于连接的
服务器那么复杂。在我们检查一些样例代码前,让我们回顾下用数据块UDS的一些考虑

.如果我们要服务端的消息(数据块)能够送回客户端,客户端必须绑定一个地址。
.服务端必须使用一个sockaddr结构来持有客户端返回地址的一些引用

server2.c 
代码
1 #include <sys/socket.h>
2 #include <sys/un.h>
3 #include <stdio.h>
4 #include <string.h>
5 #include <unistd.h>
6
7 int main(void)
8 {
9     int socket_fd;
10    struct sockaddr_un server_address; 
11    struct sockaddr_un client_address; 
12    int bytes_received, bytes_sent, address_length;
13    int integer_buffer;
14    
15
16    if((socket_fd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0)
17    {
18        perror("server: socket");
19        return 1;
20    }
21 
22    memset(&server_address, 0, sizeof(server_address));
23    server_address.sun_family = AF_UNIX;
24    strcpy(server_address.sun_path, "./UDSDGSRV");
25 
26    unlink("./UDSDGSRV");       
27    if(bind(socket_fd, (const struct sockaddr *) &server_address, 
sizeof(server_address)) < 0)
28    {
29        close(socket_fd);
30        perror("server: bind");
31        return 1;
32    }
33
34    while(1)
35    {
36      /* address_length是客户端套接字地址结构的长度.
37     听起来它应该是一样的我因为这些套接字都是struct sockaddr_un类型。 
38     不过,代码上可以用不同的套接字类型,也就是说UDS和UPD应该
39     注意拥有和传输正确的值来sendto和reply */
40
41        bytes_received = recvfrom(socket_fd, (char *) &integer_buffer, 
sizeof(int), 0, 
42                            (struct sockaddr *) &(client_address),
43                            &address_length);
44
45        if(bytes_received != sizeof(int))
46        {
47            printf("datagram was the wrong size.\n");
48        } else {
49            integer_buffer += 5;
50
51            bytes_sent = sendto(socket_fd, (char *) &integer_buffer, 
sizeof(int), 0,
52                       (struct sockaddr *) &(client_address), 
53                        address_length);
55        }
56    }
57
58    unlink("./UDSDGSRV");
59    close(socket_fd);
60
61    return 0;
62}

client2.c
1 #include <sys/socket.h>
2 #include <sys/un.h>
3 #include <stdio.h>
4 #include <string.h>
5 #include <unistd.h>
6
7 int main(void)
8 {
9     int socket_fd;
10    struct sockaddr_un server_address; 
11    struct sockaddr_un client_address; 
12    int bytes_received, bytes_sent, address_length, integer_buffer;
13    
14
15    if((socket_fd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0)
16    {
17        perror("client: socket");
18        return 1;
19    }
20       
21    memset(&client_address, 0, sizeof(struct sockaddr_un));
22    client_address.sun_family = AF_UNIX;
23    strcpy(client_address.sun_path, "./UDSDGCLNT");
24
25    unlink("./UDSDGCLNT");
26    if(bind(socket_fd, (const struct sockaddr *) &client_address, 
27         sizeof(struct sockaddr_un)) < 0)
28    {
29        perror("client: bind");
30        return 1;
31    }
32
33    memset(&server_address, 0, sizeof(struct sockaddr_un));
34    server_address.sun_family = AF_UNIX;
35    strcpy(server_address.sun_path, "./UDSDGSRV");
36
37    integer_buffer = 5;
38
39    bytes_sent = sendto(socket_fd, (char *) &integer_buffer, sizeof(int), 
0,
40                     (struct sockaddr *) &server_address, 
41                     sizeof(struct sockaddr_un));
42
43    address_length = sizeof(struct sockaddr_un);
44    bytes_received = recvfrom(socket_fd, (char *) &integer_buffer, 
sizeof(int), 0, 
45                           (struct sockaddr *) &(server_address),
46                           &address_length);
47
48    close(socket_fd);
49
50    if(bytes_received != sizeof(int))
51    {
52        printf("wrong size datagram\n");
53        return 1;
55    }
56
57    printf("%d\n", integer_buffer);
58
59    return 0;
60}

更新: 广播数据块
另外一个网络编程中使用数据块方式的原因是在广播和多播任务中。不幸的是,在UDS中
没有广播机制。Unix提供了killpg()调用来给一个进程组的所有成员发送信号.这可以用
来实现某些广播机制,结合上共享内存的使用。Linux特别的,具有futex()调用能给多
个进程发信号。在Windows上,看下邮件槽IPC机制。

★抽象名称★

另一个Linux特性是针对Unix domain sockets的抽象名称. 抽象名的套接字等同于普通U
DS,除了他们的名字不存在于文件系统中.这就意味2件事:文件权限不适用,并且他们可以
从chroot圈内部访问.技巧就是将地址名字首个自己赋值为null.从netstat -au的输出观
察, 以看出当这些抽象名套接字在使用时时什么样的.例子:

    设置首字节为null

1 address.sun_family = AF_UNIX;
2 snprintf(address.sun_path, UNIX_PATH_MAX, "#demo_socket");
3 
4 address.sun_path[0] = 0;
5
6 bind(socket_fd, (struct sockaddr *) &address, sizeof(struct sockaddr_un));

★结论★
就算你从来不需要用UDS编程,它们也是理解Unix安全模型和操作系统内部机制的一个重
要方面。对于用到UDS的地方,UDS为其开辟了一个可能的新世界。

★扩展阅读书籍★
THE Linux Programming Interface
Linux Application Development
Linux Socket Programming

★译注: ★
文中代码首列以行号开头
UDS 即 Unix Domain Sockets
根据评论对原文做了一些修改




from:http://bbs.sjtu.edu.cn/bbstcon,board,GNULinux,reid,1345013026.html


阅读更多
换一批

没有更多推荐了,返回首页