17.1 Introduction

这一章主要讲了UNIX Domain Sockets这样的进程间通讯方式,并列举了具体的几个例子。

17.2 UNIX Domain Sockets

这是一种特殊socket类型,主要用于高效的IPC,特点主要在于高效(因为省去了很多与数据无关的格式的要求)。

int socketpair(int domain, int type, int protocol, int sockfd[2]) 这个函数用于构建一对unix domain sockets;并且与之前的pipe函数不同,这里构建fd都是full-duplex的。

下面列举一个poll + message queue + 多线程 的例子。

为什么要举上面的例子?因为没办法直接用poll去管理多个message queue。

message queue在unix系统中有两种标示方法:1. 全局用一个key 2. 进程内部用一个identifier

而poll关注的对象只能是file descriptor;所以,用unix domain sockets作为二者的桥梁。

例子包含两个部分,reciver端和sender端。

reciver挂起来几个message queue,每个queue单独开一个线程去处理;主线程跟每个queue线程的关联方式就是unix domain sockets。代码如下:

 #include "../apue.3e/include/apue.h"
#include <sys/poll.h>
#include <pthread.h>
#include <sys/msg.h>
#include <sys/socket.h> #define NQ 3
#define MAXMSZ 512
#define KEY 0x123 struct threadinfo{
int qid;
int fd;
}; struct mymesg{
long mtype;
char mtext[MAXMSZ];
}; void * helper(void *arg)
{
int n;
struct mymesg m;
struct threadinfo *tip = arg; for(; ;)
{
memset(&m, , sizeof(m));
if ((n = msgrcv(tip->qid, &m, MAXMSZ, , MSG_NOERROR))<) {
err_sys("msgrcv error");
}
/*来自一个消息队列的内容 就特定的file desrciptor中*/
if (write(tip->fd, m.mtext, n)<) {
err_sys("write error");
}
}
} int main(int argc, char *argv[])
{
int i, n, err;
int fd[];
int qid[NQ]; /*message queue在process内部的identifier*/
struct pollfd pfd[NQ];
struct threadinfo ti[NQ];
pthread_t tid[NQ];
char buf[MAXMSZ]; /*给每个消息队列设定处理线程*/
for (i=; i<NQ; i++) {
/*返回消息队列的identifier 类似file descriptor*/
if ((qid[i] = msgget((KEY+i), IPC_CREAT|))<) {
err_sys("msgget error");
}
printf("queue ID %d is %d\n", i, qid[i]);
/*构建unix domain sockets*/
if (socketpair(AF_UNIX, SOCK_DGRAM, , fd)<) {
err_sys("socketpair error");
}
pfd[i].fd = fd[]; /*main线程把住fd[0]这头*/
pfd[i].events = POLLIN; /*有data要去读*/
/* qid[i]在同一个process都可以用来表示同一个message queue */
ti[i].qid = qid[i]; /*在每个线程中记录要处理的消息队列的id*/
ti[i].fd = fd[]; /*每个队列的线程把住fd[1]这头*/
/*为每个消息队列创建一个处理线程 并将对应的threadinfo参数传入线程*/
if ((err = pthread_create(&tid[i], NULL, helper, &ti[i]))!=) {
err_exit(err, "pthread_create error");
}
} for (;;) {
/*一直轮询着 直到有队列可以等待了 再执行*/
if (poll(pfd, NQ, -)<) {
err_sys("poll error");
}
/*由于能进行到这里 则一定是有队列ready了 找到所有ready的队列*/
for (i=; i<NQ; i++) {
if (pfd[i].revents & POLLIN) { /*挑出来所有满足POLLIN条件的*/
if ((n=read(pfd[i].fd, buf, sizeof(buf)))<) {
err_sys("read error");
}
buf[n] = ; /* 这个末尾赋'\0'是必要的 因为接下来要执行printf*/
printf("queue id %d, message %s\n",qid[i],buf);
}
}
}
exit();
}

sender端,用command-line argument的方式读入message的外部key,以及写入message queue的数据,具体代码如下:

#include "../apue.3e/include/apue.h"
#include <sys/msg.h> #define MAXMSZ 512 struct mymesg{
long mtype;
char mtext[MAXMSZ];
}; int main(int argc, char *argv[])
{
key_t key;
long qid;
size_t nbytes;
struct mymesg m;
if (argc != ) {
fprintf(stderr, "usage: sendmsg KEY message\n");
exit();
}
key = strtol(argv[], NULL, );
if ((qid = msgget(key,))<) {
err_sys("can't open queue key %s", argv[]);
}
memset(&m, , sizeof(m));
strncpy(m.mtext, argv[], MAXMSZ-);
nbytes = strlen(m.mtext);
m.mtype = ;
if (msgsnd(qid, &m, nbytes, )<) {
err_sys("can't send message");
}
exit();
}

执行结果如下:

分析:

(1)unix socket domain在上述代码中的好处主要是方便了多个message queue的管理

(2)引入unix socket domain虽然带来了方便,但也在reciver中引入了两次额外的cost:一个是line34的write,向unix domain socket多写了一次;一个是line80的read,从unix domain socket多读了一次。如果这种cost在可接受范围内,那么unix socket domain就可以应用。

17.2.1 Naming UNIX Domain Sockets

上面介绍的这种socketpair的方式构造unix domain sockets,输出是几个fd,因此只能用于有亲属关系的process中。

如果要unrelated process之间用unix domain sockets通信,得从外面process能找到这个unix domain socket。

struct sockaddr_un{

  sa_family_t sun_family;  /*AF_UNIX*/

  char sun_path[108];  /*pathname*/

}

