Unix/Linux编程实践教程阅读笔记-who指令实现的优化-来自第二章P48-P54的笔记
在读它之前,需要先掌握who指令的基本实现原理,参考我之前的文章:
https://www.cnblogs.com/czw52460183/p/10999434.html
怎么优化它的实现?答案是利用缓冲区。
在讲述具体方法之前,需要补充一点缓冲区的前置知识:
我们平时讲的缓冲区,基本指的是进程缓冲区,它位于用户空间,但其实,为了提高效率,内核也会使用缓冲区,这就是内核缓冲区。
当进程要从磁盘读取数据时,按照之前的讲法,就会直接进行系统调用,切换到内核态并从磁盘读取数据。但其实内核一般不直接读磁盘,而是将内核缓冲区的数据复制到进程的缓冲区中。
当进程请求的数据不在内核缓冲区时,内核把对应的数据块加入到请求列表中,随后挂起该进程,并为其它进程服务。很短时间过去后,内核把对应的数据块从磁盘读到内核缓冲区,随后把数据从内核缓冲区复制到进程缓冲区,并唤醒被挂起的进程。
之前在CSAPP笔记的第九章中(链接:https://www.cnblogs.com/czw52460183/p/10865447.html),我们展示过虚拟地址空间的结构图。
那么这个内核缓冲区位于虚拟地址空间的哪个部分呢?堆区?栈区?
我并没有查到相关资料描述其具体位置,但我的推测是:内核缓冲区属于内核,那它应该是存在于进程虚拟地址空间中属于内核的那一部分。还记得吗?之前说过每个进程虚拟地址空间的最上方,就是栈的上方,存放的是内核常驻与内存的部分,里面有内核栈的,我认为这个内核缓冲区对应的是这块地方。当然,只是猜测,如果以后发现有不对的地方,我再来纠正。
当内核缓冲区的数据积累够一定数量后,才会一次写入磁盘,在这期间万一断电了,数据会丢失。
附上一份内核缓冲区的参考文章:https://www.zhihu.com/question/30868347?sort=created
现在介绍下进程缓冲区,即用户缓冲区,在https://www.cnblogs.com/czw52460183/p/10999434.html中,我们提到过,fgetc函数的缓冲是基于用户空间的,其实它就是用了用户缓冲区作了优化比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。
而之前我们提到read函数是底层的系统调用,它的缓冲是基于内核空间的,相当于它有内核缓冲区,但它没有用户空间的缓冲机制。
进一步了解可参考文章:https://www.cnblogs.com/NeilHappy/archive/2013/03/12/2955552.html
在之前的文章中(https://www.cnblogs.com/czw52460183/p/10999434.html),我们在实现who指令时发现Mac下对读取utmpx作了封装,直接读取该文件并解析会出现乱码。虽然不知道为什么它会做这样的封装,但我们可以推测一下:我认为它可能是为了通过封装实现一个基于用户缓冲的优化。
由于read没有用户缓冲机制,因此直接用read读取utmpx需要对结构体数组中的每一个结构体单独读取,我们可以在用户空间构造一片缓冲区,每次read从内核缓冲区读取多个utmpx结构体,存放在用户缓冲区中,随后封装一个方法,为外部提供下一个结构体,当数据被取完,则再次调用read。
由于Mac下极有可能已经对读取utmpx作了封装,所以我们无法验证这个优化缓冲机制了,这里给一下书上的代码吧,我也懒得装Linux去尝试了,有条件的自己试吧。
#include <stdio.h> #include <fcntl.h> #include <sys/types.h> #include <utmp.h> //缓冲区参数 #define NRECS 16 #define UTSIZE (sizeof(struct utmp)) //结构化的返回参数 #define NULLUT ((struct utmp*)NULL) //用户缓冲区 static char utmpbuf[NRECS * UTSIZE]; //缓冲区实际存储的utmp结构体数量 static int num_recs; //缓冲区中下一个要读取的utmp结构体序号 static int cur_rec; //文件描述符 static int fd_utmp; //打开文件并初始化读取序号 int utmp_open(char *filename) { fd_utmp = open(filename,O_RDONLY); cur_rec = num_recs = 0; return fd_utmp; } //加载下一批结构体到缓冲区,并返回实际加载的数量 int utmp_reload() { //实际读取字节数 int amt_read; amt_read = read(fd_utmp,utmpbuf,NRECS*UTSIZE); //更新实际读取到的结构体数量 num_recs = amt_read/UTSIZE; //重置当前要读取的结构体序号 cur_rec = 0; return num_recs; } //获取下一个utmp结构体 struct utmp *utmp_next() { struct utmp *recp; //打开文件出错 if(fd_utmp == -1) { return NULLUT; } //当缓冲区读完且加载不到新结构体时,返回空 if(cur_rec == num_recs && utmp_reload() == 0) { return NULLUT; } //从缓冲区获取下一个结构体 recp = (struct utmp *) &utmpbuf[cur_rec * UTSIZE]; //更新下一个要读取结构体序号 cur_rec++; return recp; } //关闭文件 void utmp_close() { //只有文件被打开成功才需要关闭 if(fd_utmp != -1) { close(fd_utmp); } }
这个文件实现了对获取下一个utmp结构体的缓冲区优化封装,配合上一章写的main函数使用,稍微修改下即可,main函数就不贴出来了。