基于流的管道(STREAMS-Based Pipes)
所謂基于流的管道實際上就是一種全雙工管道,它必須在基于流的系統上才能實作,linux 預設對它是不支援的,而同樣的邏輯,我們通常可以用基于 UNIX domain 的 socket 來實作,是以這裡對它隻作簡單介紹。
關于流機制,我在 Unix環境進階程式設計學習筆記(九) 進階IO中曾經介紹過,知道可以在流首處加入處理子產品,對于基于流的管道而言,管道的兩端都是流首,是以可以在兩端都加入處理子產品,但同時,我們也隻能在處理子產品加入的一端删除它。
有時候,為了達到在不相關的程序間通信的目的,我們可以将這種管道的一端和某個實際的檔案關聯起來,也就相當于給了它一個名字,使用 fattach 函數:
int fattach(int filedes, const char *path);
調用程序必須擁有該檔案,且對它具有寫權限,或是調用程序具有超級使用者的權限。而一旦流管道的一端和某個檔案關聯上後,該檔案就不能再被通路了,任何對該檔案執行的打開操作,都是獲得管道的通路權,而不再是那個檔案本身。不過,對于管道關聯之前就已經打開了檔案的程序,仍然可以繼續通路該檔案,而不必受管道的影響。
使用 fdetach 函數可以解除關聯:
int fdetach(const char *path);
在該函數被調用以後,已經獲得管道通路權的使用者将繼續擁有該管道的通路權,而之後打開該檔案的程序獲得的是對檔案本身的通路權。
UNIX 域的 SOCKET
在 Unix環境進階程式設計學習筆記(十一) 網絡IPC:套接字中,我介紹了 socket 使用,關于 domain,一共可以有4種情況,今天,我們來看一下用于同一機器下不同程序間通信的 UNIX 域。UNIX 域同時支付流和資料報接口,不同于英特網域,這兩種接口都是可靠的。
我們可以像使用普通的 socket 那樣來使用它,在 UNIX 域的限定下,其位址格式由 sockaddr_un 資料結構來呈現。在Linux 2.4.22 以及 Solaris 9 上,sockaddr_un 的定義如下:
struct sockaddr_un {
sa_family_t sun_family;/* AF_UNIX */
char sun_path[108];/* pathname */
};
sun_path 成員指定了一個檔案名,當将一個 UNIX domain socket 和該位址綁定在一起之後,系統将為我們建立一個 S_IFSOCK 類型的該檔案。當該檔案已經存在時,bind 函數将失敗。當我們關閉該 socket 之後,檔案不會自動被删除,需要我們手動 unlink 它。
需要注意以幾點:
1. 在connect調用中指定的路徑名必須是一個目前綁定在某個打開的Unix域套接口上的路徑名。如果對于某個Unix域套接口的connect調用發現這個監聽套接口的隊列已滿,調用就立即傳回一個ECONNREFUSED錯誤。這一點不同于TCP,如果TCP監聽套接口隊列已滿,TCP監聽端就忽略新到達的SYN,而TCP連接配接發送端将數次發送SYN進行重試。
2. 在一個未綁定的Unix域套接口上發送資料報不會自動給這個套接口捆綁一個路徑名,這一點與UDP套接口不同。是以在用戶端,我們也必須顯示地bind一個路徑名到我們的套接口。
來看一個實際的例子。
服務端:
/*
*author: justaipanda
*create time:2012/09/05 21:51:27
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#define FILE_NAME "1.txt"
#define BUF_SIZE 1024
int serv_listen(const char* name) {
int fd;
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return -1;
unlink(name);
struct sockaddr_un un;
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
int len = (int)((void*)&(un.sun_path) - (void*)&un)
+ strlen(un.sun_path);
if (bind(fd, (struct sockaddr*)&un, len) < 0) {
close(fd);
return -2;
}
if (listen(fd, 128) < 0) {
close(fd);
return -3;
}
return fd;
}
int serv_accept(int listenfd) {
int fd;
struct sockaddr_un un;
int len = sizeof(un);
if ((fd = accept(listenfd, (struct sockaddr*)&un, &len)) < 0)
return -1;
((char*)(&un))[len] = '\0';
unlink(un.sun_path);
return fd;
}
int main() {
int fd;
if ((fd = serv_listen(FILE_NAME)) < 0) {
printf("listen error!\n");
exit(fd);
}
while(1) {
int sclient = serv_accept(fd);
if (sclient < 0) {
printf("accept error!\n");
exit(sclient);
}
char buffer[BUF_SIZE];
ssize_t len = recv(sclient, buffer, BUF_SIZE, 0);
if (len < 0) {
printf("recieve error!\n");
close(sclient);
continue;
}
buffer[len] = '\0';
printf("receive[%d]:%s\n", (int)len, buffer);
if (len > 0) {
if('q' == buffer[0]) {
printf("server over!\n");
exit(0);
}
char* buffer2 = "I'm a server!";
len = send(sclient, buffer2, strlen(buffer2), 0);
if (len < 0)
printf("send error!\n");
}
close(sclient);
}
return 0;
}
用戶端:
/*
*author: justaipanda
*create time:2012/09/06 10:36:43
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#define FILE_NAME "1.txt"
#define CLI_PATH "/var/tmp/"
#define BUF_SIZE 1024
int cli_conn(const char* name) {
int fd;
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return -1;
struct sockaddr_un un;
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid());
unlink(un.sun_path);
int len = (int)((void*)&(un.sun_path) - (void*)&un)
+ strlen(un.sun_path);
if (bind(fd, (struct sockaddr*)&un, len) < 0) {
close(fd);
return -2;
}
if(chmod(un.sun_path, S_IRWXU) < 0) {
close(fd);
return -3;
}
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
len = (int)((void*)&(un.sun_path) - (void*)&un)
+ strlen(un.sun_path);
if (connect(fd, (struct sockaddr*)&un, len) < 0) {
close(fd);
return -4;
}
return fd;
}
int main() {
int fd;
if ((fd = cli_conn(FILE_NAME)) < 0) {
printf("connect error!\n");
exit(fd);
}
char buffer[BUF_SIZE];
printf("input:");
scanf("%s", buffer);
int len = send(fd, buffer, strlen(buffer), 0);
if (len < 0) {
printf("send error!\n");
exit(len);
}
len = recv(fd, buffer, BUF_SIZE, 0);
if (len < 0) {
printf("recieve error!\n");
exit(len);
}
buffer[len] = '\0';
printf("receive[%d]:%s\n", (int)len, buffer);
return 0;
}
對于關聯的程序,我們也可以使用 socketpair 函數簡化工作:
int socketpair(int domain, int type, int protocol, int sockfd[2]);
這個函數的功能實際上就相當于建立了一個全雙工的管道,其中,domain 隻能被指定為 AF_UNIX。
傳送檔案描述符
許多時候,如果可以在程序間傳遞檔案描述符,這将極大的簡化我們程式的設計。
所謂傳送檔案描述符實際上是指——讓接收程序打開一個檔案描述符,該描述符的值不一定和發送程序發送的檔案描述符相同,但它們都指向同一檔案表。
如果發送程序關閉了檔案描述符,這并不會真的關閉檔案或裝置,因為系統認為,接收程序仍然打開着檔案,即使此時,接收程序可能還未收到該檔案描述符。
傳送檔案描述符一般可以有兩種方式:基于流的管道和 UNIX domain socket,對于前者,這裡不多下文筆,主要講後者。利用 UNIX domain socket 傳遞檔案描述符需要使用前面講過的 sendmsg 和 recvmsg 函數(參見Unix環境進階程式設計學習筆記(十一) 網絡IPC:套接字)。利用 msghdr 結構體中的 msg_constrol 成員傳遞描述符。該成員指向 cmsghdr 結構:
struct cmsghdr {
socklen_t cmsg_len;/* data byte count, including header */
int cmsg_level; /* originating protocol */
int cmsg_type;/* protocol-specific type */
/* followed by the actual control message data */
};
為了發送檔案描述符,将 cmsg_len 設定為 cmsghdr 結構的長度再加上一個整型(描述符)的長度,cmsg_level 字段設定為 SOL_SOCKET,cmsg_type 字段設定為 SCM_RIGHTS,用以指明我們在傳送通路權限。
對于這個結構體的操作宏:
unsigned char *CMSG_DATA(struct cmsghdr *cp);
//Returns: pointer to data associated with cmsghdr structure
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mp);
//Returns: pointer to first cmsghdr structure associated with the msghdr structure, or NULL if none exists
struct cmsghdr *CMSG_NXTHDR(struct msghdr *mp, struct cmsghdr *cp);
//Returns: pointer to next cmsghdr structure associated with the msghdr structure given the current cmsghdr structure, or NULL if we're at the last one
unsigned int CMSG_LEN(unsigned int nbytes);
//Returns: size to allocate for data object nbytes large
下面看一個使用 UNIX domain socket 傳遞檔案描述符的例子,該例子是由前面的例子修改而來,其中 serv_listen,serv_accept,以及 cli_conn 函數和原來一樣,就不再重複了。
服務端:
int send_fd(int fd, int fd_to_send) {
struct msghdr msg;
msg.msg_name = NULL;
msg.msg_namelen = 0;
struct cmsghdr *cmptr = NULL;
char buffer[BUF_SIZE];
struct iovec iov;
if (fd_to_send >= 0) {
int cmsg_len = CMSG_LEN(sizeof(int));
cmptr = malloc(cmsg_len);
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
cmptr->cmsg_len = cmsg_len;
*(int*)CMSG_DATA(cmptr) = fd_to_send;
msg.msg_control = cmptr;
msg.msg_controllen = cmsg_len;
sprintf(buffer, "OK!");
}
else {
if (-1 == fd_to_send)
sprintf(buffer, "cannot open file!");
else
sprintf(buffer, "wrong command!");
msg.msg_control = NULL;
msg.msg_controllen = 0;
}
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
iov.iov_base = buffer;
iov.iov_len = strlen(buffer);
sendmsg(fd, &msg, 0);
if (cmptr)
free(cmptr);
return 0;
}
int main() {
int fd;
if ((fd = serv_listen(FILE_NAME)) < 0) {
printf("listen error!\n");
exit(fd);
}
while(1) {
int sclient = serv_accept(fd);
if (sclient < 0) {
printf("accept error!\n");
exit(sclient);
}
char buffer[BUF_SIZE];
ssize_t len = recv(sclient, buffer, BUF_SIZE, 0);
if (len < 0) {
printf("recieve error!\n");
close(sclient);
continue;
}
buffer[len] = '\0';
printf("receive[%d]:%s\n", (int)len, buffer);
if (len > 0) {
if('q' == buffer[0]) {
printf("server over!\n");
exit(0);
}
else if ('f' == buffer[0]) {
int new_fd = open(buffer + 2, O_RDWR);
send_fd(sclient, new_fd);
}
else
send_fd(sclient, -2);
}
close(sclient);
}
return 0;
}
用戶端:
int recv_fd(int fd, char *buffer, size_t size) {
struct cmsghdr *cmptr;
int cmsg_len = CMSG_LEN(sizeof(int));
cmptr = malloc(cmsg_len);
struct iovec iov;
iov.iov_base = buffer;
iov.iov_len = size;
struct msghdr msg;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cmptr;
msg.msg_controllen = cmsg_len;
int len = recvmsg(fd, &msg, 0);
if (len < 0) {
printf("receve message error!\n");
exit(0);
}
else if (len == 0) {
printf("connection closed by server!\n");
exit(0);
}
buffer[len] = '\0';
int cfd = -1;
if (cmptr->cmsg_type != 0)
cfd = *(int*)CMSG_DATA(cmptr);
free(cmptr);
return cfd;
}
int main() {
int fd;
if ((fd = cli_conn(FILE_NAME)) < 0) {
printf("connect error!\n");
exit(fd);
}
char buffer[BUF_SIZE];
printf("input:");
fgets(buffer, BUF_SIZE, stdin);
buffer[strlen(buffer) - 1] = '\0';
int len = send(fd, buffer, strlen(buffer), 0);
if (len < 0) {
printf("send error!\n");
exit(len);
}
int cfd = recv_fd(fd, buffer, BUF_SIZE);
printf("data:%s\n", buffer);
if (cfd >= 0) {
printf("received open file:%d\n", cfd);
}
return 0;
}