这个结构体可以用来被构造成一个“可以被外面process找到的”的unix domain socket的地址,类似于“ip+port”的作用

具体需要如下三个步骤的操作:

1)fd = socket(AF_UNIX, SOCK_STREAM, 0) // 产生unix domain socket

2)un.sun_family = AF_UNIX strcpy(un.sun_path, pathname)

3)bind(fd, (struct sockaddr *)&un, size)  // 将unix domain socket与fd绑定

另,这里的pathname需要是一个独一无二的文件名。后面的一系列内容,都把sockaddr_un按照ip+port进行理解就顺畅了

有了结构体中sun_path这个文件名,这个unix domain socket就有了自己独一无二的标识,其他进程就可以通过这个标识找到它。

 #include "../apue.3e/include/apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h> int main(int argc, char *argv[])
{
int fd, size;
struct sockaddr_un un; un.sun_family = AF_UNIX;
memset(un.sun_path, , sizeof(un.sun_path));
strcpy(un.sun_path, "foo.socket"); if ((fd = socket(AF_UNIX, SOCK_STREAM, ))<) {
err_sys("socket fail");
}
size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
if (bind(fd, (struct sockaddr *)&un, size)<) {
err_sys("bind failed");
}
printf("UNIX domain socket bound\n");
exit();
}

这里“foo.socket"不需要事先真的存在,它只需要是一个独特的名称就可以了。

执行结果如下:

程序执行的当前文件夹下是没有foo.socket这个文件的

执行如上程序:

可以看到执行完程序后:

(1)foo.socket这个文件自动生成了,而且文件类型是socket(srwxrwxr-x中的s)

(2)如果foo.socket已经被占用了是没办法再绑定其他的unix domain socket的

17.3 Unique Connections

基于17.2.1的naming unix domain socket技术,就可以针对unix domain socket展开listen, accept, connect等一些列用于network socket的操作;用这样的方式来实现同一个host内部的IPC。

具体的示意图,如下所示:

apue中分别给出了listen accept connect三个函数的unix domain socket版。

int serv_listen(const char *name);

int serv_accpet(int listenfd, uid_t *uidptr);

int cli_conn(const char *name);

具体实现如下:

serv_listen函数(返回一个unix domain socket专门用于监听client发送来的请求

 #include "../apue.3e/include/apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h> #define QLEN 10 /*只要传入一个well known name 就可返回fd*/
int serv_listen(const char *name)
{
int fd;
int len;
int err;
int rval;
struct sockaddr_un un; /*对name的长度上限有要求*/
if (strlen(name) >= sizeof(un.sun_path)) {
errno = ENAMETOOLONG;
return -;
}
/*这里创建的方式是SOCK_STREAM*/
if ((fd = socket(AF_UNIX, SOCK_STREAM, ))<) {
return -;
}
/*防止name已经被占用了 这是一种排他的做法*/
unlink(name);
/*初始化socket address structure*/
memset(&un, , sizeof(un.sun_path));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
/*执行bind操作 因为有name所以可以绑定*/
if (bind(fd, (struct sockaddr *)&un, len)<) {
rval = -;
goto errout;
}
/*执行listen的操作 并设置等待队列的长度*/
if (listen(fd, QLEN)<) {
rval = -;
goto errout;
}
return fd;
errout:
err = errno;
close(fd);
errno = err;
return rval;
}

serv_accpet函数(这里有一点没看懂 为什么client's name有30s的限制

 #include "../apue.3e/include/apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <errno.h> #define STALE 30 /*client's name can't be older than this sec*/ int serv_accept(int listenfd, uid_t *uidptr)
{
int clifd;
int err;
int rval;
socklen_t len;
time_t staletime;
struct sockaddr_un un;
struct stat statbuf;
char *name; /*name中存放的是发起请求的client的地址信息*/ /*因为sizeof不计算结尾的\0 所以在计算分配内存的时候要考虑进来*/
if ((name = malloc(sizeof(un.sun_path+)))==NULL) {
return -;
}
len = sizeof(un);
/*就在这里阻塞着 等着client端发送来请求*/
if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len))<) {
free(name);
return -;
}
/*再让len为path的实际长度 并存到name中*/
len -= offsetof(struct sockaddr_un, sun_path);
memcpy(name, un.sun_path, len);
name[len] = ; /*最后补上\0*/
if (stat(name, &statbuf)<) { /*让statbuf获得client关联的文件的status*/
rval = -;
goto errout;
} /*1. 验证与client端关联的文件类型是不是socket file*/
#ifdef S_ISSOCK
if (S_ISSOCK(statbuf.st_mode)==) {
rval = -;
goto errout;
}
#endif
/*2. 验证与clinet端关联的文件的权限*/
/*G for group O for owner U for user */
/*验证permission只有user-read user-write user-execute*/
/*注意 ||运算符的优先级 要高于 !=运算符的优先级*/
if ((statbuf.st_mode & (S_IRWXG | S_IRWXO)) ||
(statbuf.st_mode & S_IRWXU) != S_IRWXU) {
rval = -;
goto errout;
}
/*3. 验证与client端关联的文件被创建的时间*/
staletime = time(NULL) - STALE; /**/
if (statbuf.st_atim < staletime ||
statbuf.st_ctim < staletime ||
statbuf.st_mtim < staletime) {
rval = -;
goto errout;
}
if (uidptr != NULL) {
*uidptr = statbuf.st_uid;
}
unlink(name);
free(name);
return clifd; errout:
err = errno;
close(clifd);
free(name);
errno = err;
return rval;
}

cli_conn

 #include "../apue.3e/include/apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h> #define CLI_PATH "/var/tmp" /*客户端标示*/
