第十五章UNIX域協議
15.1概述
UNIX域協議並不是一個實際的協議族,而是在單個主機上執行客戶和服務通訊的一種方案。unix域協議可以被視爲IPC通信方式之一。
UNIX域提供兩類套接字:字節流套接字和數據報套接字。
1)unix域套接字往往比通信兩端位於同一個主機的TCP套接字快出一倍。如果服務器於客戶機處於同一個主機,客戶就打開服務器的unix域字節流連接,否則打開一個服務器的TCP連接。
2)unix域套接字可以在同一個主機不同進程之間傳遞描述符
3)unix域套接字較新實現把客戶的憑證提供給服務器,從而提供額外的安全檢測措施。(用戶ID和組ID)
15.1UNIX域套接字地址結構
struct sockaddr_un{
sa_family_t sun_family;
char sun_path[104];
}
sunpath數組中的路徑名必須以空字符結尾。實現提供SUNLEN宏以一個指向sockaddrun結構的指針爲參數並返回該結構的長度,其中包含路徑名中非空的字節數。未指定地址通過以空字符串作爲路徑名只是,也就是一個sunpath[0]值爲0的地址結構。
unix 域套接字的bind 調用
#include "unp.h"
#include "common.h"
int main(int argc, char** argv)
{
int sockfd;
int len;
sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);
struct sockaddr_un serveraddr,cliaddr;
serveraddr.sun_family = AF_LOCAL;
strncpy(serveraddr.sun_path,argv[1],sizeof(serveraddr.sun_path)-1);
unlink(argv[1]);
bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
len = sizeof(cliaddr);
getsockname(sockfd,(struct sockaddr *)&cliaddr,&len);
printf("biund name = %s,returned len = %d\n", cliaddr.sun_path, len);
exit(0);
}
1.我們需要刪除路徑文件,如果文件存在那麼會刪除文件,如果文件存在的話綁定unix域套接字會失敗,如果不存在unlink會出現一個令我們忽略的錯誤。
2.bind 然後getsockname
我們使用strncpy複製命令行參數,以避免路徑名過長導致地址溢出。既然我們已經把地址結構初始化爲0,並且從sun_path中減去1,可以肯定該路徑名以空字符串結尾。之後調用bind 再使用getsockname取得綁定的路徑名字並且顯示結果。
php的實現:
<?php
$dir ='/ourc/fpm2';
$socket = socket_create(AF_UNIX,SOCK_STREAM,0);
unlink($dir);
socket_bind($socket,$dir);
$addr = [];
socket_getsockname($socket,$addr);
var_dump($addr);
15.3 socketpair 函數
socketpair函數創建兩個隨後連接起來的套接字。本函數僅僅適應於unix域套接字。
#include <sys/socket.h>
int socketpair(int family,int type,int protocol,int sockfd[2]);
family參數必須爲AFLOCAL,protocol參數必須爲0.type參數既可以是SOCKSTREAM。要創建的兩個套接字描述符作爲sockfd[0]和sockfd[1]返回。
類似管道的例子:
/*
*進程雙向通信
*/
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
int main()
{
int sv[2]; //一對無名的套接字描述符
if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sv) < 0) //成功返回零 失敗返回-1
{
perror("socketpair");
return 0;
}
pid_t id = fork(); //fork出子進程
if (id == 0) //孩子
{
//close(sv[0]); //在子進程中關閉讀
close(sv[1]); //在子進程中關閉讀
const char* msg = "i am children\n";
char buf[1024];
while (1)
{
// write(sv[1],msg,strlen(msg));
write(sv[0], msg, strlen(msg));
sleep(1);
//ssize_t _s = read(sv[1],buf,sizeof(buf)-1);
ssize_t _s = read(sv[0], buf, sizeof(buf) - 1);
if (_s > 0)
{
buf[_s] = '\0';
printf("children say : %s\n", buf);
}
}
}
else //父親
{
//close(sv[1]);//關閉寫端口
close(sv[0]);//關閉寫端口
const char* msg = "i am father\n";
char buf[1024];
while (1)
{
//ssize_t _s = read(sv[0],buf,sizeof(buf)-1);
ssize_t _s = read(sv[1], buf, sizeof(buf) - 1);
if (_s > 0)
{
buf[_s] = '\0';
printf("father say : %s\n", buf);
sleep(1);
}
// write(sv[0],msg,strlen(msg));
write(sv[1], msg, strlen(msg));
}
}
return 0;
}
15.4 套接字函數
1)由bind 創建路徑名默認訪問權限應爲0777(屬主用戶、組用戶和其他用戶都可讀可寫並且可執行),並且按照當前umask值進行修改。
2)unix域套接字路徑名應該是一個絕對路徑,而不是一個相對的路徑名。避免使用者後者的原因是它依賴於調用者當前的工作目錄。也就是說服務器捆綁一個相對路徑名字,客戶就得在與服務器相同的目錄中才能成功調用connect 和 sendto。
3)在connect調用中指定路徑名必須是一個當前綁定在某個打開的
5)Unix域字節流套接字與tcp一樣都有一個無邊界 的字節流接口
6)如果unix某個域套接字隊列已滿會返回ECONNREFUSED錯誤,不會像TCP一樣忽略掉信號
7)unix域數據報類似與UDP套接字,提供一個不可靠的數據報服務。
8)在一個未綁定的unix域套接字上發送數據報不會自動給這個套接字綁定一個路徑名,這一點不等同於UDP套接字:在一個未綁定的UDP套接字上發送UDP數據報導致這個套接字綁定一個臨時端口。這就意味着除非數據報發送端已經綁定了一個路徑名到他的套接字,否則接收端無法迴應答數據報。類似的某個unix域套接字的connect調用不會給本套接字綁定一個路徑名,這一點不同於TCP和UDP。
15.5 unix域字節流客戶服務程序
與tcp 類似,服務端程序:
// // Created by root on 18-11-10. //
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <stdint.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
#include <zconf.h>
void sigchild(int signo)
{
pid_t pid;
int stat;
while ((pid = waitpid(-1,&stat,WNOHANG)) > 0)
printf("child %d terminated\n",pid);
return;
}
int main()
{
int listenfd;
listenfd = socket(AF_LOCAL,SOCK_STREAM,0);
struct sockaddr_un serveraddr,cliaddr;
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sun_family = AF_LOCAL;
strcpy(serveraddr.sun_path,"/usr/local/soft/default/unix_socket");
unlink("/usr/local/soft/default/unix_socket");
bind(listenfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
listen(listenfd,50);
signal(SIGCHLD,sigchild);
bzero(&cliaddr, sizeof(cliaddr));
pid_t child_pid;
int clien;
int connfd;
for(;;)
{
clien = sizeof(cliaddr);
if((connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clien)) < 0)
{
if(errno == EINTR)
{
continue;
} else{
printf("accept error\n");
exit(0);
}
}else{
if((child_pid = fork()) == 0)
{
close(listenfd);
char buf[100];
if(read(connfd,buf,sizeof(buf)) > 0)
{
printf("%s\n",buf);
exit(-1);
}
}
}
close(connfd);
}
}
客戶端程序:
//
// Created by root on 18-12-22.
//
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <stdint.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
#include <zconf.h>
int main(int argc,char** argv)
{
int sockfd;
struct sockaddr_un servaddr;
sockfd = socket(AF_LOCAL,SOCK_STREAM,0);
bzero(&servaddr, sizeof(servaddr));
strcpy(servaddr.sun_path,"/usr/local/soft/default/unix_socket");
servaddr.sun_family = AF_LOCAL;
connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
char buf[40];
bzero(&buf,sizeof(buf));
strcpy(buf,"hello world");
write(sockfd,buf,sizeof(buf));
}
15.6 數據報服務客戶程序
與TCp一樣不再寫了,只不過是把SOCKSTREAM變爲SOSCKDRGM
15.7 描述符傳遞
當考慮一個進程的描述符傳遞到另一個進程的時候,我們通常會想到
fork調用返回後,子進程共享父進程所有打開的描述符
exec調用執行之後,所有描述符通常保持打開狀態不變
進程打卡一個描述符,調用fork,然後父進程關閉這個描述符,子進程處理這個描述符。然而有的時候我們想讓子進程把秒殺父母傳遞到父進程
當前的unix系統提供了用於從一個進程向任意一個其他進程傳遞一個打開的描述符的方法,也就是說兩個進程不需要存在親源關係,比如父子進程關係。這種技術首先在兩個進程之間創建一個unix域套接字,然後使用sendmsg跨這個套接字發送一個特殊消息。這個消息由內核來專門處理,會把打開的描述符從發送進程傳遞到接收進程。
兩個進程之間描述符的傳遞步驟如下:
1)創建一個字節流或數據報的套接字
如果目標是父子進程,那麼可以用socketpair創建一個可用於在父子進程之間交換的描述符流管道。
如果進程之間沒有血緣關係,那麼服務進程必須要創建一個域服務器套接字,bind一個路徑名到這個套接字,來允許該進程connect到這個套接字。然後客戶可以向發送一個打開某個描述符的請求,服務器再把這個描述符通過域套接字傳遞迴客戶。客戶和服務之間也可以通過unix域數據報套接字,不過這麼做沒什麼好處。而且數據報還有丟失的可能性。
2)發送進程通過調用函數unix任意函數打開一個描述符。這些函數有open,pipe,mkfifo,socket,accept,可以在進程之間傳遞的秒殺父母不限制類型,這就是我們稱他爲描述符傳遞而不是文件描述符傳遞的原因。
3)發送創建一個msghdr結構體,其中含有描述符。POSIX規定描述符作爲輔助數據(msghdr結構的msgcontrol成員)發送,不過老的實現是通過msgaccrights成員。發送進程通過調用sendmsg跨來自步驟1的unix域套接字發送該描述符。至此我們說我們的描述符在飛行中。即使發送進程在調用sendmsg後接收進程用recvmsg之前關閉了描述符,對於接收進程依然保持打開狀態。發送一個描述符會讓這個描述符的引用計數+1。
4)接收進程在調用recvmsg接收來自步驟1的描述符。這個描述符在接收進程中的秒描述符不同於他在發送中的描述符是正常的。傳遞一個描述符並不是傳遞一個符號,而是涉及到在接收進程中創建一個新的符號,而這個新描述符和發送進程中飛行前的那個描述符指向內核中相同的文件表項。
客戶和服務器之間必須存在某種協議,以便描述符在接收進程中預先知道何時接收。如果接收進程在調用recvmsg沒有用於分配接收描述符的空間,而且之前一個描述符已經被傳遞而且被等待 讀取,這個早先傳遞的描述符就會被關閉。另外在接收描述符的recvmsg中,應該避免使用MSG_PEEK,否則後果不可預料。
描述符傳遞的小例子
#include "unp.h"
int my_open(const char*,int);
int main(int argc,char** argv)
{
int fd,n;
char buff[BUFSIZE];
if(argc != 2)
err_quit("usage:mycat <pathname>");
if((fd = my_open(argv[1],O_RDONLY)) < 0)
err_sys("cannot open %s",argv[1]);
while((n = read(fd,buff,BUFFSIZE)))
write(STDOUT_FILENO,buff,n);
exit(0);
}
int my_open(const char *pathname,int mode)
{
int fd,sockfd[2],status;
pid_t childpid;
char c,argsockfd[10],argmode[10];
socketpair(AF_LOCAL,SOCK_STREAM,0,sockfd);
if((childpid = fork()) == 0){
close(sockfd[0]);
snprintf(argsockfd,sizeof(argsockfd),"%d",sockfd[1]);
snprintf(argmode,sizeof(argmode),"%d",sockfd[1]);
execl("./openfile","openfile",argsockfd,pathname,argmode,(char *)NULL);
err_sys("execl error");
}
close(sockfd[1]);
waitpid(childpid,&status,0);
if(WIFEXITED(status) == 0)
err_quit("child did not terminate");
if((status = WEXITSTATUS(status)) == 0)
read_fd(sockfd[0],&c,1,&fd);
else{
errno = status;
fd = -1;
}
close(soclfd[0]);
return fd;
}
1)首先我們使用socketpair創建了一個流管道,返回了兩個描述符:sockfd[0]和sockfd[1].
2)調用fork,子進程然後關閉流管道的一端。流管道的另一端的描述符格式化輸出到argsockfd字符數組,打開方式格式化輸出到argmode字符數組,這裏使用snprintf格式化進行輸出是因爲exec的參數必須是字符串。子進程隨後調用execl執行openfile程序。這個函數不會返回,除非他出現錯誤。一旦成功,openfile程序打開所請求文件時碰到一個錯誤,它將以相應的errno值作爲退出狀態終止自身。
3)父進程等待子進程 父進程關閉流管道的另一端並且使用waitpid等待子進程終止(防止出現殭屍進程)。子進程的終止狀態再status之中返回,我們首先應該檢查是否是正常終止(也就是說不是被某一個信號終止),如果是正常終止接着調用WEXITSTATUS宏把終止狀態切換成退出狀態,退出狀態的值再0~255之間。我們馬上會看到在打開請求文件的時候會碰到一個錯誤,他將會以對應的errno值作爲退出狀態來終止自身
readfd函數的使用:
zsize_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;
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.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 != SOL_SOCKET)
err_quit("control level != SOL_SOCKET");
if(cmptr->cmsg_type != SCM_RIGHTRS)
err_quit("control level != SCM_RIGHTRS");
*recvfd = *((int *)CMSG_DATA(cmptr));
}else{
*recvfd = -1
}
#else
if(msg.msg_accrightslen == sizeof(int))
*recvfd = newfd;
else
*recvfd = -1;
#endif
return n;
}
openfile程序
#include "unp.h"
int main(int argc,char** argv)
{
int fd;
if(argc != 4)
err_quit("openfile <sockfd*> <filename> <mode>");
if((fd = open(argv[2],atoi(argv[3]))) < 0)
exit((errno > 0) ? errno : 255);
if(write_fd(atoi(argv[1]),"",1,fd) < 0)
exit((errno > 0) ? errno : 255);
exit(0);
}
writefd函數
#include "common.h"
#define HAVE_MSGHDR_MSG_CONTROL
ssize_t writefd(int fd, void* ptr, size_t nbytes, int sendfd)
{
struct msghdr msg;
struct iovec iov[1];
iov[0].iov_base = ptr;
iov[0].iov_len = nbytes;
msg.msg_name = NULL;
msg.msg_namelen = 0;
#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);
//CMSG_FIRSTHDR的實現有許多種,返回cmsg_control的地址)
cmptr = CMSG_FIRSTHDR(&msg);
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
*((int *)CMSG_DATA(cmptr)) = sendfd;
#else
msg.msg_accrights = (caddr_t)&sendfd;
msg.msg_accrightslen = sizeof(int);
#endif
return sendmsg(fd,&msg,0);
}
write_fd函數把描述符傳遞迴父進程之後,本進程立即終止。本章早先說過,發送進程可以不等落地就關閉已經傳遞的描述符,因爲內核指導描述符在飛行仲,從而爲接收進程保持打開狀態。
自己寫的實驗例子:
msg_main.c
#include "common.h"
#define BUFSIZE 255
int my_open(const char* pathname, int mode);
ssize_t readfd(int fd, void* ptr, size_t nbytes, int *recvfd);
int main(int argc, char** argv)
{
int fd, n;
char buf[BUFSIZE];
printf("%s\n", argv[1]);
if (argc != 2)
{
sys_err("usage mycat <pathname>");
}
if ((fd = my_open(argv[1], O_RDONLY)) < 0)
{
printf("cannot 1111 %s\n", argv[1]);;
printf("cannot open %s\n", argv[1]);;
exit(-1);
}
while ((n = read(fd, buf, BUFSIZE)) > 0)
write(STDOUT_FILENO,buf,n);
exit(0);
}
ssize_t readfd(int fd,void* ptr,size_t nbytes,int *recvfd)
{
struct msghdr msg;
struct iovec iov[1];
int n;
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;
struct cmsghdr* imptr;
union {
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
}control_un;
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
if ((n = recvmsg(fd, &msg, 0)) <= 0)
{
printf("recvmsg main fd:%d\n", fd);
return n;
}
imptr = CMSG_FIRSTHDR(&msg);
if ((imptr != NULL) && (imptr->cmsg_len == CMSG_LEN(sizeof(int))))
{
if (imptr->cmsg_level != SOL_SOCKET)
{
printf("control level != SOL_SOCKET");
exit(-1);
}
if (imptr->cmsg_type != SCM_RIGHTS)
{
printf("control type != SCM_RIGHTRS");
exit(-1);
}
*recvfd = *((int *)CMSG_DATA(imptr));
}
else {
*recvfd = -1;
}
return n;
}
int my_open(const char* pathname,int mode)
{
int fd, sockfd[2], status;
pid_t childpid;
char c, argsockfd[10], argmode[10];
socketpair(AF_LOCAL,SOCK_STREAM,0,sockfd);
if ((childpid = fork()) == 0)
{
close(sockfd[0]);
//將數字全部格式化爲字符串
snprintf(argsockfd,sizeof(argsockfd),"%d",sockfd[1]);
snprintf(argmode, sizeof(argmode), "%d", mode);
printf("argsockfd n:%s\n", argsockfd);
printf("argmode n:%s\n", argmode);
printf("argmode n:%s\n", pathname);
execl("./openfile", "openfile", argsockfd, pathname, argmode, (char*)NULL);
sys_err("execl error");
}
close(sockfd[1]);
waitpid(childpid,&status,0);
printf("status n:%d\n", status);
if (WIFEXITED(status) == 0)
sys_err("child did not terminate");
if ((status = WEXITSTATUS(status)) == 0)
{
readfd(sockfd[0],&c,1,&fd);
}
else {
errno = status;
fd = -1;
}
close(sockfd[0]);
return fd;
}
msg.c:
#include "common.h"
#define HAVE_MSGHDR_MSG_CONTROL
ssize_t writefd(int fd, void* ptr, size_t nbytes, int sendfd);
int main(int argc,char** argv)
{
int fd;
if (argc != 4)
{
printf("openfile <sockfd> <filename> <mode>");
exit(-1);
}
if ((fd = open(argv[2], atoi(argv[3]))) < 0)
{
exit((errno > 0) ? errno :errno);
}
if (writefd(atoi(argv[1]), "", 1, fd) < 0)
{
printf("errno:%d\n",errno);
exit((errno > 0) ? errno : 255);
}
printf("55555555\n");
exit(0);
}
ssize_t writefd(int fd, void* ptr, size_t nbytes, int sendfd)
{
struct msghdr msg;
struct iovec iov[1];
iov[0].iov_base = ptr;
iov[0].iov_len = nbytes;
msg.msg_name = NULL;
msg.msg_namelen = 0;
#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);
//CMSG_FIRSTHDR的實現有許多種,返回cmsg_control的地址)
cmptr = CMSG_FIRSTHDR(&msg);
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
*((int *)CMSG_DATA(cmptr)) = sendfd;
#else
msg.msg_accrights = (caddr_t)&sendfd;
msg.msg_accrightslen = sizeof(int);
#endif
int result = sendmsg(fd,&msg,0);
printf("result:%d\n", result);
return result;
}
運行:
[root@localhost ourc]# ./msg_main /ourc/a.php
/ourc/a.php
argsockfd n:4
argmode n:0
argmode n:/ourc/a.php
result:0
55555555
status n:0
recvmsg main fd:3
php 字節流 域套接字 跨進程傳遞描述符代碼案例
服務端:
<?php
ini_set("display_errors",true);
//
$socket = socket_create(AF_UNIX,SOCK_STREAM,0);
$dir = "/root/fpm";
unlink($dir);
socket_bind($socket,$dir);
socket_listen($socket,100);
while(1)
{
$connfd = socket_accept($socket);
var_dump($connfd);
$data = ["controllen" => socket_cmsg_space(SOL_SOCKET, SCM_RIGHTS, 3)];
$msg = socket_recvmsg($connfd,$data,0);
var_dump($data);
}
客戶端:
<?php
ini_set("display_errors", true);
$socket = socket_create(AF_UNIX, SOCK_STREAM, 0);
$dir = "/root/fpm";
$result = socket_connect($socket, $dir);
var_dump($result);
$fp = fopen("/root/common.h", "r");
var_dump($fp);
$r = socket_sendmsg($socket, [
"iov" => [" "],
"control" =>
[
[
"level" => SOL_SOCKET,
"type" => SCM_RIGHTS,
"data" => [$fp]
]
]
]
, 0);
php 數據報流 域套接字 跨進程傳遞描述符代碼案例
<?php
$s = socket_create(AF_UNIX, SOCK_DGRAM, 0) or die("err");
unlink($dir);
$br = socket_bind($s, $dir) or die("err");
$dir = "/root/fpm";
while(1)
{
$data = ["name" => [], "buffer_size" => 2000, "controllen" => socket_cmsg_space(SOL_SOCKET, SCM_RIGHTS, 3)];
$result = socket_recvmsg($s, $data, 0);
if($result)
{
var_dump($data);
}
}
客戶端: <?php iniset("displayerrors", true); $socket = socketcreate(AFUNIX, SOCKSTREAM, 0); $dir = "/root/fpm"; $result = socketconnect($socket, $dir);
var_dump($result);
$fp = fopen("/root/common.h", "r");
var_dump($fp);
$r = socket_sendmsg($socket, [
"iov" => [" "],
"control" =>
[
[
"level" => SOL_SOCKET,
"type" => SCM_RIGHTS,
"data" => [$fp]
]
]
]
, 0);