UNP Chapter 14 - Unix域协议
14.1. 概述
Unix域协议并不是一个实际的协议族,它只是在同一台主机上进行客户-服务器通信时,使用与在不同主机上的客户和服务器间通信时相同的API(套接口或XTI)的一种方法。
当客户和服务器在同一台主机上时,Unix域协议是IPC通信方式的一种替代品。
Unix域提供了两种类型的套接口:字节流套接口(与TCP类似)和数据报套接口(与UDP类似)。
14.2. Unix域套接口地址结构
// 在《sys/un.h>头文件中定义的Unix域套接口地址结构
struct sockaddr_un
{
uint8_t sun_len;
sa_family_t sun_family; /* AF_LOCAL */
char sun_path[104]; /* null-terminated pathname */
};
下面程序建立一个Unix域套接口,给它捆绑一个路径名,然后调用getsockname输出已绑定的路径名。
#include "unp.h"
int main(int argc, char * * argv)
{
int sockfd;
socklen_t len;
struct sockaddr_un addr1, addr2;
if(argc != 2)
err_quit("usage: unixbind <pathname>");
sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
unlink(argv[1]); /* OK if this fails */ // 如果路径名已存在,先用unlink将其删除,如果路径名不存在,unlink会返回错误。
bzero(&addr1, sizeof(addr1));
addr1.sun_family = AF_LOCAL;
strncpy(addr1.sun_puth, argv[1], sizeof(addr1.sun_path)-1 );
Bind(sockfd, (SA *)&addr1, SUN_LEN(&addr1));
len = sizeof(addr2);
Getsockname(sockfd, (SA*)&addr2, &len);
printf("bound name = %s, returned len = %d \n", addr2.sun_path, len);
exit(0);
}
14.3. socketpair函数
socketpair函数建立一对相互连接的套接口,这个函数支队Unix域套接口使用。
#include <sys/socket.h>
int socketpair(int family, int type, int protocol, int sockfd[2]); //返回: 成功返回0,出错返回-1
family必须为AF_LOCAL,protocol必须为0,type可以是SOCK_STREAM或SOCK_DGRAM,新创建的两个套接口描述字作为sockfd[0]和sockfd[1]返回,创建的两个套接口是没有名字的,即没有涉及隐式bind。
以SOCK_STREAM作为type调用socketpair所得到的结果称为流管道(stream pipe),这和一般的Unix管道(由pipe函数生成)类似,但流管道是全双工的,即两个描述字都是可读写的。
14.4. 套接口函数
当用于Unix域套接口时,套接口函数有一些差别和限制。
14.5. Unix域字节流客户-服务器程序
重写的TCP回射客户-服务器程序,以使用Unix域套接口
#include "unp.h"
int main(int argc, char * * argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_un cliaddr, servaddr; //两个套接口地址结构的数据类型现在是sockaddr_un
void sig_chld(int);
listenfd = Socket(AF_LOCAL, SOCK_STREAM, 0); //socket第一个参数AF_LOCAL, 以建立Unix域字节流套接口
unlink(UNIXSTR_PATH); //常值为/tmp/unix.str
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, UNIXSTR_PATH);
Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
Signal(SIGCHLD, sig_chld);
for( ; ; )
{
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd, (SA*)&cliaddr, &clilen)) < 0)
{
if(errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
if((childpid = Fork()) == 0) /* child process */
{
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
下面是使用Unix域字节流协议的回射客户程序
#include "unp.h"
int main(int argc, char * * argv)
{
int sockfd;
struct sockaddr_un servaddr; //含有服务器地址的套接口地址结构现在是一个sockaddr_un结构
sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, UNIXSTR_PATH);
Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));
str_cli(stdin, sockfd); /* do it all */
exit(0);
}
14.6. Unix域数据报客户-服务器程序
重写的UDP客户-服务器程序,以使用Unix域数据报套接口
#include "unp.h"
int main(int argc, char * * argv)
{
int sockfd;
struct sockaddr_un servaddr, cliaddr;
sockfd = Socket(AF_LOCAL, SOCK_DGRAM, 0);
unlink(UNIXDG_PATH);
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, UNIXDG_PATH);
Bind(sockfd, (SA*)&servaddr, sizeof(servaddr));
dg_echo(sockfd, (SA*)&cliaddr, sizeof(cliaddr));
}
下面是使用Unix域数据报协议的回射客户程序
#include "unp.h"
int main(int argc, char * * argv)
{
int sockfd;
struct sockaddr_un cliaddr, servaddr;
sockfd = Socket(AF_LOCAL, SOCK_DGRAM, 0);
bzero(&cliaddr, sizeof(cliaddr)); /* bind an address for us */
cliaddr.sun_family = AF_LOCAL;
strcpy(cliaddr.sun_path, tmpnam(NULL)); //与UDP客户不同,当使用Unix域数据报协议时,我们必须显示地给套接口bind一个路径名
Bind(sockfd, (SA*)&cliaddr, sizeof(cliaddr));
bzero(&servaddr, sizeof(servaddr)); /* fill in server address */
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, UNIXDG_PATH);
dg_cli(stdin, sockfd, (SA*)&servaddr, sizeof(servaddr));
exit(0);
}
14.7. 描述字传递
当考虑从一个进程向另一个进程传递打开的描述字时,我们通常会:
1. 在fork调用后,子进程共享父进程的所有打开的描述字
2. 在调用exec时所有描述字仍保持打开
第一个例子中进程打开一个描述字,调用fork,然后父进程关闭描述字,让子进程处理这个描述字。这样将一个打开的描述字从父进程传递到子进程。
但我们也想让子进程打开一个描述字并将其传给父进程。当前的Unix系统提供了一个方法,可以从一个进程向其他任何进程传递打开的描述字。也就是说,进程之间不需要有什么关系,譬如父子进程。这种技术要求先在两个进程之间建立一个Unix域套接口,然后用sendmsg从这个套接口发一个特殊的消息,这个消息由内核做特殊处理,以将打开的描述字从发送方传递到接收方。
1. 创建一个字节流的或数据报的Unix域套接口。如果目标是fork一个子进程,让子进程打开描述字并将它传回父进程,那么父进程可以用socketpair创建一个流管道,用它来传递描述字。如果进程之间没有亲缘关系,那么服务器必须创建一个Unix域字节流套接口,bind一个路径名,让客户connect到这个套接口。然后客户可以向服务器发送一个请求以打开某个描述字,服务器将描述字通过Unix域套接口传回。在客户和服务器之间也可以使用Unix域数据报套接口,但这样做没什么好处,而且数据报存在丢失的可能性。
2. 进程可以用任何返回描述字的Unix函数打开一个描述字:譬如open, pipe, mkfifo, socket或accept。可以在进程之间传递任何类型的描述字,这是为什么我们将这种技术称为“传递描述字”而不是“传递文件描述字”的原因。
3. 发送进程建立一个msghdr结构,其中包含要传递的描述字。发送进程调用sendmsg通过第一步得到的Unix域套接口发出描述字,这是我们说描述字是“在飞行中(in flight)”的,即使在发送进程调用sendmsg之后,但在接收进程调用recvmsg之前将描述字关闭,它仍会为接收进程保持打开状态,描述字的发送导致它的访问技术加1.
4. 接收进程调用recvmsg在Unix域套接口上接收描述字,传递描述字不是传递描述字的编号,而是在接收进程中创建一个新的描述字,指向内核的文件表中与发送进程发送的描述字相同的项。
下面提供一个描述字传递的例子
14.8. 接收发送者的凭证
下面列出了另一种能在Unix域套接口上作为辅助数据传送的数据:fcred结构的用户凭证,该结构在<sys/ucred.h>头文件中定义。
struct fcred
{
uid_t fc_ruid; /* read user ID */
gid_t fc_rgid; /* read group ID */
char fc_login[MAXLOGNAME]; /* setlogin() name */
uid_t fc_uid; /* effective user ID */
short fc_ngroups; /* number of groups */
gid_t fc_groups[NGROUPS]; /* supplementary group IDs */
};
#define fc_gid fc_groups[0] /* effective group ID */
通常MAXLOGNAME为16,NGROUPS也是16,fc_ngroups总是至少为1,fc_groups数组的第一个元素为有效的组ID。
只要遵循以下条件,该信息在Unix域套接口上总是可用的:
1. 凭证是作为辅助数据在Unix域套接口上发送的,但接收方必须打开LOCAL_CREDS套接口选项。
2. 在数据报套接口上,每个数据报都带有凭证。在字节流套接口上凭证只发送一次,是在第一次发送数据时发出的。
3. 凭证不能和描述字一起发送,也就是说,在单个消息中只能发送这两种辅助数据中的一种。
4. 用户是不能伪造凭证的。也就是说,在Unix域套接口上发送辅助数据时,内核会加以验证,确保不是级别为SOL_SOCKET, 类型为SCM_CREDS的辅助数据,如果发送方试图伪造凭证,辅助数据会被内核丢弃。
14.9. 小结