#define CLI_PERM S_IRWXU /*权限设置*/ int cli_conn(const char *name)
{
int fd;
int len;
int err;
int rval;
struct sockaddr_un un, sun;// un代表client端 sun代表server端
int do_unlink = ;
/*1. 验证传入的name是否合理
* 这个name是server的name 先校验server name的长度 */
if (strlen(name) >= sizeof(un.sun_path)) {
errno = ENAMETOOLONG;
return -;
}
/*2. 构建client端的fd
* 这个fd是client的专门发送请求的fd*/
if ((fd = socket(AF_UNIX, SOCK_STREAM, ))<) {
return -;
}
/*3. 构建client端的地址*/
/* 将文件名+进程号共写进un.sun_path 并记录长度 这里约定了path的格式*/
memset(&un, , sizeof(un));
un.sun_family = AF_UNIX;
sprintf(un.sun_path, "%s%05ld", CLI_PATH, (long)getpid());
printf("file is %s\n", un.sun_path);
len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
/*4. 将构建的fd与构建的client端地址绑定*/
unlink(un.sun_path); /*防止CLI_PATH+pid这个特殊的文件名已经被占用了*/
if (bind(fd, (struct sockaddr *)&un, len)<) {
rval = -;
goto errout;
}
/* 为什么要先绑定再设定权限?因为如果不能绑定 修改权限就是无用功*/
if (chmod(un.sun_path, CLI_PERM)<) {
rval = -;
do_unlink = ;
goto errout;
}
/*5. 告诉client通过name去找server*/
/* 通过这个name这个key与'server'的process建立连接*/
memset(&sun, ,sizeof(sun));
sun.sun_family = AF_UNIX;
strcpy(sun.sun_path, name);
len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
if (connect(fd, (struct sockaddr *)&sun, len)<) {
rval = -;
do_unlink = ;
goto errout;
}
return fd;
errout:
err = errno;
close(fd);
if (do_unlink) {
unlink(un.sun_path);
}
errno = err;
return raval;
}

17.4 Passing File Descriptors

在进程间传递file descriptor是也是unix domain socket的一种强大的功能。文件打开的各种细节,都隐藏在server端了。

至今在apue上已经有三种进程间的file descriptor的传递方式:

(1)figure3.8的情况,不同的process分别打开同一个file,每个process中的fd有各自的file table,这两个fd基本没有什么关系:

  

(2)figure8.2的情况,parent通过fork产生child,整个parent的memory layout都copy到child中,这两个fd属于不同的地址空间,但是值是相同的,并且共享同一个file table:

  

(3)17.4节的情况,通过unix domain socket的方式传递fd,这两个fd属于不同的地址空间,除了共享同一个file table没有其他的不同:

  

这一部分还讲了其他一些相关的结构体内容,这些细节为了看懂代码而用,关键记住上面的三种fd方式就可以了。

apue这部分自己设定了一个protocol,设定通过unix domain socket传递fd的协议,这个协议的细节不用关注太多;重点看如何告诉系统,发送的是一个fd。

利用unix domain socket发送和接收fd的代码如下:

send_fd的代码(如何告诉系统发送的是一个fd?先把struct cmsghdr cmptr设定好line43~45,将cmptr赋值给struct msghdr msg中的msg.msg_control,这样系统就知道发送的是一个fd

 #include "../apue.3e/include/apue.h"
#include <bits/socket.h>
#include <sys/socket.h> /* 由于不同系统对于cmsghdr的实现不同 CMSG_LEN这个宏就是计算cmsghdr+int
* 所需要的memory大小是多少 这样动态分配内存的时候才知道分配多少大小*/
#define CONTROLLEN CMSG_LEN(sizeof(int)) static struct cmsghdr *cmptr = NULL; int send_fd(int fd, int fd_to_send)
{
struct iovec iov[];
struct msghdr msg;
char buf[]; /*这是真正的协议头的两个特征bytes*/
/*scatter read or gather write 具体参考14.6
* 具体到这里的情景比较简单 因为iovec的长度只有1 相当于就调用了一个write
* 但是Unix domain socket的格式要去必须是struct iovec这种数据格式*/
iov[].iov_base = buf;
iov[].iov_len = ;
msg.msg_iov = iov;
msg.msg_iovlen = ;
msg.msg_name = NULL;
msg.msg_namelen = ;
/*调用send_fd分两种情况:
* 1. 正常调用传递fd, 则fd_to_send是大于零的
* 2. 在send_err中调用send_fd, 则fd_to_send表示的是errorcode*/
if (fd_to_send<) {
msg.msg_control = NULL;
msg.msg_controllen = ;
buf[] = -fd_to_send; /*出错的fd_to_send都是负数*/
if (buf[] == ) { /*这个protocol并不是完美的 如果fd_to_send
是-256 则没有正数与其对应 协议在这里特殊处理-1与-256都代表 errorcode 1*/
buf[] = ;
}
}
else {
/*这里cmptr获得的memory大小是由CMSG_LEN算出来的*/
if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL ) {
return -;
}
/*通过Unix domain socket发送fd 就如下设置*/
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
cmptr->cmsg_len = CONTROLLEN;
/*将cmptr融进要发送的msg*/
msg.msg_control = cmptr;
msg.msg_controllen = CONTROLLEN;
/*得搞清楚strut cmsghdr的结构
* struct cmsghdr{
* socklen_t cmsg_len;
* int cmsg_level;
* int cmsg_type;
* }
* // followed by the actual control message data
* CMSG_DATA做的事情就是在cmsghdr紧屁股后面放上'fd_to_send'这个内容
* ubuntu系统上查看<sys/socket.h>文件中的这个宏的具体实现
* 这个宏的具体实现就是struct cmsghdr结构体的指针+1, 然后将这个位置*/
*(int *)CMSG_DATA(cmptr) = fd_to_send;
buf[] = ;
}
buf[] = ; /*这就是给recv_fd设定的null byte flag recv_fd()函数中就是靠这个位来判断的*/
/*这里校验的sendmsg返回值是不是2 就是char buf[2]中的内容
* struct msghdr msg中 只有msg_iov中的数据算是被校验的内容
* 而msg_control这样的数据 都叫ancillary data 即辅助数据
* 辅助数据虽然也跟着发送出去了 但是不在sendmsg返回值的校验标准中*/
if (sendmsg(fd, &msg, )!=) {
return -;
}
return
}

