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. 小结



你可能感兴趣的:(unix)