【APUE】Chapter17 Advanced IPC & sign extension & 结构体内存对齐
2016-01-11 23:44
561 查看
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。代码如下:
sender端,用command-line argument的方式读入message的外部key,以及写入message queue的数据,具体代码如下:
执行结果如下:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111163145288-904169724.jpg)
分析:
(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就有了自己独一无二的标识,其他进程就可以通过这个标识找到它。
这里“foo.socket"不需要事先真的存在,它只需要是一个独特的名称就可以了。
执行结果如下:
程序执行的当前文件夹下是没有foo.socket这个文件的
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111192632366-380490878.jpg)
执行如上程序:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111192816647-1887823783.jpg)
可以看到执行完程序后:
(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。
具体的示意图,如下所示:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111210117835-949847649.jpg)
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发送来的请求)
serv_accpet函数(这里有一点没看懂 为什么client's name有30s的限制)
cli_conn
17.4 Passing File Descriptors
在进程间传递file descriptor是也是unix domain socket的一种强大的功能。文件打开的各种细节,都隐藏在server端了。
至今在apue上已经有三种进程间的file descriptor的传递方式:
(1)figure3.8的情况,不同的process分别打开同一个file,每个process中的fd有各自的file table,这两个fd基本没有什么关系:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111214604803-1715187451.jpg)
(2)figure8.2的情况,parent通过fork产生child,整个parent的memory layout都copy到child中,这两个fd属于不同的地址空间,但是值是相同的,并且共享同一个file table:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111214656757-1309949321.jpg)
(3)17.4节的情况,通过unix domain socket的方式传递fd,这两个fd属于不同的地址空间,除了共享同一个file table没有其他的不同:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111214754553-1136899920.jpg)
这一部分还讲了其他一些相关的结构体内容,这些细节为了看懂代码而用,关键记住上面的三种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)
接收端的代码recv_fd如下(代码不难理解,有个坑是line56是apue勘误表中才修改过来,否则有问题;勘误表的链接:http://www.apuebook.com/errata3e.html)
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代码如下:
maind.c的代码如下:
其余lib和include中的代码有的是apue书上这个章节的,有的是apue源代码提供的lib,这些不再赘述了。
直接看运行结果(在当前文件夹下面设定了一个xbf的文本文件,流程是让client发送以只读方式打开这个文件的请求,由server打开这个文件,然后再将fd返回)
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111224057085-700261192.jpg)
先得注意msg.msg_controllen与CONTROLLEN是不等的,这是原书勘误表中的一个bug。
server中打开的xbf文件的fd就是存在了msg这个结构体的最后的位置发送过来的。
如果将main.c中的line91注释掉,结果如下:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111224526335-256072579.jpg)
可以看到,真正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代码如下:
loop.pool.c的代码如下:
===================================分割线===================================
记录几个遇到的技术细节问题
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带来的影响:
执行结果如下:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111231806147-1892740852.jpg)
上面的例子应该可以包含绝大多数情况了。
这是当时看过的一个不错的资料: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:/article/1917751.html
(2)要想理解为什么要进行内存对齐,可以参考下面的blog:http://www.cppblog.com/snailcong/archive/2009/03/16/76705.html
(3)从实操层面,学习如何计算结构体的内存对齐方法,可以参考下面的blog:/article/2052873.html
把上面的内容总结起来,可得结构体内存对齐如下的结论:
1 A元素是结构体前面的元素 B元素是结构体后面的元素,一般结构体开始的偏移量是0,则:A元素必须让B元素满足 B元素的寻址偏移量是B元素size的整数倍大小
2 整个结构的大小必须是其中最大字段大小的整数倍。
按照上面两个原则 就大概能算出来常规套路下结构体需要内存对齐后的大小
最后还是自己写一个例子,通过实操记忆一下:
运行结果如下:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111233407163-1326296716.jpg)
分析:
(1)结构体内存对齐按照上面说的规律
(2)其余的变量内存分配,并不是完全按照变量定义的顺序,我的理解是按照变量的所占字节的大小,字节大的分配在高地址(stack地址分配由高向低生长),这样有助于节约内存空间,降低内存对齐带来的memory的浪费。
另,深入看了一下malloc函数,果然malloc也是考虑了内存对齐的问题的。
(1)man malloc可以看到如下的信息:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111233907335-1470584407.jpg)
(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这本书刷到这里也差不多了,后面两章内容不是很新暂时不刷了。
这本书看过一遍,感觉还是远远不够,以后应该常放在手边翻阅。
这一章主要讲了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, 0, sizeof(m)); if ((n = msgrcv(tip->qid, &m, MAXMSZ, 0, MSG_NOERROR))<0) { err_sys("msgrcv error"); } /*来自一个消息队列的内容 就特定的file desrciptor中*/ if (write(tip->fd, m.mtext, n)<0) { err_sys("write error"); } } } int main(int argc, char *argv[]) { int i, n, err; int fd[2]; int qid[NQ]; /*message queue在process内部的identifier*/ struct pollfd pfd[NQ]; struct threadinfo ti[NQ]; pthread_t tid[NQ]; char buf[MAXMSZ]; /*给每个消息队列设定处理线程*/ for (i=0; i<NQ; i++) { /*返回消息队列的identifier 类似file descriptor*/ if ((qid[i] = msgget((KEY+i), IPC_CREAT|0666))<0) { err_sys("msgget error"); } printf("queue ID %d is %d\n", i, qid[i]); /*构建unix domain sockets*/ if (socketpair(AF_UNIX, SOCK_DGRAM, 0, fd)<0) { err_sys("socketpair error"); } pfd[i].fd = fd[0]; /*main线程把住fd[0]这头*/ pfd[i].events = POLLIN; /*有data要去读*/ /* qid[i]在同一个process都可以用来表示同一个message queue */ ti[i].qid = qid[i]; /*在每个线程中记录要处理的消息队列的id*/ ti[i].fd = fd[1]; /*每个队列的线程把住fd[1]这头*/ /*为每个消息队列创建一个处理线程 并将对应的threadinfo参数传入线程*/ if ((err = pthread_create(&tid[i], NULL, helper, &ti[i]))!=0) { err_exit(err, "pthread_create error"); } } for (;;) { /*一直轮询着 直到有队列可以等待了 再执行*/ if (poll(pfd, NQ, -1)<0) { err_sys("poll error"); } /*由于能进行到这里 则一定是有队列ready了 找到所有ready的队列*/ for (i=0; i<NQ; i++) { if (pfd[i].revents & POLLIN) { /*挑出来所有满足POLLIN条件的*/ if ((n=read(pfd[i].fd, buf, sizeof(buf)))<0) { err_sys("read error"); } buf = 0; /* 这个末尾赋'\0'是必要的 因为接下来要执行printf*/ printf("queue id %d, message %s\n",qid[i],buf); } } } exit(0); }
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 != 3) { fprintf(stderr, "usage: sendmsg KEY message\n"); exit(1); } key = strtol(argv[1], NULL, 0); if ((qid = msgget(key,0))<0) { err_sys("can't open queue key %s", argv[1]); } memset(&m, 0, sizeof(m)); strncpy(m.mtext, argv[2], MAXMSZ-1); nbytes = strlen(m.mtext); m.mtype = 1; if (msgsnd(qid, &m, nbytes, 0)<0) { err_sys("can't send message"); } exit(0); }
执行结果如下:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111163145288-904169724.jpg)
分析:
(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, 0, sizeof(un.sun_path)); strcpy(un.sun_path, "foo.socket"); if ((fd = socket(AF_UNIX, SOCK_STREAM, 0))<0) { err_sys("socket fail"); } size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path); if (bind(fd, (struct sockaddr *)&un, size)<0) { err_sys("bind failed"); } printf("UNIX domain socket bound\n"); exit(0); }
这里“foo.socket"不需要事先真的存在,它只需要是一个独特的名称就可以了。
执行结果如下:
程序执行的当前文件夹下是没有foo.socket这个文件的
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111192632366-380490878.jpg)
执行如上程序:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111192816647-1887823783.jpg)
可以看到执行完程序后:
(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。
具体的示意图,如下所示:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111210117835-949847649.jpg)
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 -1; } /*这里创建的方式是SOCK_STREAM*/ if ((fd = socket(AF_UNIX, SOCK_STREAM, 0))<0) { return -2; } /*防止name已经被占用了 这是一种排他的做法*/ unlink(name); /*初始化socket address structure*/ memset(&un, 0, 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)<0) { rval = -3; goto errout; } /*执行listen的操作 并设置等待队列的长度*/ if (listen(fd, QLEN)<0) { rval = -4; 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+1)))==NULL) { return -1; } len = sizeof(un); /*就在这里阻塞着 等着client端发送来请求*/ if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len))<0) { free(name); return -2; } /*再让len为path的实际长度 并存到name中*/ len -= offsetof(struct sockaddr_un, sun_path); memcpy(name, un.sun_path, len); name[len] = 0; /*最后补上\0*/ if (stat(name, &statbuf)<0) { /*让statbuf获得client关联的文件的status*/ rval = -3; goto errout; } /*1. 验证与client端关联的文件类型是不是socket file*/ #ifdef S_ISSOCK if (S_ISSOCK(statbuf.st_mode)==0) { rval = -4; 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 = -5; goto errout; } /*3. 验证与client端关联的文件被创建的时间*/ staletime = time(NULL) - STALE; /**/ if (statbuf.st_atim < staletime || statbuf.st_ctim < staletime || statbuf.st_mtim < staletime) { rval = -6; 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 = 0; /*1. 验证传入的name是否合理 * 这个name是server的name 先校验server name的长度 */ if (strlen(name) >= sizeof(un.sun_path)) { errno = ENAMETOOLONG; return -1; } /*2. 构建client端的fd * 这个fd是client的专门发送请求的fd*/ if ((fd = socket(AF_UNIX, SOCK_STREAM, 0))<0) { return -1; } /*3. 构建client端的地址*/ /* 将文件名+进程号共写进un.sun_path 并记录长度 这里约定了path的格式*/ memset(&un, 0, 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)<0) { rval = -2; goto errout; } /* 为什么要先绑定再设定权限?因为如果不能绑定 修改权限就是无用功*/ if (chmod(un.sun_path, CLI_PERM)<0) { rval = -3; do_unlink = 1; goto errout; } /*5. 告诉client通过name去找server*/ /* 通过这个name这个key与'server'的process建立连接*/ memset(&sun, 0 ,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)<0) { rval = -4; do_unlink = 1; 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基本没有什么关系:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111214604803-1715187451.jpg)
(2)figure8.2的情况,parent通过fork产生child,整个parent的memory layout都copy到child中,这两个fd属于不同的地址空间,但是值是相同的,并且共享同一个file table:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111214656757-1309949321.jpg)
(3)17.4节的情况,通过unix domain socket的方式传递fd,这两个fd属于不同的地址空间,除了共享同一个file table没有其他的不同:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111214754553-1136899920.jpg)
这一部分还讲了其他一些相关的结构体内容,这些细节为了看懂代码而用,关键记住上面的三种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[1]; struct msghdr msg; char buf[2]; /*这是真正的协议头的两个特征bytes*/ /*scatter read or gather write 具体参考14.6 * 具体到这里的情景比较简单 因为iovec的长度只有1 相当于就调用了一个write * 但是Unix domain socket的格式要去必须是struct iovec这种数据格式*/ iov[0].iov_base = buf; iov[0].iov_len = 2; msg.msg_iov = iov; msg.msg_iovlen = 1; msg.msg_name = NULL; msg.msg_namelen = 0; /*调用send_fd分两种情况: * 1. 正常调用传递fd, 则fd_to_send是大于零的 * 2. 在send_err中调用send_fd, 则fd_to_send表示的是errorcode*/ if (fd_to_send<0) { msg.msg_control = NULL; msg.msg_controllen = 0; buf[1] = -fd_to_send; /*出错的fd_to_send都是负数*/ if (buf[1] == 0) { /*这个protocol并不是完美的 如果fd_to_send 是-256 则没有正数与其对应 协议在这里特殊处理-1与-256都代表 errorcode 1*/ buf[1] = 1; } } else { /*这里cmptr获得的memory大小是由CMSG_LEN算出来的*/ if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL ) { return -1; } /*通过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[1] = 0; } buf[0] = 0; /*这就是给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, 0)!=2) { return -1; } return 0 }
接收端的代码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[1]; struct msghdr msg; status = -1; for ( ; ; ) { iov[0].iov_base = buf; iov[0].iov_len = sizeof(buf); msg.msg_iov = iov; msg.msg_iovlen = 1; msg.msg_name = NULL; msg.msg_namelen = 0; if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL) return(-1); msg.msg_control = cmptr; msg.msg_controllen = CONTROLLEN; if ((nr = recvmsg(fd, &msg, 0)) < 0) { err_ret("recvmsg error"); return(-1); } else if (nr == 0) { err_ret("connection closed by server"); return(-1); } /* * 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++ == 0) { if (ptr != &buf[nr-1]) err_dump("message format error"); status = *ptr & 0xFF; /* prevent sign extension */ if (status == 0) { 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 -= 2; } } if (nr > 0 && (*userfunc)(STDERR_FILENO, buf, nr) != nr) return(-1); if (status >= 0) /* 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[10]; struct iovec iov[3]; static int fd[2] = {-1, -1}; /*首次需要建立child parent的链接*/ if (fd[0] < 0) { printf("frist time build up fd_pipe\n"); /*构建一个全双工的pipe*/ if (fd_pipe(fd) < 0) { err_ret("fd_pipe error"); return -1; } printf("fd[0]:%d,fd[1]:%d\n",fd[0],fd[1]); if((pid = fork())<0){ err_ret("fork error"); return -1; } else if (pid ==0) { /*child*/ close(fd[0]); /*这个地方需要注意 这种full-duplex的fd 可以把in和out都挂到这个fd上面 之前只挂了stdin没有挂out所以有问题*/ /*将child的stdin 衔接到fd[1]上面*/ if (fd[1] != STDIN_FILENO && dup2(fd[1],STDIN_FILENO)!=STDIN_FILENO) { err_sys("dup2 error to stdin"); } /*将child的stdout 衔接到fd[1]上面*/ if (fd[1] != STDOUT_FILENO && dup2(fd[1],STDOUT_FILENO)!=STDOUT_FILENO) { err_sys("dup2 error to stdout"); } /*执行opend这个程序 这时opend这个程序的stdin就指向fd[1] child和parent通过pipe连接了起来*/ if (execl("./opend", "opend", (char *)0)<0) { err_sys("execl error"); } } close(fd[1]); /*parent*/ } /*iov三个char array合成一个char array 每个array以空格分开*/ sprintf(buf, " %d", oflag); iov[0].iov_base = CL_OPEN " "; /* string concatenation */ iov[0].iov_len = strlen(CL_OPEN) + 1; iov[1].iov_base = name; /*传入的filename在中间的io*/ iov[1].iov_len = strlen(name); iov[2].iov_base = buf; iov[2].iov_len = strlen(buf) + 1; /* +1 for null at end of buf */ len = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len; /*通过fd[0] fd[1]这个通道 由client向server发送数据*/ /*writev在会把缓冲区的输出数据按顺序集合到一起 再发送出去*/ if (writev(fd[0], &iov[0], 3) != len) { err_ret("writev error"); return(-1); } /* read descriptor, returned errors handled by write() */ return recv_fd(fd[0], 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)-1] == '\n') { line[strlen(line)-1] = 0; } /*打开文件*/ if ((fd = csopen(line, O_RDONLY))<0) { continue; } /*把fd这个文件读写完成*/ printf("fd obtained from other process : %d\n",fd); while ((n = read(fd, buf, BUFFSIZE))>0) { if (write(STDOUT_FILENO, buf, n)!= n) { err_sys("write error"); } } if (n<0) { err_sys("read error"); } close(fd); } exit(0); }
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 != 3 || strcmp(argv[0], CL_OPEN) != 0) { strcpy(errmsg, "usage: <pathname> <oflag>\n"); return(-1); } pathname = argv[1]; /* save ptr to pathname to open */ oflag = atoi(argv[2]); return(0); } 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(-1); argv[argc = 0] = buf; while ((ptr = strtok(NULL, WHITE)) != NULL) { if (++argc >= MAXARGC-1) /* -1 for room for NULL at end */ return(-1); 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-1] != 0) { send_err(fd, -1, errmsg); return; } if (buf_args(buf, cli_args) < 0) { /* parse args & set options */ send_err(fd, -1, errmsg); return; } if ((newfd = open(pathname, oflag)) < 0) { send_err(fd, -1, errmsg); return; } if (send_fd(fd, newfd) < 0) /* 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))<0) { err_sys("read error on stream pipe"); } else if (nread == 0) { break; } handle_request(buf, nread, STDOUT_FILENO); } exit(0); }
其余lib和include中的代码有的是apue书上这个章节的,有的是apue源代码提供的lib,这些不再赘述了。
直接看运行结果(在当前文件夹下面设定了一个xbf的文本文件,流程是让client发送以只读方式打开这个文件的请求,由server打开这个文件,然后再将fd返回)
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111224057085-700261192.jpg)
先得注意msg.msg_controllen与CONTROLLEN是不等的,这是原书勘误表中的一个bug。
server中打开的xbf文件的fd就是存在了msg这个结构体的最后的位置发送过来的。
如果将main.c中的line91注释掉,结果如下:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111224526335-256072579.jpg)
可以看到,真正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)) < 0) log_sys("serv_listen error"); /* 将server这个用于监听的fd加入集合*/ FD_SET(listenfd, &allset); /* 需要监听的最大的fd就是刚刚分配的listenfd*/ maxfd = listenfd; maxi = -1; for ( ; ; ) { rset = allset; /* rset gets modified each time around */ /* select中的&rset这个参数 返回的时候只保留ready的fd*/ if ((n = select(maxfd + 1, &rset, NULL, NULL, NULL)) < 0) log_sys("select error"); /* 处理有client发送请求的case*/ if (FD_ISSET(listenfd, &rset)) { /* accept new client request */ if ((clifd = serv_accept(listenfd, &uid)) < 0) 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 = 0; i <= maxi; i++) { /* go through client[] array */ if ((clifd = client[i].fd) < 0) /*没被占用的*/ continue; if (FD_ISSET(clifd, &rset)) { /*在监听的set中*/ /* read argument buffer from client */ if ((nread = read(clifd, buf, MAXLINE)) < 0) { log_sys("read error on fd %d", clifd); } else if (nread == 0) { /* 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 = -1; pfd[i].events = POLLIN; pfd[i].revents = 0; } *maxfd = newmax; return(pfd); } void loop(void) { int i, listenfd, clifd, nread; char buf[MAXLINE]; uid_t uid; struct pollfd *pollfd; int numfd = 1; int maxfd = NALLOC; /* 先分配10个fd槽 */ if ((pollfd = malloc(NALLOC * sizeof(struct pollfd))) == NULL) err_sys("malloc error"); for (i = 0; i < NALLOC; i++) { pollfd[i].fd = -1; pollfd[i].events = POLLIN; /*read*/ pollfd[i].revents = 0; } /* obtain fd to listen for client requests on */ if ((listenfd = serv_listen(CS_OPEN)) < 0) log_sys("serv_listen error"); client_add(listenfd, 0); /* we use [0] for listenfd */ pollfd[0].fd = listenfd; for ( ; ; ) { /* 这里控制的是numfd而不是maxfd*/ if (poll(pollfd, numfd, -1) < 0) log_sys("poll error"); /* 1. 先判断是否有新的client请求 */ if (pollfd[0].revents & POLLIN) { /* accept new client request */ if ((clifd = serv_accept(listenfd, &uid)) < 0) 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 = 0; 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 = 1; 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)) < 0) { log_sys("read error on fd %d", pollfd[i].fd); } else if (nread == 0) { 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-1)) { /* 这个应该是corner case的判断*/ /* 这么做是为了节约空间 * 把末端的fd及相关信息顶到i这个位置上 */ /* pack the array */ pollfd[i].fd = pollfd[numfd-1].fd; pollfd[i].events = pollfd[numfd-1].events; pollfd[i].revents = pollfd[numfd-1].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 = -1; char c1 = 254; /*默认254是int类型占4bytes 转换成char类型占1bytes 直接截取低8位*/ unsigned char c2 = 254; /*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://images2015.cnblogs.com/blog/707631/201601/707631-20160111231806147-1892740852.jpg)
上面的例子应该可以包含绝大多数情况了。
这是当时看过的一个不错的资料: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:/article/1917751.html
(2)要想理解为什么要进行内存对齐,可以参考下面的blog:http://www.cppblog.com/snailcong/archive/2009/03/16/76705.html
(3)从实操层面,学习如何计算结构体的内存对齐方法,可以参考下面的blog:/article/2052873.html
把上面的内容总结起来,可得结构体内存对齐如下的结论:
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); }
运行结果如下:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111233407163-1326296716.jpg)
分析:
(1)结构体内存对齐按照上面说的规律
(2)其余的变量内存分配,并不是完全按照变量定义的顺序,我的理解是按照变量的所占字节的大小,字节大的分配在高地址(stack地址分配由高向低生长),这样有助于节约内存空间,降低内存对齐带来的memory的浪费。
另,深入看了一下malloc函数,果然malloc也是考虑了内存对齐的问题的。
(1)man malloc可以看到如下的信息:
![](http://images2015.cnblogs.com/blog/707631/201601/707631-20160111233907335-1470584407.jpg)
(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这本书刷到这里也差不多了,后面两章内容不是很新暂时不刷了。
这本书看过一遍,感觉还是远远不够,以后应该常放在手边翻阅。
相关文章推荐
- GPUImage滤镜之锐化
- POJ【2031】Building a Space Station
- UiNavigationControllerDemo 使用协议代理在UiViewControl间正反向传值
- UIControl的几个事件
- query 中 (function( window, undefined ) {})(window)写法详解(转)
- iOS开发UITableView小结
- 【UI基础】——提示框和警示框的实现
- GPUImage滤镜之自然饱和度
- Arduino接口-week1-Lesson3
- UIScreen的scale属性
- IOS-UIScrollView 与 UICllectionVew
- Arduino接口-week1-Lesson2
- 同时安装Xcode6和Xcode7导致出现N多UUID 模拟器解决办法
- 【java】request.getParameter 和 request.getAttribute 的功能作用详解.
- EasyUI 学习使用笔记
- IOS-UITableView 及自定义cell
- 创建型-生成器/建造者builder
- Arduino接口-week1-Lesson1
- android ui
- IO_文件分割与合并_RandomAccessFile_初始化参数_SequenceInputStreamJAVA164-167