接收端的代码recv_fd如下(代码不难理解,有个坑是line56是apue勘误表中才修改过来,否则有问题;勘误表的链接:http://www.apuebook.com/errata3e.html

 #include "open_fd.h"
#include <sys/socket.h> /* struct msghdr */ /* size of control buffer to send/recv one file descriptor */
#define CONTROLLEN CMSG_LEN(sizeof(int)) static struct cmsghdr *cmptr = NULL; /* malloc'ed first time */ /*
* Receive a file descriptor from a server process. Also, any data
* received is passed to (*userfunc)(STDERR_FILENO, buf, nbytes).
* We have a 2-byte protocol for receiving the fd from send_fd().
*/
int
recv_fd(int fd, ssize_t (*userfunc)(int, const void *, size_t))
{
int newfd, nr, status;
char *ptr;
char buf[MAXLINE];
struct iovec iov[];
struct msghdr msg; status = -;
for ( ; ; ) {
iov[].iov_base = buf;
iov[].iov_len = sizeof(buf);
msg.msg_iov = iov;
msg.msg_iovlen = ;
msg.msg_name = NULL;
msg.msg_namelen = ;
if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL)
return(-);
msg.msg_control = cmptr;
msg.msg_controllen = CONTROLLEN;
if ((nr = recvmsg(fd, &msg, )) < ) {
err_ret("recvmsg error");
return(-);
} else if (nr == ) {
err_ret("connection closed by server");
return(-);
} /*
* See if this is the final data with null & status. Null
* is next to last byte of buffer; status byte is last byte.
* Zero status means there is a file descriptor to receive.
*/
for (ptr = buf; ptr < &buf[nr]; ) {
if (*ptr++ == ) {
if (ptr != &buf[nr-])
err_dump("message format error");
status = *ptr & 0xFF; /* prevent sign extension */
if (status == ) {
printf("msg.msg_controllen:%zu\n", msg.msg_controllen);
printf("CONTROLLEN:%zu\n", CONTROLLEN);
if (msg.msg_controllen < CONTROLLEN)
err_dump("status = 0 but no fd");
newfd = *(int *)CMSG_DATA(cmptr);
} else {
newfd = -status;
}
nr -= ;
}
}
if (nr > && (*userfunc)(STDERR_FILENO, buf, nr) != nr)
return(-);
if (status >= ) /* final data has arrived */
return(newfd); /* descriptor, or -status */
}
}

17.5 An Open Server, Version 1

这一节正是利用17.4中的passing file descriptor的技术来构建一个"open" server:

这个server专门用来接收client发送的请求(即打开哪个文件,怎么打开),然后在server端把文件打开,再利用unix domain socket的技术把file descriptor给传递过去。

具体用到的技术就是client运行起来,通过fork+execl的方式调用opend(相当于server端的程序),并且通过socketpair的方式建立进程间的通信。

