## 1. 概述
?unix域協議并不是一個實際的協議族,而是在單個主機上執行客戶/服務器通信的一種方法。unix域提供兩類套接字:字節流套接字(類似TCP)和數據報套接字(類似UDP)。使用unix域協議有如下的優勢:
(1)unix域套接字往往比通信兩端位于同一個主機的TCP套接字快出一倍。
(2)unix域套接字可用于在同一個主機上的不同進程之間傳遞描述符。
(3)unix域套接字較新的實現把客戶的憑證(用戶ID和組ID)提供給服務器,從而能夠提供額外的安全檢查措施。
?unix域中用于標識客戶和服務器的協議地址是普通文件系統中的路徑名。
### 1) unix域套接字地址結構
struct sockaddr_un{
?sa_family_t?? ?sun_family;?? ??? ?/*AF_LOCAL*/
?char?? ??? ?sun_path[104];?? ??? ?/*null-terminated pathname*/
};
### 2) unix域套接字的bind調用
~~~
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <netdb.h>
#define SA struct sockaddr
int main(int argc, char **argv)
{
int sockfd;
socklen_t len;
struct sockaddr_un addr1, addr2;
if (argc != 2){
printf("argument should be 2\n");
exit(1);
}
sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);
unlink(argv[1]);
bzero(&addr1, sizeof(addr1));
addr1.sun_family = AF_LOCAL;
strncpy(addr1.sun_path, 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);
return 0;
}
~~~
程序輸出:
~~~
leichaojian@ThinkPad-T430i:~$ ./unixbind /tmp/moose
bound name = /tmp/moose, returned len = 13
leichaojian@ThinkPad-T430i:~$ ll /tmp/moose
srwxrwxr-x 1 leichaojian leichaojian 0 10月 8 22:18 /tmp/moose=
~~~
### 3)套接字函數
(1)由bind創建的路徑名默認訪問權限應為0777,并按照當前umask值進行修正。
(2)與unix域套接字關聯的路徑名應該是一個絕對路徑名,而不是一個相對路徑名。避免使用后者的原因是它的解析依賴于調用者的當前工作目錄。也就是說,要是服務器捆綁一個相對路徑名,客戶就得在與服務器相同的目錄中(或者必須知道這個目錄)才能成功調用connect或sendto。
(3)在connect調用中指定的路徑名必須是一個當前綁定在某個打開的unix域套接字上的路徑名,而且它們的套接字類型(字節流或數據報)也必須一致。出錯條件包括:(a)該路徑名已存在卻不是一個套接字(unlink來刪除已存在文件);(b)該路徑名已存在且是一個套接字,不過沒有與之關聯的打開的描述符;(c)該路徑名已存在且是一個打開的套接字,不過類型不符。
(4)調用connect連接一個unix域套接字涉及的權限測試等同于調用open以只寫方式訪問相應的路徑名。
(5)unix域字節流套接字類似TCP套接字:它們都為進程提供一個無記錄邊界的字節流接口。
(6)如果對于某個unix域字節流套接字的connect調用發現這個監聽套接字的隊列已滿,調用就立即返回一個ECONNREFUSED錯誤。
(7)unix域數據報套接字類似于UDP套接字:它們都提供一個保留記錄邊界的不可靠的數據報服務。
(8)在一個未綁定的unix域套接字上發送數據報不會自動給這個套接字捆綁一個路徑名,connect也一樣。
## 2. unix域字節流客戶/服務器程序
服務端:
~~~
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define SA struct sockaddr
#define MAXLINE 1024
#define UNIXSTR_PATH "/tmp/unix.str"
extern int errno;
void sig_chld(int);
void str_echo(int sockfd);
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_un cliaddr, servaddr;
listenfd = socket(AF_LOCAL, SOCK_STREAM, 0);
unlink(UNIXSTR_PATH);
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, UNIXSTR_PATH);
bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
listen(listenfd, 5);
signal(SIGCHLD, sig_chld);
for ( ; ; ){
clilen = sizeof(cliaddr);
if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0){
if (errno == EINTR)
continue;
else{
printf("accept error\n");
exit(0);
}
}
if ((childpid = fork()) == 0){
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}
void str_echo(int sockfd)
{
char recvline[MAXLINE];
int n;
while ((n = read(sockfd, recvline, MAXLINE)) > 0){
recvline[n] = '\0';
write(sockfd, recvline, n);
}
}
void sig_chld(int signo)
{
pid_t pid;
int stat;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
~~~
客戶端:
~~~
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SA struct sockaddr
#define UNIXSTR_PATH "/tmp/unix.str"
#define MAXLINE 1024
void str_cli(FILE *fd, int sockfd);
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_un servaddr;
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);
exit(0);
}
void str_cli(FILE *fd, int sockfd)
{
int n;
int recvline[MAXLINE], sendline[MAXLINE];
while (fgets(sendline, MAXLINE, fd) != NULL){
write(sockfd, sendline, strlen(sendline));
if ((n = read(sockfd, recvline, MAXLINE)) > 0){
fputs(recvline, stdout);
}
}
}
~~~
程序輸出:
服務端:
~~~
leichaojian@ThinkPad-T430i:~$ ./unixstrserv
child 12698 terminated
child 12700 terminated
~~~
客戶端:
~~~
leichaojian@ThinkPad-T430i:~$ ./unixstrcli
hello world
hello world
^C
leichaojian@ThinkPad-T430i:~$ ./unixstrcli
what
what
^C
~~~
## 3. 描述符傳遞
?? 當考慮從一個進程到另一個進程傳遞打開的描述符時,我們通常會想到:
1)fork調用返回之后,子進程共享父進程的所有打開的描述符。
2)exec調用執行之后,所有描述符通常保持打開狀態不變。
?? 在第一個例子中,進程先打開一個描述符,再調用fork,然后父進程關閉這個描述符,子進程則處理這個描述符。這樣一個打開的描述符就從父進程傳遞到子進程。那如何從子進程傳遞描述符到父進程呢?
?? 當前的unix系統提供了用于從一個進程向任一其他進程傳遞任一打開的描述符的方法。這種技術要求首先在這兩個進程之間創建一個unix域套接字,然后使用sendmsg跨這個套接字發送一個特殊信息。這個消息由內核來專門處理,會把打開的描述符從發送進程傳遞到接收進程。
?? 在兩個進程之間傳遞描述符涉及的步驟如下:
1) 創建一個字節流或數據報的unix域套接字
?? 如果目標是fork一個子進程,讓子進程打開待傳遞的描述符,再把它傳遞回父進程,那么父進程可以預先調用socketpair創建一個可用于父子進程之間交換描述符的流管道。
?? 如果進程之間沒有親緣關系,那么服務器必須創建一個unix域字節流套接字,bind一個路徑名到該套接字,以允許客戶進程connect到該套接字。然后客戶可以向服務器發送一個打開某個描述符的請求,服務器再把該描述符通過unix域套接字傳遞回客戶。
2) 發送進程通過調用返回描述符的任一unix函數打開一個描述符,這些函數的例子有open,pipe,mkfifo,socket和accept,可以在進程之間傳遞的描述符不限類型。
3) 發送進程創建一個msghdr結構,其中含有待傳遞的描述符。
4) 接收進程調用recvmsg在來自步驟1的unix域套接字上接收這個描述符。
程序mycat.c如下:
~~~
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
extern int errno;
#define BUFFSIZE 4096
int my_open(const char *pathname, int mode);
ssize_t read_fd(int fd, void *tr, size_t nbytes, int *recvfd);
int main(int argc, char **argv)
{
int fd, n;
char buff[BUFFSIZE];
if (argc != 2){
printf("argument should be 2\n");
return 1;
}
if ((fd = my_open(argv[1], O_RDONLY)) < 0){
printf("cannot open %s\n", argv[1]);
exit(1);
}
while ((n = read(fd, buff, BUFFSIZE)) > 0)
write(STDOUT_FILENO, buff, n);
return 0;
}
int my_open(const char *pathname, int mode)
{
int fd, sockfd[2], status;
pid_t childpid;
char c, argsockfd[10], argmode[10];
//socketpair函數創建兩個隨后連接起來的套接字,因為隨后fork,所以實際上sockfd存儲的是連接起來的父子進程
socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);
if ((childpid = fork()) == 0){
close(sockfd[0]); //因為子進程會完全復制父進程的描述符,所以要關閉父進程的描述符
snprintf(argsockfd, sizeof(argsockfd), "%d", sockfd[1]); //子進程將描述符傳遞給流管道父進程的一端(即sockfd[1])
snprintf(argmode, sizeof(argmode), "%d", mode);
execl("./openfile", "openfile", argsockfd, pathname, argmode, (char *)NULL);
printf("execl error\n");
}
close(sockfd[1]); //父進程中關閉子進程的描述符(這里如果父進程關閉sockfd[1],則子進程就關閉sockfd[0],反之亦然)
waitpid(childpid, &status, 0);
if (WIFEXITED(status) == 0){
printf("child did not terminate\n");
exit(1);
}
if ((status = WEXITSTATUS(status)) == 0)
read_fd(sockfd[0], &c, 1, &fd);
else{
errno = status;
fd = -1;
}
close(sockfd[0]);
return (fd);
return 1;
}
ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd)
{
struct msghdr msg;
struct iovec iov[1];
ssize_t n;
#ifdef HAVE_MSGHDR_MSG_CONTROL
union{
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
} control_un;
struct cmsghdr *cmptr;
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
#else
// int newfd;
// msg.msg_accrights = (caddr_t)&newfd;
// msg.msg_accrightslen = sizeof(int);
#endif
msg.msg_name = NULL;
msg.msg_namelen = 0;
iov[0].iov_base = ptr;
iov[0].iov_len = nbytes;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
if ((n = recvmsg(fd, &msg, 0)) <= 0)
return n;
#ifdef HAVE_MSGHDR_MSG_CONTROL
if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL &&
cmptr->cmsg_len == CMSG_LEN(sizeof(int))){
if (cmptr->cmsg_level != SQL_SOCKET){
printf("control level != SOL_SOCKET\n");
exit(1);
}
if (cmptr->cmsg_type != SCM_RIGHTS){
printf("control type != SCM_RIGHTS\n");
exit(1);
}
*recvfd = *((int)*)CMSG_DATA(cmptr);
} else
*recvfd = -1;
#else
// if (msg.msg_accrightslen == sizeof(int))
// *recvfd = newfd;
// else
// *recvfd = -1;
#endif
return n;
}
~~~
?? 但是我在調試程序的時候發現,在函數my_open中并沒有進入子進程,即fork()后并沒有進入子進程進行執行execl函數,導致程序的失敗,這到底是為什么呢?