读《CSAPP》 2E II -- 2015.08.26– 09.18
读《CSAPP》2E I的笔记。
为读《CSAPP》III做准备。
2015.08.26-- 09.05
针对的优化代码对象是程序常被运行的(用于实时处理数据等的程序),如循环内的代码移动、过程调用、存储器引用。想要得到执行速度快的代码,可以直接使用汇编语言编写,但汇编语言涉及到移植性问题。
优化程序性能指导。
表示程序性能的方式:CPE。 优化目标:处理器延迟界限、吞吐量界限。 提升性能的评判方法:Amdahl定律。 |
减少过程调用的开销,消除妨碍编译器优化代码的因素。
(1) 代码移动 对象:如循环中测试条件为无副作用的函数调用时 原因:编译器不会对可能含有副作用(不判断函数是否有副作用)的代码进行代码移动。 (2) 减少函数调用 对象:如在循环中调用函数 原因:过程调用会带来相当大的开销,且可能妨碍大多数形式程序的优化。 (3) 消除不必要的存储器引用 对象:如循环中对存储器的不必要引用 原因:对存储器的引用所耗时间更长(对比于寄存器),可以将不必要的存储器引用换成引用存储器更少的操作或者对寄存器的引用的操作。 (4) 循环展开 减少分支预测错误的开销;重关联变换(gcc编译器-funroll-loop参数带此功能)。 |
理解微处理器体系结构,进一步提高程序性能。
(1) 执行指令流程/机制 ---- 提高并行性(一条指令对应一组寄存器;条件传送) (2) 硬件单元性能 |
理解存储器层次结构。
让程序具有局部性,将数据存在高层的存储器中。 |
优化程序实践方式。
(1) 用测试程序性能的工具找到程序中耗时较大的部分 (2) 读耗时较大部分对应的汇编代码(预测并行执行的操作及硬件资源的利用;关键路径) |
2015.09.05-- 09.10
存储器的线性数组模型。
Figure 1. 存储器的线性模型
存储技术。
RAM,ROM,磁盘(结构),固态硬盘(结构)。
对含“控制器”的磁盘和固态硬盘来说,“控制器”屏蔽了二者的物理结构而提供给操作系统程序一种逻辑结构。操作系统根据“控制器”提供的“逻辑结构”通过“控制器”完成对磁盘、硬盘的读写操作。
存储器层次结构。
现代的计算机系统中都使用了“存储器层次结构”这种方法。这样可以达到“成本与层次结构底层最便宜设备相当,但是却以接近于层次结构顶部存储设备的高速率向程序提供数据”的效果。
局部性和高速缓存友好性。
局部性:时间局部性(分块)和空间局部性(利于存储器层次结构的好处,如虚拟存储器)。
高速缓存友好性:能使cache更容易有较低的不命中率(结合具体的cache机制);对局部变量的反复引用是好的,因为编译器能够将它们缓存在寄存器文件中(时间局部性);步长为1的引用模式是好的,因为存储器层次结构中所有层次上的缓存都是将数据存储为连续的块(空间局部性),空间局部性和循环展开。
处理器的(逻辑)控制流。
(CPU的)程序计数器从一条指令的地址到另一条指令的地址的过程称为控制转移,这样的控制转移序列被称为处理器的控制流。
那些突变的(如中断、陷阱、故障(信号,非本地跳转)、终止)控制转移序列称为处理器异常控制流。
并发流。
一个逻辑流的执行在时间上与另一个流重叠。
并行流。
多个流并发的运行在不同的处理器核或者计算机上。
多任务。
进程:一个执行中的程序实例。一个进程和其他进程轮流运行的概念即为多任务。
操作系统内核使用“上下文切换”的异常控制流在实现多任务。上下文切换机制建立在中断、陷阱、故障等异常机制之上。上下文是内核为进程维持的,是进程重新启动运行所需的目的寄存器、浮点寄存器、程序计数器、用户栈、内核栈和各种数据结构(页表,进程表)的备份。
内核中的调度器代码负责各进程之间的切换(调度)。
异常。
事件让处理器发生异常控制转移;当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。
进程(私有)地址空间和物理存储器的对应。
不同进程拥有相似(入口地址,内容段位置相同)地址空间;不同进程的地址空间对应不同的寄存器和物理存储地址空间就可以实现多任务(但保持了链接后的可执行程序用用相同结构的虚拟地址空间)。
2015.09.07-- 09.14
虚拟储存器(VM)。
现代计算机系统为了更加有效地管理存储器并且少出错而提出的一种对主存的抽象概念。
物理寻址(早期PC)。
将主存组织成连续线性数组后直接用线性数组的索引(标号,物理地址空间)访问主存的方式。
虚拟寻址(现代PC)。
CPU通过生成一个虚拟地址(虚拟地址空间中的值)来访问主存,这个虚拟地址在被送到存储器之前先被MMU转换成适当的物理地址。将虚拟地址转换为物理地址的方式叫做地址翻译,由内存管理单元MMU完成。
虚拟页和物理页。
虚拟地址(存储器)块;物理地址(存储器)块。
页表。
记录虚拟页和物理页之间的对应关系,知道MMU将虚拟地址翻译成对应的物理地址(虚拟页对应物理页);长期驻扎在DRAM中。
存储器映射。
Linux通过将一个虚拟存储区与一个磁盘上的对象关联起来,以初始化这个虚拟存储区域的内容,这个过程称为存储器映射。
2015.09.14
应用程序应使用如RIO(自己根据需求调用Unix I/O编写的函数)和C标准库I/O;Unix I/O标准I/O更适用于网络应用程序。
使用I/O函数打开一个CPU外的设备时,内核程序将设备的内容载入到内存,并将该设备当成文件模型来对待,为之创建v-node表(包含“文件访问”、“文件大小”、“文件类型”等信息),再创建一个“打开的文件表”(包含“指向v-node表的指针”、“文件位置”、“引用计数”等信息),并往该进程的描述表中添加指向“打开的文件表”中该文件表项的文件描述符(fd)。就这样,应用程序通过fd来访问文件进而也访问到了该设备。
2015.09.14-09.16
客户端-服务器模型。
每个网络应用都是基于客户端-服务器模型的,这里提到的客户端和服务器是运行在机器中的程序(进程)。客户端-服务器模型的基本操作是事物(transaction,指客户端和服务器执行的一系列步骤)。客户端-服务器事物由4步组成:
(1) 当一个客户端需要服务时,它向服务器发送一个请求,发起一个事物。如,当Web浏览器需要一个文件时,它就发送一个请求给Web服务器。
(2) 服务器收到客户端请求后,解释它,并以适当的方式操作它的资源(如一个Web服务器管理一组磁盘文件资源)。如,当Web服务器收到浏览器发出的请求后,它就读一个磁盘文件。
(3) 服务器给客户端发送一个响应,并等待下一个请求。如,Web服务器将文件发送回客户端。
(4) 客户端收到响应并处理它。如,当Web浏览器收到来自服务器的一页后,它就在屏幕上显示此页。
以太网段 局域网 Internet。【意会一下】
一个以太网段包括一些电缆(双绞线)和一个叫集线器的盒子。以太网段通常跨越一些小的区域,例如某建筑物的一个房间或者一个楼层。主机的适配器将连接到集线器的一个端口上,每个以太网适配器都有一个全球唯一的48位地址,它存储在这个适配器的非易失性存储器上。
使用一些电缆和叫网桥的小盒子,多个以太网段可以连接成较大的局域网(桥接以太网)。
在层次更高级别中,多个不兼容的局域网可以通过叫做路由器的特殊计算机连接起来,组成一个Internet(互联网络)。每台路由器对于它所连接到的每个网络都有一个适配器(端口)。
因特网域名和IP地址。
因特网客户端和服务器通过IP地址相互通信。然而,对于人们而言,大整数(IP地址)是很难记住的,所以因特网定义了一组更加人性化的域名,以及一种将域名映射到IP地址的机制。域名是一串用点号分割的单词,如:misskissC.cmcl.cs.cmu.edu
因特网连接。
因特网的客户端-服务器端的连接是通过两端的套接字地址唯一确定的。每个套接字都有相应的地址,是由一个因特网地址(IP地址)和一个16位的整数端口组成的,用“地址:端口”来表示。站在程序的角度,(因特网)的套接字地址存放在某个结构体的某16字节的元素中。(当客户端发起一个连接请求时,客户端的套接字地址中的端口是由内核自动分配的,即临时端口。服务器套接字地址中的端口通常是某个知名的端口,适合这个服务相对应的,如Web服务器通常使用端口80,电子邮件服务器使用端口25。在Unix上,/etc/services包含一张这台机器提供的服务以及它们的知名端口的综合列表 -- 可更改)。在程序中具体通过套接字接口完成连接过程。
Web基础。
HTTP HTML。
Web客户端和服务器之间的交互是基于HTTP(Hypertext TransferProtocol,超文本传输协议)。一个Web客户端(浏览器)打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请示的内容,然后关闭连接。浏览器读取这些内容,并把它显示在屏幕上。
Web内容可以用一种叫做HTML(Hypertext MarkupLanguage,超文本标记语言)的语言来编写。一个HTML程序(页)包含指令(标记),它们告诉浏览器如何如何显示这页中的各种文本和图形对象。HTML可以在一个页面上包含指针(超链接),这些指针可以指向存放在任何因特网主机上的内容。如“<a href="http://www.cmu.edu/index.html">CarnegieMellon</a>”语句会在浏览器页面上高粱显示文本对象Carnegie Mellon,并且创建一个超链接,它指向存放在CMU Web服务器上叫做index.html的HTML文件。如果用户单击了这个高亮文本对象,浏览器就会从CMU服务器中请求相应的HTML文件并显示它。
Web内容。
对于Web客户端和服务器而言,内容是与一个MIME(MulipurposeInternet Mail Extensions,多用途的网际邮件扩充协议)类型相关的字节序列(text/html -- HTML页面;text/plain -- 无格式文本;application/postscript-- Postscript文档;image/gif -- GIF格式编码的二进制图像;image/jpeg -- JPEG格式编码的二进制图像)。Web服务器以两种不同的方式向客户端提供内容:
(1) 取一个磁盘文件,并将它的内容返回给客户端。磁盘文件称为静态内容,而返回文件给客户端的过程称为服务静态内容。
(2) 运行一个可执行文件,并将它的输出返回给客户端。运行时可执行文件产生的输出称为动态内容,而运行程序并返回它的输出到客户端的过程称为服务动态内容。
URL。
每条由Web服务器返回的内容都是和它管理的某个文件相关联的。这些文件中的每一个都有一个唯一的名字,叫做URL(Universal ResourceLocator)。如RULhttp://www.google.com:80/index.html 表示因特网主机www.google.com 上一个称为/index.html的HTML文件,它是由一个监听端口80的Web服务器管理的。可执行文件的URL可以在文件名后包括程序参数。“?”字符分割文件名和参数,且每个参数用“&”字符隔开。如URLhttp://bluefish.ics.cs.cmu.edu:8000/cgi-bin/adder?15000&213 标识了一个叫做/cgi-bin/adder的可执行文件,会带两个参数字符串15000和213来调用它。在事物过程中,客户端和服务器使用URL的不同部分。如 客户端使用http://www.google.com:80 来决定与哪类服务器联系,服务器在哪里,以及它监听的端口号是多少。服务器使用后缀/index.html来发现它在文件系统的中的文件,并确定请求的是静态内容还是动态内容。
HTTP事物。
HTTP请求:请求行(<method><uri> <version>,如GET / HTTP/1.1) + 0个或者多个请求报头(<headername>:<header data>,如host:www.aol.com) + 一个空的文本行终止列表。
HTTP响应:响应行格式为<version><status code><status message>,响应行(如HTTP/1.0 200 OK) + 0个或更多的响应报头(如MIME-Version:1.0) + 终止报头的空行 + 响应主体(<html>…</html>)。
DNS。
DNS(Domain Name System,域名系统)为分布世界范围内的数据库,用来维护因特网域名集合和IP地址集合之间的映射。
2015.09.16
逻辑控制流在时间上重叠就为并发。使用应用级(调内核提供接口等)并发的应用程序称为并发编程。现在操作系统提供了3种构造并发编程的方法:进程、I/O多路复用和线程。无论哪一种机制,同步对共享数据的并发访问都是一个困难的问题,但有相应的机制(方法)来客服这个困难。细节可在编程实践中学习。
进程。
一个执行中的程序的实例。
I/O多路复用。
调用内核提供的函数,让进程挂起,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。【假设要求你编写一个echo服务器,它也能对用户从标准输入键入的交互命令做出响应。在这种情况下,服务器必须响应两个独立的I/0事件:网络客户端发起连接请求;用户在键盘上输入命令行。先等待哪个事件呢?没有哪一个选择是理想的。针对这种困境就可以使用I/O复用技术。】
线程。
线程是运行在进程上下文的逻辑流。线程由内核自动调度,并且内核通过一个整数ID来识别线程。多个线程运行在单一进程的上下文中,因此共享这个进程虚拟地址空间的整个内容,包括它的代码、数据、堆、共享库和打开的文件。
GPROF、VALGRTND、VTUNF。
STRACE;PS;TOP;TMAP;/proc
getrusage函数可检测VM的缺页数量。
cache原理细节 6.4,6.6。多级页表细节 9.6-9.7。
错误包装函数。
实现一个简单的分配器(虚拟内存)。
2015.09.18
宿机:i7-4790+ Windows10_x64。
虚拟机:VMware-workstation-full-12.0.0+ ubuntu-12.04.5-desktop-i386。
运行/解析一下作者编写的“网络编程”和“并发编程”的代码,对这部分内容来一个感性认识,随便学习下人家的代码风格。《CSAPP》2E书中的源码下载地址:http://csapp.cs.cmu.edu/2e/students.html。此部分内容属于走走过场。
将下载的源码目录code拷贝到linux中(/home/mgn/rbooks/csapp/examples/code)。在编译具体文件时要手动给编译器csapp.c和csapp.h的路径。
功能。
从命令行读取一个域名或点分十进制地址,并显示相应的主机条目。
编译、运行。
因为csapp.c内含libpthread.so库中进程相关的函数,所以需要手动为gcc指定libpthread.so链接库(-lpthread);-I参数为gcc指定.h所在的路径(此处目的是指定csapp.h的路径)。【一个域名和一个IP一一对应;多个域名可以映射到多个IP】
localhost为引用运行在同一台机器上的客户端和服务器提供了一种便利可移植的方式。每台因特网主机都有本地定义的域名localhost,这个域名总是映射为本地会送地址127.0.0.1。
源码。
/* $begin hostinfo */ #include "csapp.h" int main(int argc, char **argv) { char **pp; struct in_addr addr; //IP地址 struct hostent *hostp; //域名-主机条目结构 if (argc != 2) { fprintf(stderr, "usage: %s <domain name or dotted-decimal>\n", argv[0]); exit(0); } // 将点分十进制串(argv[1])转换为一个网络网络字节顺序的IP地址(addr) if (inet_aton(argv[1], &addr) != 0) hostp = Gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET); //csapp.c else hostp = Gethostbyname(argv[1]); //csapp.c printf("official hostname: %s\n", hostp->h_name); for (pp = hostp->h_aliases; *pp != NULL; pp++) printf("alias: %s\n", *pp); for (pp = hostp->h_addr_list; *pp != NULL; pp++) { addr.s_addr = ((struct in_addr *)*pp)->s_addr; printf("address: %s\n", inet_ntoa(addr)); //将IP地址串转换为点分十进制串返回 } exit(0); } /* $end hostinfo */
hostinfo.c调用csapp.c中的函数源码:
/* $begin csapp.c */ …… struct hostent *Gethostbyname(const char *name) { struct hostent *p; //返回与name同域名的主机条目 if ((p = gethostbyname(name)) == NULL) dns_error("Gethostbyname error"); return p; } /* $end gethostbyname */ struct hostent *Gethostbyaddr(const char *addr, int len, int type) { struct hostent *p; //通过ip地址从DNS数据库中检索任意的主机条目 if ((p = gethostbyaddr(addr, len, type)) == NULL) dns_error("Gethostbyaddr error"); return p; } ……
功能。
echoclient.c:echo客户端程序。此程序根据命令行参数提供的服务器地址和端口连接服务器。在和服务器建立连接之后,客户端进入一个循环,反复从标准输入读取文本行,发送文本行给服务器,从服务器读取回送的行,并输出结果到标准输出。当fgets在标准输入上遇到EOF或者因为用户键入ctrl-d,或者因为在一个重定向的输入文件中用尽了所有的文本行时,循环就终止。
echoserver.c:echo服务器端程序。打开监听描述符后,进入一个无限循环,每次循环都等待一个来自客户端的连接请求,输出已连接客户端的域名和IP地址,并调用echo函数为这些客户端服务。在echo程序返回后,主程序关闭已经连接描述符。一旦客户端和服务器关闭了它们各自的描述符,连接也就终止了。(一次只能处理一个客户端)
编译、运行。
打开两个putty连接,一个用于编译、运行服务器程序,一个用于编译、运行客户端程序。
服务器程序运行,等待客户端的连接。
服务器连接客户端和收到客户端字符串后的响应。
源码。
/* * echoclient.c - An echo client */ /* $begin echoclientmain */ #include "csapp.h" int main(int argc, char **argv) { int clientfd, port; char *host, buf[MAXLINE]; rio_t rio; if (argc != 3) { fprintf(stderr, "usage: %s <host> <port>\n", argv[0]); exit(0); } host = argv[1]; port = atoi(argv[2]); clientfd = Open_clientfd(host, port); //csapp.c Rio_readinitb(&rio, clientfd); //csapp.c while (Fgets(buf, MAXLINE, stdin) != NULL) { //csapp.c Rio_writen(clientfd, buf, strlen(buf)); //csapp.c Rio_readlineb(&rio, buf, MAXLINE); //csapp.c Fputs(buf, stdout); //csapp.c } Close(clientfd); //line:netp:echoclient:close //csapp.c exit(0); } /* $end echoclientmain */
/* * echoserveri.c - An iterative echo server */ /* $begin echoserverimain */ #include "csapp.h" void echo(int connfd); int main(int argc, char **argv) { int listenfd, connfd, port, clientlen; struct sockaddr_in clientaddr; struct hostent *hp; char *haddrp; if (argc != 2) { fprintf(stderr, "usage: %s <port>\n", argv[0]); exit(0); } port = atoi(argv[1]); listenfd = Open_listenfd(port); //csapp.c while (1) { clientlen = sizeof(clientaddr); connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); //csapp.c /* determine the domain name and IP address of the client */ hp = Gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr, sizeof(clientaddr.sin_addr.s_addr), AF_INET); //csapp.c haddrp = inet_ntoa(clientaddr.sin_addr); //将IP地址串转换为十进制点分串 printf("server connected to %s (%s)\n", hp->h_name, haddrp); echo(connfd); Close(connfd); //csapp.c } exit(0); } /* $end echoserverimain */
除echoserveri.c中的echo函数在echo.c中之外,其余函数都是在csapp.c中定义的错误封包函数。
/* $begin csapp.c */ …… int Open_clientfd(char *hostname, int port) { int rc; // 和运行在主机hostname上得服务器建立一个连接,并在知名端口prot上监听连接。 // 作者自定义函数,具体通过调用socket,gethostbyname,connect等函数实现。 if ((rc = open_clientfd(hostname, port)) < 0) { if (rc == -1) unix_error("Open_clientfd Unix error"); else dns_error("Open_clientfd DNS error"); } return rc; } int Open_listenfd(int port) { int rc; // 打开和返回一个监听描述符,这个描述符准备好在知名端口prot上接收连接请求 // 作者自定义函数,具体通过socket,listen等函数实现。 if ((rc = open_listenfd(port)) < 0) unix_error("Open_listenfd error"); return rc; } /* $begin open_clientfd */ int open_clientfd(char *hostname, int port) { int clientfd; struct hostent *hp; struct sockaddr_in serveraddr; if ((clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) return -1; /* check errno for cause of error */ /* Fill in the server's IP address and port */ if ((hp = gethostbyname(hostname)) == NULL) return -2; /* check h_errno for cause of error */ bzero((char *) &serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; bcopy((char *)hp->h_addr_list[0], (char *)&serveraddr.sin_addr.s_addr, hp->h_length); serveraddr.sin_port = htons(port); /* Establish a connection with the server */ if (connect(clientfd, (SA *) &serveraddr, sizeof(serveraddr)) < 0) return -1; return clientfd; } /* $end open_clientfd */ /* $begin open_listenfd */ int open_listenfd(int port) { int listenfd, optval=1; struct sockaddr_in serveraddr; /* Create a socket descriptor */ if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) return -1; /* Eliminates "Address already in use" error from bind. */ if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int)) < 0) return -1; /* Listenfd will be an endpoint for all requests to port on any IP address for this host */ bzero((char *) &serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons((unsigned short)port); if (bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0) return -1; /* Make it a listening socket ready to accept connection requests */ if (listen(listenfd, LISTENQ) < 0) return -1; return listenfd; } /* $end open_listenfd */ ……… //其余直接看csapp.c,函数过多,有的超过了网络编程的内容
Tiny Web可作为在《CSAPP》2EIII时编写网络编程应用时作为参考,此次作为感性认识网络编程可不继续读TinyWeb的源码。
累且失去续做耐性,留到《CSAPP》2E III或者被跳过自己实现。