将书上的代码整理了一下(main.c表示client端,maind.c表示server端,lib文件夹中包含用到的一些函数,include文件夹中的.h文件包括各种公用的lib

main.c代码如下:

 #include "open_fd.h"
#include <fcntl.h>
#include <sys/uio.h> #define BUFFSIZE 8192
#define CL_OPEN "open" // client's request for server int csopen(char *name, int oflag)
{
pid_t pid;
int len;
char buf[];
struct iovec iov[];
static int fd[] = {-, -};
/*首次需要建立child parent的链接*/
if (fd[] < ) {
printf("frist time build up fd_pipe\n");
/*构建一个全双工的pipe*/
if (fd_pipe(fd) < ) {
err_ret("fd_pipe error");
return -;
}
printf("fd[0]:%d,fd[1]:%d\n",fd[],fd[]);
if((pid = fork())<){
err_ret("fork error");
return -;
}
else if (pid ==) { /*child*/
close(fd[]);
/*这个地方需要注意 这种full-duplex的fd 可以把in和out都挂到这个fd上面 之前只挂了stdin没有挂out所以有问题*/
/*将child的stdin 衔接到fd[1]上面*/
if (fd[] != STDIN_FILENO && dup2(fd[],STDIN_FILENO)!=STDIN_FILENO) {
err_sys("dup2 error to stdin");
}
/*将child的stdout 衔接到fd[1]上面*/
if (fd[] != STDOUT_FILENO && dup2(fd[],STDOUT_FILENO)!=STDOUT_FILENO) {
err_sys("dup2 error to stdout");
}
/*执行opend这个程序 这时opend这个程序的stdin就指向fd[1] child和parent通过pipe连接了起来*/
if (execl("./opend", "opend", (char *))<) {
err_sys("execl error");
}
}
close(fd[]); /*parent*/
} /*iov三个char array合成一个char array 每个array以空格分开*/
sprintf(buf, " %d", oflag);
iov[].iov_base = CL_OPEN " "; /* string concatenation */
iov[].iov_len = strlen(CL_OPEN) + ;
iov[].iov_base = name; /*传入的filename在中间的io*/
iov[].iov_len = strlen(name);
iov[].iov_base = buf;
iov[].iov_len = strlen(buf) + ; /* +1 for null at end of buf */
len = iov[].iov_len + iov[].iov_len + iov[].iov_len;
/*通过fd[0] fd[1]这个通道 由client向server发送数据*/
/*writev在会把缓冲区的输出数据按顺序集合到一起 再发送出去*/
if (writev(fd[], &iov[], ) != len) {
err_ret("writev error");
return(-);
}
/* read descriptor, returned errors handled by write() */
return recv_fd(fd[], write);
} /*这是client端调用的程序*/
int main(int argc, char *argv[])
{
int n, fd;
char buf[BUFFSIZE], line[MAXLINE];
/*每次从stdin cat进来filename*/
while (fgets(line, MAXLINE, stdin)!=NULL) {
/*替换把回车替换掉*/
if (line[strlen(line)-] == '\n') {
line[strlen(line)-] = ;
}
/*打开文件*/
if ((fd = csopen(line, O_RDONLY))<) {
continue;
}
/*把fd这个文件读写完成*/
printf("fd obtained from other process : %d\n",fd);
while ((n = read(fd, buf, BUFFSIZE))>) {
if (write(STDOUT_FILENO, buf, n)!= n) {
err_sys("write error");
}
}
if (n<) {
err_sys("read error");
}
close(fd);
}
exit();
}

maind.c的代码如下:

 #include <errno.h>
#include <fcntl.h>
#include "open_fd.h" #define CL_OPEN "open"
#define MAXARGC 50
#define WHITE " \t\n" char errmsg[MAXLINE];
int oflag;
char *pathname; /* cli_args和buf_args两个函数起到把读进来的buf解析的功能
* 了解大体功能即可 不用细看*/ int cli_args(int argc, char **argv)
{
if (argc != || strcmp(argv[], CL_OPEN) != ) {
strcpy(errmsg, "usage: <pathname> <oflag>\n");
return(-);
}
pathname = argv[]; /* save ptr to pathname to open */
oflag = atoi(argv[]);
return();
} int buf_args(char *buf, int (*optfunc)(int, char **))
{
char *ptr, *argv[MAXARGC];
int argc; if (strtok(buf, WHITE) == NULL) /* an argv[0] is required */
return(-);
argv[argc = ] = buf;
while ((ptr = strtok(NULL, WHITE)) != NULL) {
if (++argc >= MAXARGC-) /* -1 for room for NULL at end */
return(-);
argv[argc] = ptr;
}
argv[++argc] = NULL; /*
* Since argv[] pointers point into the user's buf[],
* user's function can just copy the pointers, even
* though argv[] array will disappear on return.
*/
return((*optfunc)(argc, argv));
} void handle_request(char *buf, int nread, int fd)
{
int newfd;
if (buf[nread-] != ) {
send_err(fd, -, errmsg);
return;
}
if (buf_args(buf, cli_args) < ) { /* parse args & set options */
send_err(fd, -, errmsg);
return;
}
if ((newfd = open(pathname, oflag)) < ) {
send_err(fd, -, errmsg);
return;
}
if (send_fd(fd, newfd) < ) /* send the descriptor */
err_sys("send_fd error");
close(newfd); /* we're done with descriptor */
} /*server端*/
int main(void)
{
int nread;
char buf[MAXLINE];
for (; ; ){
/*一直阻塞着 等着stdin读数据*/
if ((nread = read(STDIN_FILENO, buf, MAXLINE))<) {
err_sys("read error on stream pipe");
}
else if (nread == ) {
break;
}
handle_request(buf, nread, STDOUT_FILENO);
}
exit();
}

其余lib和include中的代码有的是apue书上这个章节的,有的是apue源代码提供的lib,这些不再赘述了。

直接看运行结果(在当前文件夹下面设定了一个xbf的文本文件,流程是让client发送以只读方式打开这个文件的请求,由server打开这个文件,然后再将fd返回)

先得注意msg.msg_controllen与CONTROLLEN是不等的,这是原书勘误表中的一个bug。

server中打开的xbf文件的fd就是存在了msg这个结构体的最后的位置发送过来的。

如果将main.c中的line91注释掉,结果如下:

可以看到,真正client接收到的fd的值,与server端发送时候的fd的值是没有关系的,只是client端哪个最小的fd的值可用,就会用这个fd的值对应上server打开的xbf这个文件。

总结一下,流程是这样的:

(1)server打开xbf文件 →

(2)server将与xbf文件对应的fd挂到cmsghdr的最后 →

(3)server通过fd_pipe产生的unix domain socket将msghdr发送到client端 →

(4)在发送的过程中kernel记录的应该是这个fd对应的file table信息 →

(5)在client接收到这个file table时候,kernel分配一个client端可用的最小fd →

(6)client端获得了一个fd并且这个fd已经指向开打的xbf文件

其余的具体protocol不用细看,但是一些技术细节后面再单独记录。

17.6 An Open Server Version 2

这里主要用到的是naming unix domain socket的技术,为的是可以在unrelated process之间传递file descriptor。

理解这个部分的重点是书上17.29和17.30两个loop函数的实现:一个用的是select函数,一个用的是poll函数。(还需要熟悉守护进程的知识以及command-line argument的解析的套路

要想迅速串起来这部分的代码,还得回顾一下select和poll函数,这二者的输入参数中都有value-on return类型的,先理解好输入参数。

loop.select.c代码如下:

 #include    "opend.h"
#include <sys/select.h> void
loop(void)
{
int i, n, maxfd, maxi, listenfd, clifd, nread;
char buf[MAXLINE];
uid_t uid;
fd_set rset, allset; /* 与poll的用法不同 这里喂给select的fd_set是不预先设定大小的
* 而是靠maxfd来标定大小*/
FD_ZERO(&allset);
/* obtain fd to listen for client requests on */
if ((listenfd = serv_listen(CS_OPEN)) < )
log_sys("serv_listen error");
/* 将server这个用于监听的fd加入集合*/
FD_SET(listenfd, &allset);
/* 需要监听的最大的fd就是刚刚分配的listenfd*/
maxfd = listenfd;
maxi = -; for ( ; ; ) {
rset = allset; /* rset gets modified each time around */
/* select中的&rset这个参数 返回的时候只保留ready的fd*/
if ((n = select(maxfd + , &rset, NULL, NULL, NULL)) < )
log_sys("select error");
/* 处理有client发送请求的case*/
if (FD_ISSET(listenfd, &rset)) {
/* accept new client request */
if ((clifd = serv_accept(listenfd, &uid)) < )
log_sys("serv_accept error: %d", clifd);
i = client_add(clifd, uid);
FD_SET(clifd, &allset); /*A 向allset中增加需要监听的内容*/
if (clifd > maxfd) /* 更新select监控的最大的fd大小*/
maxfd = clifd; /* max fd for select() */
if (i > maxi) /* 更新Client array的大小*/
maxi = i; /* max index in client[] array */
log_msg("new connection: uid %d, fd %d", uid, clifd);
continue;
}
/* 没有新的client 处理Client array中ready的client */
for (i = ; i <= maxi; i++) { /* go through client[] array */
if ((clifd = client[i].fd) < ) /*没被占用的*/
continue;
if (FD_ISSET(clifd, &rset)) { /*在监听的set中*/
/* read argument buffer from client */
if ((nread = read(clifd, buf, MAXLINE)) < ) {
log_sys("read error on fd %d", clifd);
} else if (nread == ) { /* nread=0表明client已经关闭了*/
log_msg("closed: uid %d, fd %d",
client[i].uid, clifd);
client_del(clifd); /* client has closed cxn */
FD_CLR(clifd, &allset); /* B 从allset中删除需要监听的内容*/
close(clifd);
} else { /* process client's request */
handle_request(buf, nread, clifd, client[i].uid);
}
}
}
}
}

loop.pool.c的代码如下:

#include    "opend.h"
#include <poll.h> #define NALLOC 10 /* # pollfd structs to alloc/realloc */ static struct pollfd *
grow_pollfd(struct pollfd *pfd, int *maxfd)
{
int i;
int oldmax = *maxfd;
int newmax = oldmax + NALLOC; if ((pfd = realloc(pfd, newmax * sizeof(struct pollfd))) == NULL)
err_sys("realloc error");
for (i = oldmax; i < newmax; i++) {
pfd[i].fd = -;
pfd[i].events = POLLIN;
pfd[i].revents = ;
}
*maxfd = newmax;
return(pfd);
} void
loop(void)
{
int i, listenfd, clifd, nread;
char buf[MAXLINE];
uid_t uid;
struct pollfd *pollfd;
int numfd = ;
int maxfd = NALLOC; /* 先分配10个fd槽 */
if ((pollfd = malloc(NALLOC * sizeof(struct pollfd))) == NULL)
err_sys("malloc error");
for (i = ; i < NALLOC; i++) {
pollfd[i].fd = -;
pollfd[i].events = POLLIN; /*read*/
pollfd[i].revents = ;
} /* obtain fd to listen for client requests on */
if ((listenfd = serv_listen(CS_OPEN)) < )
log_sys("serv_listen error");
client_add(listenfd, ); /* we use [0] for listenfd */
pollfd[].fd = listenfd; for ( ; ; ) {
/* 这里控制的是numfd而不是maxfd*/
if (poll(pollfd, numfd, -) < )
log_sys("poll error");
/* 1. 先判断是否有新的client请求 */
if (pollfd[].revents & POLLIN) {
/* accept new client request */
if ((clifd = serv_accept(listenfd, &uid)) < )
log_sys("serv_accept error: %d", clifd);
client_add(clifd, uid);
/* possibly increase the size of the pollfd array */
/* 如果Client array数量超过了pollfd的数量 就realloc*/
if (numfd == maxfd)
pollfd = grow_pollfd(pollfd, &maxfd);
pollfd[numfd].fd = clifd;
pollfd[numfd].events = POLLIN;
pollfd[numfd].revents = ;
numfd++;
log_msg("new connection: uid %d, fd %d", uid, clifd);
/* 与select不同 这里没有continue 而是可以直接向下进行
* 为什么可以直接向下进行 而select就不可以
* 因为poll使用pollfd来标定需要等着的fd的
* 每个struct pollfd中
* a. 既有关心ready的事件
* b. 又有真正ready的事件
* 处理一个fd并不会影响其他fd的状态*/
}
/* 2. 再判断有哪些ready的client*/
for (i = ; i < numfd; i++) {
if (pollfd[i].revents & POLLHUP) {
goto hungup;
} else if (pollfd[i].revents & POLLIN) {
/* read argument buffer from client */
if ((nread = read(pollfd[i].fd, buf, MAXLINE)) < ) {
log_sys("read error on fd %d", pollfd[i].fd);
} else if (nread == ) {
hungup:
/* the client closed the connection */
log_msg("closed: uid %d, fd %d",
client[i].uid, pollfd[i].fd);
client_del(pollfd[i].fd);
close(pollfd[i].fd);
if (i < (numfd-)) { /* 这个应该是corner case的判断*/
/* 这么做是为了节约空间
* 把末端的fd及相关信息顶到i这个位置上 */
/* pack the array */
pollfd[i].fd = pollfd[numfd-].fd;
pollfd[i].events = pollfd[numfd-].events;
pollfd[i].revents = pollfd[numfd-].revents;
/* 由于把末位的顶到i这个位置上
* 所以要再check一遍这个位置 */
i--; /* recheck this entry */
}
numfd--;
} else { /* process client's request */
handle_request(buf, nread, pollfd[i].fd,
client[i].uid);
}
}
}
}
}

===================================分割线===================================

记录几个遇到的技术细节问题

1. sign extension的问题

上面recv_fd中的line54有一个不是很直观的做法

int status;

char *ptr;

status = *ptr & 0xFF;

ptr是char类型,可以代表0~255的值,代表不同的返回状态。比如*ptr为128的值用二进制表示为1000000。

由于status是int类型占4bytes 32bits,如果直接status = *ptr,就涉及到位扩展的问题,最高位到底是当成符号位还是取值位呢?

(1)首先,char到底是有符号还是无符号的,取决于编译器,见这篇文章(http://descent-incoming.blogspot.jp/2013/02/c-char-signed-unsigned.html

(2)0xFF默认是无符号int型,高位8都为0

因此,无论char是不是有符号的,一旦与0xFF做了与运算,则相当于把char类型的最高位自动当成了取值位了。就避免了上面提到的符号位扩展的问题。

为了方便记忆,写了一个小例子记录这种sign extension带来的影响:

 #include <stdio.h>
#include <stdlib.h> int main(int argc, char *argv[])
{
/*验证int的byte数目*/
int status = -;
char c1 = ; /*默认254是int类型占4bytes 转换成char类型占1bytes 直接截取低8位*/
unsigned char c2 = ;
/*gcc编译器 默认的char是有符号的 因为直接从char转换到int是用char的符号位补齐高位*/
status = c1;
printf("status converted from c1 : %d\n", status);
/*如果是unsigned char是没有符号位的 因此从unsigned char转换到int是高位直接补0*/
status = c2;
printf("status converted from c2 : %d\n", status);
/*验证默认的0xFF是4 bytes 32 bits的*/
printf("size of defalut int : %ld\n", sizeof(0xFF));
status = c1 & 0xFF;
printf("status converted from c1 & 0xFF : %d\n", status);
/*如果是1 byte 8 bits的int类型*/
int8_t i8 = 0xFF;
status = c1 & i8;
printf("status converted from c1 & int8_t i8 : %d\n", status);
}

执行结果如下:

上面的例子应该可以包含绝大多数情况了。

这是当时看过的一个不错的资料:http://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Data/signExt.html

2. sizeof与strelen的问题

http://www.cnblogs.com/carekee/articles/1630789.html

3. 结构体内存对齐问题

send_fd和recv_fd代码中都用到了一个宏定义CMSG_LEN:查看这个宏在socket.h中的定义,引申出CMSG_ALIGN这个内存对齐的宏定义。

(1)要想回顾CMSG_ALIGN怎么做到内存对齐的,可以参考下面的blog:http://blog.csdn.net/duanlove/article/details/9948947

(2)要想理解为什么要进行内存对齐,可以参考下面的blog:http://www.cppblog.com/snailcong/archive/2009/03/16/76705.html

(3)从实操层面,学习如何计算结构体的内存对齐方法,可以参考下面的blog:http://blog.csdn.net/hairetz/article/details/4084088

把上面的内容总结起来,可得结构体内存对齐如下的结论:

1 A元素是结构体前面的元素 B元素是结构体后面的元素,一般结构体开始的偏移量是0,则:A元素必须让B元素满足 B元素的寻址偏移量是B元素size的整数倍大小
2 整个结构的大小必须是其中最大字段大小的整数倍。
按照上面两个原则 就大概能算出来常规套路下结构体需要内存对齐后的大小
最后还是自己写一个例子,通过实操记忆一下:
 #include <stdio.h>
#include <stdlib.h> struct str1{
char a;
char b;
short c;
long d;
}; struct str2{
char a;
}; int main(int argc, char *argv[])
{
struct str2 s2;
struct str1 s1;
char *p;
char c;
short s;
long l; printf("size of str2 : %ld\n", sizeof(struct str2));
printf("addr of str2.a : %p\n", &s2.a);
printf("size of str1 : %ld\n", sizeof(struct str1));
printf("addr of str1.a : %p\n", &s1.a);
printf("addr of str1.b : %p\n", &s1.b);
printf("addr of str1.c : %p\n", &s1.c);
printf("addr of str1.d : %p\n", &s1.d);
printf("addr of char pointer p : %p\n", &p);
printf("addr of char c : %p\n", &c);
printf("addr of long l : %p\n", &l);
printf("addr of short s : %p\n", &s);
}

运行结果如下:

分析:

(1)结构体内存对齐按照上面说的规律

(2)其余的变量内存分配,并不是完全按照变量定义的顺序,我的理解是按照变量的所占字节的大小,字节大的分配在高地址(stack地址分配由高向低生长),这样有助于节约内存空间,降低内存对齐带来的memory的浪费。

另,深入看了一下malloc函数,果然malloc也是考虑了内存对齐的问题的。

(1)man malloc可以看到如下的信息:

(2)这个blog专门讲malloc考虑内存对齐的内存分配机制的:http://blog.csdn.net/elpmis/article/details/4500917

4. 对于char c = 0 和 char c = '\0'问题的解释

二者本质是一样的,只是表述上有所区别,ascii码'\0'的值就是0.

http://stackoverflow.com/questions/16955936/string-termination-char-c-0-vs-char-c-0

===================================分割线===================================

APUE这本书刷到这里也差不多了,后面两章内容不是很新暂时不刷了。

这本书看过一遍,感觉还是远远不够,以后应该常放在手边翻阅。

【APUE】Chapter17 Advanced IPC & sign extension & 结构体内存对齐的更多相关文章

  1. 关于结构体内存对齐方式的总结(#pragma pack()和alignas())

    最近闲来无事,翻阅msdn,在预编译指令中,翻阅到#pragma pack这个预处理指令,这个预处理指令为结构体内存对齐指令,偶然发现还有另外的内存对齐指令aligns(C++11),__declsp ...

  2. C++ struct结构体内存对齐

    •小试牛刀 我们自定义两个结构体 A 和 B: struct A { char c1; char c2; int i; double d; }; struct B { char c1; int i; ...

  3. [C/C++] 结构体内存对齐用法

    一.为什么要内存对齐 经过内存对齐之后,CPU的内存访问速度大大提升; 内存空间按照byte划分,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内 ...

  4. [C/C++] 结构体内存对齐:alignas alignof pack

    简述: alignas(x):指定结构体内某个成员的对齐字节数,指定的对齐字节数不能小于它原本的字节数,且为2^n; #pragma pack(x):指定结构体的对齐方式,只能缩小结构体的对齐数,且为 ...

  5. C语言-结构体内存对齐

    C语言结构体对齐也是老生常谈的话题了.基本上是面试题的必考题.内容虽然很基础,但一不小心就会弄错.写出一个struct,然后sizeof,你会不会经常对结果感到奇怪?sizeof的结果往往都比你声明的 ...

  6. c 结构体内存对齐详解

    0x00简介 首先要知道结构体的对齐规制 1.第一个成员在结构体变量偏移量为0的地址处 2.其他成员变量对齐到某个数字的整数倍的地址处 对齐数=编辑器默认的一个对齐数与该成员大小的较小值 vs中默认的 ...

  7. go语言结构体内存对齐

    cpu要想从内存读取数据,需要通过地址总线,把地址传输给内存,内存准备好数据,输出到数据总线,交给cpu,如果地址总线只有8根,那这个地址就只有8位可以表示[0,255]256个地址,因为表示不了更多 ...

  8. C++结构体内存对齐跨平台测试

    测试1,不规则对齐数据. Code: #include <stdio.h> #pragma pack(push) #pragma pack(8) struct Test8 { char a ...

  9. C/C++ 结构体内存对齐

    内存对齐是指的是编译器在编译的时候总是会将结构体的元素的地址放在一些合适的位置使得CPU访问这些数据的效率变得更高.首先来看下面这个例子: struct A{ char a; char b; int ...

随机推荐

  1. zobrist hashing

    Zobrist 哈希是一种专门针对棋类游戏而提出来的编码方式,以其发明者 Albert L.Zobrist 的名字命名.Zobrist 哈希通过一种特殊的置换表,也就是对棋盘上每一位置的各个可能状态赋 ...

  2. 第八篇 :微信公众平台开发实战Java版之如何网页授权获取用户基本信息

    第一部分:微信授权获取基本信息的介绍 我们首先来看看官方的文档怎么说: 如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑. 关于网页授权回调域 ...

  3. 初次使用Docker的体验笔记

    一.前言 Docker容器已经发布许久,但作为一名程序员如今才开始接触,实在是罪过--        在此之前,我还没有对Docker进行过深入的了解,对它的认识仍停留在:这是一种新型的虚拟机.这样的 ...

  4. hyper-v无线网络上外网

    这个通过无线网络上外网也是找了很多文章,大部分写的都不详细,没有办法成功,原理就是创建一个虚拟网卡,然后把创建的虚拟网卡和无线网卡桥接,虚拟机中使用创建的虚拟网卡,这里创建的虚拟网卡指的是用hyper ...

  5. myeclipe eclipse 常遇问题:Some projects cannot be imported 、java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver、The file connot be validate

    1.Some projects cannot be imported because they already exist in the workspace 2.Some projects were ...

  6. zsh

    一.简介 Zsh 也许是目前最好用的 shell,是 bash 替代品中较为优秀的一个.   二.优点 1)补全 zsh 的命令补全功能非常强大,可以补齐路径,补齐命令,补齐参数等. 按下 tab 键 ...

  7. 【Android UI设计与开发】8.顶部标题栏(一)ActionBar 奥义·详解

    一.ActionBar介绍 在Android 3.0中除了我们重点讲解的Fragment外,Action Bar也是一个非常重要的交互元素,Action Bar取代了传统的tittle bar和men ...

  8. [cocos2dx] 让UIButton支持disable状态

    摘要: 主要解决cocos2dx-2.2.2版本中, UIButton显示不了disable状态图的问题. 顺便, 理解了一下cocos2dx中UIWidget的渲染原理. 博客: http://ww ...

  9. Codeforces 13C Sequence --DP+离散化

    题意:给出一个 n (1 <= n <= 5000)个数的序列 .每个操作可以把 n 个数中的某一个加1 或 减 1.问使这个序列变成非递减的操作数最少是多少 解法:定义dp[i][j]为 ...

  10. HDU 2899 Strange fuction 【三分】

    三分可以用来求单峰函数的极值. 首先对一个函数要使用三分时,必须确保该函数在范围内是单峰的. 又因为凸函数必定是单峰的. 证明一个函数是凸函数的方法: 所以就变成证明该函数的一阶导数是否单调递增,或者 ...