UNIX域協議并不是一個實際的協議族,而是在單個主機上執行客戶/服務器通信的一種方法,所用API與在不同主機上執行客戶/服務器通信所用的API(套接口API)相同。UNIX域協議可視為進程間通信(ipC)方法之一。
UNIX域提供兩類套接口:字節流套接口(類似TCP)和數據報套接口(類似UDP)。
使用UNIX域套接口的理由有3個:
在源自Berkeley的實現中,UNIX域套接口往往比通信兩端位于同一主機的TCP套接口快出一倍。
UNIX域套接口可用于在同一個主機上的不同進程間傳遞描述字。
UNIX域套接口較新的實現把客戶的憑證(用戶ID和組ID)提供給服務器,從而能夠提供額外的安全檢查措施。
UNIX域中用于標識客戶和服務器的協議地址是普通文件系統中的路徑名。這些路徑名不是普通的UNIX文件:除非把它們和UNIX域套接口關聯起來,否則無法讀寫這些文件。
在頭文件<sys/un.h>中定義了UNIX域套接口地址結構:
struct sockaddr_un { sa_family_t sun_family; /* AF_LOCAL */ char sun_path[104]; /* null-terminated pathname */};
存放在sun_path數組中的路徑名必須以空格字符結尾。
實現提供的SUN_LEN宏以一個指向sockaddr_un結構的指針為參數并返回該結構的長度,其中包括路徑名中非空字節數。
未指定地址(通配地址),通過以空字符串作為路徑名指示,也就是一個sun_path[0]值為0的地址結構。這是UNIX域中與IPv4的INADDR_ANY常值以及IPv6的IN6ADDR_ANY_INIT常值等價的一個地址。
POSIX把UNIX域協議重新命名為“本地IPC”,以消除它對于UNIX操作系統的依賴。歷史性的AF_UNIX常值變為AF_LOCAL。盡管POSIX努力使它獨立于操作系統,它的套接口地址結構仍然保留_un后綴。
實例:UNIX域套接口的bind調用
創建一個UNIX域套接口,往其上bind一個路徑名,再調用getsockname輸出這個綁定的路徑名。
#include <sys/un.h>#include <sys/socket.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/types.h>intmain(int argc, char **argv){ int sockfd; socklen_t len; struct sockaddr_un addr1, addr2; if(argc != 2) { PRintf("usage: unixbind <pathname> "); exit(0); } sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); unlink(argv[1]); /* 如果文件系統中已存在該路徑名,bind將會失敗。為此我們先調用unlink刪除這個路徑名,以防止它已經存在。 */ bzero(&addr1, sizeof(addr1)); addr1.sun_family = AF_LOCAL; strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path) - 1); bind(sockfd, (struct sockaddr *)&addr1, SUN_LEN(&addr1)); len = sizeof(addr2); getsockname(sockfd, (struct sockaddr *)&addr2, &len); printf("bound name = %s, returned len = %d/n", addr2.sun_path, len); exit(0);}
運行結果如下:
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]返回。
本函數類似于UNIX的pipe函數:返回兩個彼此連接的描述字。事實上,源自berkeley的實現通過執行與socketpair一樣的內部操作給出pipe接口。
這樣創建的兩個套接口不曾命名;也就是說其中沒有涉及隱式的bind調用。它與調用pipe創建的普通UNIX管道類似,差別在于流管道(socketpair創建的)是全雙工的,即兩個描述字都是既可讀又可寫。
POSIX不要求全雙工管道。
當用于UNIX域套接口時,套接口函數中存在一些差異和限制:
由bind創建的路徑名缺省訪問權限應為0777(屬主用戶、組用戶和其他用戶都可讀、可寫并可執行),并按照當前umask值進行修正。
與UNIX域套接口關聯的路徑名應該是一個絕對路徑名,而不是一個相對路徑名。
在connect調用中指定的路徑名必須是一個當前捆綁在某個打開的UNIX域套接口上的路徑名,而且它們的套接口類型(字節流或數據報)也必須一致。
調用connect連接一個UNIX域套接口涉及的權限測試等同于調用open以只讀方式訪問相應的路徑名。
UNIX域字節流套接口類似于TCP套接口:它們都為進程提供一個無記錄邊界的字節流接口。
如果對于某個UNIX域字節流套接口的connect調用發現這個監聽套接口的隊列已滿,調用就立即返回一個ECONNREFUSED錯誤。這一點不同于TCP:如果TCP監聽套接口的隊列已滿,TCP監聽端就忽略新到達的SYN,而TCP連接發起端將數次發送SYN進行重試。
UNIX域數據報套接口類似UDP套接口:它們都提供一個保留記錄邊界的不可靠的數據報服務。
在一個未綁定的UNIX域套接口上發送數據報不會自動給這個套接口捆綁一個路徑名,這一點不同于UDP套接口:在一個未綁定的UDP套接口上發送UDP數據報導致給這個套接口捆綁一個臨時端口。這一點意味著除非數據報發送端已經捆綁一個路徑名到它的套接口,否則數據報接收端無法發回應答數據報。類似地,對于某個UNIX域數據報套接口的connect調用不會給本套接口綁定一個路徑名,這一點不同于TCP和UDP。
/* unixstrserv01.c */#include <sys/un.h>#include <errno.h>#include <sys/wait.h>#include <signal.h>#include <sys/socket.h>#include <sys/types.h>#include <stdio.h>#include <stdlib.h>#include <strings.h>#include <string.h>#define UNIXSTR_PATH "/tmp/unix.str"intmain(int argc, char **argv){ int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_un cliaddr, servaddr; void sig_chld(int); daemonize("unixstrserver"); 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, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(listenfd, 5); signal(SIGCHLD, sig_chld); for(;;) { clilen = sizeof(cliaddr); if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) { if(errno == EINTR) continue; /* back to for() */ else { perror("accept"); exit(1); } } if((childpid = fork()) == 0) { close(listenfd); str_echo(connfd); exit(0); } close(connfd); }}voidsig_chld(int signo){ pid_t pid; int stat; while((pid = waitpid(-1, &stat, WNOHANG)) > 0) { printf("child %d terminated/n", pid); } return;}
/* unixstrcli01.c */#include <sys/un.h>#include <strings.h>#include <sys/un.h>#include <sys/socket.h>#include <sys/types.h>#include <stdio.h>#include <stdlib.h>#define UNIXSTR_PATH "/tmp/unix.str"intmain(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, (struct sockaddr *)&servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); exit(0);}
其他相關使用到的函數參見:http://www.CUOXin.com/nufangrensheng/p/3587962.html 以及http://www.CUOXin.com/nufangrensheng/p/3544104.html。
/* unixdgserv01.c */#include <sys/un.h>#include <sys/socket.h>#define UNIXDG_PATH "/tmp/unix.dg"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, (struct sockaddr *)&servaddr, sizeof(servaddr)); dg_echo(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr));}
/* unixdgcli01.c */#include <sys/un.h>#include <stdio.h>#include <stdlib.h>#include <sys/socket.h>#include <sys/types.h>#define UNIXDG_PATH "/tmp/unix.dg"intmain(int argc, char **argv){ int sockfd; struct sockaddr_un cliaddr, servaddr; sockfd = socket(AF_LOCAL, SOCK_DGRAM, 0); bzero(&cliaddr, sizeof(cliaddr)); cliaddr.sun_family = AF_LOCAL; strcpy(cliaddr.sun_path, tmpnam(NULL)); bind(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); bzero(&servaddr, sizeof(servaddr)); servaddr.sun_family = AF_LOCAL; strcpy(servaddr.sun_path, UNIXDG_PATH); dg_cli(stdin, sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); exit(0);}
使用到的相關函數可參考:http://www.CUOXin.com/nufangrensheng/p/3592158.html。
注意
與UDP客戶不同的是,當使用UNIX域數據報協議時,我們必須顯式bind一個路徑名到我們的套接口,這樣服務器才會有回送應答的路徑名。
當考慮從一個進程到另一個進程傳遞打開的描述字時,我們通常會想到:
(1)fork調用返回后,子進程共享父進程的所有打開的描述字。
(2)exec調用執行之后,所有描述字通常保持打開狀態不變。
在(1)中,進程先打開一個描述字,再調用fork,然后父進程關閉這個描述字,子進程則處理這個描述字。這樣一個打開的描述字就從父進程傳遞到子進程。然而我們也可能想讓子進程打開一個描述字并把它傳遞給父進程。
當前的UNIX系系統提供了用于從一個進程到任一其他進程傳遞任一打開的描述字的方法。也就是說,這兩個進程之間無需存在親緣關系。這種技術要求首先在這兩個進程之間創建一個UNIX域套接口,然后使用sendmsg跨這個UNIX域套接口發送一個特殊消息。這個消息由內核處理,從而把打開的描述字從發送進程傳遞到接收進程。使用UNIX域套接口的描述字傳遞方法是最便于移植的編程技術。
在兩個進程之間傳遞描述字涉及的步驟如下:
(1)創建一個字節流或數據報的UNIX域套接口。
如果目標是fork一個子進程,讓子進程打開待傳遞的描述字,再把它傳遞回父進程,那么父進程可以預先調用socketpair創建一個可用于在父子進程之間交換描述字的流管道。
如果進程之間沒有親緣關系,那么服務器進程必須創建一個UNIX域字節流套接口,bind一個路徑到該套接口,以允許客戶進程connect到該套接口。客戶然后可以向服務器發送一個打開某個描述字的請求,服務器再把該描述字通過UNIX域套接口傳遞回客戶。客戶和服務器之間也可以使用UNIX域數據報套接口,不過這么做缺乏優勢,而且數據報存在被丟棄的可能性。
(2)發送進程通過調用返回描述字的任一UNIX函數打開一個描述字,這些函數的例子有:open、pipe、mkfifo、socket和accept。可以在進程之間傳遞的描述字不限類型,這就是我們稱這種技術為“描述字傳遞”而不是“文件描述字傳遞”的原因。
(3)發送進程創建一個msghdr結構(http://www.CUOXin.com/nufangrensheng/p/3567376.html),其中含有待傳遞的描述字。POSIX規定描述字作為輔助數據(msghdr結構的msg_control成員)發送。發送進程調用sendmsg跨來自步驟(1)的UNIX域套接口發送該描述字。至此我們說這個描述字“在飛行中(in flight)”。即使發送進程在調用sendmsg之后但在接收進程調用recvmsg之前就關閉了該描述字,對于接收進程它仍然保持打開狀態。發送一個描述字導致該描述字的引用計數加1.
(4)接收進程調用recvmsg在來自步驟(1)的UNIX域套接口上接收這個描述字。這個描述字在接收進程中的描述字號不同于它在發送進程中的描述子號是正常的。傳遞一個描述字并不是傳遞一個描述字號,而是涉及在接收進程中創建一個新的描述字,而這個描述字指引的內核中文件表項和發送進程中飛行前的那個描述字指引的相同。
客戶和服務器之間必須存在某種應用協議,以便描述字的接收進程預先知道何時期待接收。另外,在期待接收描述字的recvmsg調用中應該避免使用MSG_PEEK標志,否則后果不可預料。
新聞熱點
疑難解答