这一章中的内容出现在博主的多篇文章中,所以并不对这一章进行详细的说明解释,只是对几个比较重要的概念进行说明
一.写时复制技术
COW技术初窥:
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
那么子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?
在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
还有个细节问题就是,fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
COW详述:
现在有一个父进程P1,这是一个主体,那么它是有灵魂也就身体的。现在在其虚拟地址空间(有相应的数据结构表示)上有:正文段,数据段,堆,栈这四个部分,相应的,内核要为这四个部分分配各自的物理块。即:正文段块,数据段块,堆块,栈块。至于如何分配,这是内核去做的事,在此不详述。
1. 现在P1用fork()函数为进程创建一个子进程P2,
内核:
(1)复制P1的正文段,数据段,堆,栈这四个部分,注意是其内容相同。
(2)为这四个部分分配物理块,P2的:正文段->PI的正文段的物理块,其实就是不为P2分配正文段块,让P2的正文段指向P1的正文段块,数据段->P2自己的数据段块(为其分配对应的块),堆->P2自己的堆块,栈->P2自己的栈块。如下图所示:同左到右大的方向箭头表示复制内容。
2. 写时复制技术:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
3. vfork():这个做法更加火爆,内核连子进程的虚拟地址空间结构也不创建了,直接共享了父进程的虚拟空间,当然了,这种做法就顺水推舟的共享了父进程的物理空间
通过以上的分析,相信大家对进程有个深入的认识,它是怎么一层层体现出自己来的,进程是一个主体,那么它就有灵魂与身体,系统必须为实现它创建相应的实体, 灵魂实体与物理实体。这两者在系统中都有相应的数据结构表示,物理实体更是体现了它的物理意义。以下援引LKD
传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下—举例来说,fork()后立即调用exec()—它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于Unix强调进程快速执行的能力,所以这个优化是很重要的。这里补充一点:Linux COW与exec没有必然联系
这一章介绍的主题是--高级文件 I/O
一. 分散--聚集I/O
分散聚集I/O是一种进行输入和输出的方法。通过此方法,单一系统调用可以将缓冲区向量写入单一数据流,或者将单一数据流读取到缓冲区向量。这个类型的I/O之所以会有此名称,是因为数据会被分散至或聚集自特定的缓冲区向量。这种方式的输入和输出又称为向量I/O。相比较之下,第二章的标准读取和写入系统调用所提供的是线性I/O。
这里有两个函数实现了一对采用分散--聚集I/O机制的系统调用。
名称:: readv/writev
功能:散布读/聚集写
用法:#include <sys/uio.h> 函数原形: ssize_t readv(int filedes,const struct iovec*iov,int iovcnt); ssize_t writev(int filedes,const struct iovec*iov,int iovcnt); 参数: filedes 文件描述符 iov 指向iovec结构数组的一个指针。 iovcnt 数组元素的个数
返回值:
若成功则返回已读、写的字节数,若出错则返回-1
readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数成为散布读和聚集写。
这两个函数的第二个参数是指向iovec结构数组的一个指针:
struct iovec{ void *iov_base; size_t iov_len; };
writev以顺序iov[0]至iov[iovcnt-1]从缓冲区中聚集输出数据。writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和。
readv则将读入的数据按上述同样顺序散布读到缓冲区中。readv总是先填满一个缓冲区,然后再填写下一个。readv返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回0。
下面给出一个实现的例子说明函数的用法:#include <stdio.h> #include <fcntl.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/uio.h> int main(){ int fd = open("test", O_RDWR); char *buf[] = {"abcdefg\n", "abcdefgh\n", "abcdefghi\n"}; struct iovec iov[3]; int i, nr, j; for(i = 0; i < 3; i++){ iov[i].iov_base = buf[i]; iov[i].iov_len = strlen(buf[i]+1); } ftruncate(fd, 0); nr = writev(fd, iov, 3); close(fd); fd = open("test", O_RDWR); char a[10], b[11], c[12]; iov[0].iov_base = a; iov[0].iov_len = sizeof(a); iov[1].iov_base = b; iov[1].iov_len = sizeof(b); iov[2].iov_base = c; iov[2].iov_len = sizeof(c); nr = readv(fd, iov, 3); printf("%d %d\n", (int)iov[0].iov_base, (int)a); for(i = 0; i < 3; i++){ printf("%s", (char*)iov[i].iov_base); } close(fd); return 0; }
说明:
和多次的线性I/O相比,向量I/O不仅减少了系统调用的次数,而且可以经过内核的优化提供性能的改善。一个进程可以执行单次向量操作不会与另一个进程的操作交叉在一起的风险。内核动态分配内部数据结构表示每个区段,但是如果小于8的话,内核会在它使用的堆栈上为段创建一个小型的数组,大小为8,这时就不需要动态分配了。
实现:
readv()和writev()的简单实现可以在用户空间中以一个简单的循环来完成,它看起来可能会是这样:
#include<unistd.h> #include<sys/uio.h> ssize_t naive_writev(int fd , const struct iovec *iov , int count) { ssize_t ret = 0; int i; for(i = 0; i < count; i++){ ssize_t nr; nr = write(fd, iov[i].iov_base, iov[i].iov_len); if(nr == -1){ ret == -1; break; } ret += nr; } return ret; }
事实上,Linux内核内部所有I/O均采用向量的方式,尽管read()和write()被实现成向量I/O,但是向量只有一个段,所以,仍是线性的实现原理。
二.将文件映射至内存---内存映射
首先原理图如下:
Linux提供了内存映射函数mmap, 它把文件内容映射到一段内存上(准确说是虚拟内存上), 通过对这段内存的读取和修改, 实现对文件的读取和修改, 先来看一下mmap的函数声明:
头文件: <unistd.h> <sys/mman.h> 原型: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offsize); 返回值: 成功则返回映射区起始地址, 失败则返回MAP_FAILED(-1). 参数: addr: 指定映射的起始地址, 通常设为NULL, 由系统指定. length: 将文件的多大长度映射到内存. prot: 映射区的保护方式, 可以是: PROT_EXEC: 映射区可被执行. PROT_READ: 映射区可被读取. PROT_WRITE: 映射区可被写入. PROT_NONE: 映射区不能存取. flags: 映射区的特性, 可以是: MAP_SHARED: 对映射区域的写入数据会复制回文件, 且允许其他映射该文件的进程共享. MAP_PRIVATE: 对映射区域的写入操作会产生一个映射的复制(copy-on-write), 对此区域所做的修改不会写回原文件. 此外还有其他几个flags不很常用, 具体查看linux C函数说明. fd: 由open返回的文件描述符, 代表要映射的文件. offset: 以文件开始处的偏移量, 必须是分页大小的整数倍, 通常为0, 表示从文件头开始映射. 下面说一下内存映射的步骤: 用open系统调用打开文件, 并返回描述符fd. 用mmap建立内存映射, 并返回映射首地址指针start. 对映射(文件)进行各种操作, 显示(printf), 修改(sprintf). 用munmap(void *start, size_t lenght)关闭内存映射. 用close系统调用关闭文件fd.
注意事项:
在修改映射的文件时, 只能在原长度上修改, 不能增加文件长度, 因为内存是已经分配好的.
UILite是一款继承自WTL以及和界面无关的功能库的合集,能够生成很小的可执行文件。如果你也象我一样希望自己的程序又小又快的话,UILite就是你的选择。当然,我们还要克服一些障碍:
1) ATL/WTL样式的模板类初看起来有点怪异
2) 没有类向导的支持,所以要手工处理所有的消息映射。
3) MSDN没有正式的文档支持,你现在看到的就是UILite的最权威的文档
4) 买不到参考书籍
5) 没有微软的官方支持, 因为这是我写的库
6) ATL/WTL的窗口与MFC的窗口有很大的不同,你所了解的有关MFC的知识并不全部适用与WTL。 UILite的界面部分基于WTL的。
从另一方面讲,UILite 也有它自身的优势:
1) 不需要学习或掌握复杂的文档/视图框架。
2) 具有WINDOWS的基本的界面特色,支持大部分WINDOWS或者MFC的窗口控件
3) 增强了一些WINDOWS/MFC的特性,比如对浏览器的支持,WEBBrowser支持了JS的调用和回调,方便编写和网页有互动的程序。
4) 可生成较小的可执行文件,得益于设计优良的WTL。
5) 你可以修正自己使用的UILite 中的错误(BUG)而不会影响其他的应用程序,因为UILite的界面部分全是用模版实现的,所以你看到的是所有的源码
6)DirectUI支持,DirectUI是指无窗口UI自绘界面,即界面上所有的控件都不是窗口,而是直接绘制出来的,UILite支持基于XML配置的动态DirectUI界面编程,这里实现了业务和界面分离,界面实现了布局和资源分离,资源实现了语言资源和非语言资源分离,对于开发者来说,这些资源的管理都不用关心,这些UILite都已经做好了,窗口部分实现了DirectUI和标准WINDOWS窗口混合使用,也就是开发这可以在DirectUI窗口中包含标准Windows窗口,听起来很神奇是吧,这些UILite也帮你做好了。
7)丰富的字符串处理,查找、替换、hash、字符集转换、反向字符串操作、内存字符串操作、字符串和数据(int,,long,double\rect,font,color)等等的无缝转换
8) 加密解密,支持绝大部分加密解密操作
9)压缩解压
10)URL操作,支持大部分网络协议(HTTP/FTP)等
11)SOCKET网络、UILite使用模版技术实现了网络的高度的可定制和高性能要求。
12)Event事件模型,UILite使用模版技术加上智能指针技术实现了数据的事件驱动模型,有效的解决了数据业务模块的高效智能通讯问题,实现了模块间的高内聚。
13)内存池\、对象池,解决了内存碎片问题
14)线程/线程池,让开发者不用再关心线程的强杀导致的种种问题,智能线程分配技术,有效的节省了系统线程开销。
15)轻量级的HTTP服务器支持
16)) XML支持,作者设计的XML解析器,是世界上解析最快的XML解析器,同时支持宽字符和窄字符两种字符,支持直接内存解析,支持大部分格式的XML文档,UILite的DirectUI就是基于Markup的XML解析器。
17) INI格式文件的解析支持,命令行解析的支持等
18)文件操作的封装,支持File、TextFile、MemoryFile、IndexFile等
19)辅助的统计相关支持,调试、日志、时间统计等。
20)时间的封装
21)进程间的通讯等
22) 数据库支持,支持各种数据库的访问,以及内嵌到程序的SQLite的封装
23) 脚本的支持,UILite支持UI的脚本交互,也可以支持非UI程序的脚本交互,支持javascript/vbs等脚本交互
24) 最低支持VC6及以上版本,非界面库支持跨平台实现。
在接下来的文章中,我将介绍UILite的各个方面,先从UI部分介绍。
这里贴出使用UILite的案例: