Browsed by
月份:2017年1月

IO 多路复用 — select 和 poll

IO 多路复用 — select 和 poll

本文为 高性能网络编程学习 系列文章的第2篇

下面我们使用select, poll, epoll来进行IO multiplexing 本文我们演示 select 和 poll做IO多路复用

select的用法查看man page就可以得到,这里只列出遇到的问题

  • nfds参数,表明要watch的最大fd的数字 举例说明,如果你要watch的文件描述符为24 — 31,那么你应该将nfds设置为32
  • 为了防止SIGPIPE信号终止程序运行,要讲SIGPIPE信号忽略掉

代码如下

进行到这里的时候遇到了一个很费解的问题,server端在读取数据的时候会收到recv(): connection reset by peer这样的错误,使用tcpdump抓包查看后,发现client关闭链接的时候(cli.Close)并没有发送FIN给server端,而是直接关闭了链接,这时server再请求读取client的数据或者发送数据给client就会收到cli发来的RST(connection reset by peer) 关于这个问题的具体产生原因,目前有一个原因

  • 当客户端关闭链接的时候内核network buffer内还有数据,则不会发送FIN,而是发送RST

其他的原因还在调查,待定

Linux Kernel Development Resources

Linux Kernel Development Resources

Here are some resources for digging into linux kernel development (Keep updating)

Books

  • Understanding Linux Kernel [ULK]
  • Linux Kernel Development [LKD]
  • Linux Driver Development [LDD]
  • Linux Kernel Module Programming Guide [LKMPG]
  • Linux in a Nutshell

 

Sites

 

ULK Chapter2 总结

ULK Chapter2 总结

本章的知识结构如下

地址转换过程

谈到内存地址,具体来讲有三种不同类型的地址,逻辑地址,线性地址,物理地址。

  • 逻辑地址对应内存的分段
  • 线性地址对应分页
  • 物理地址对应到硬件芯片上的内存单元的地址

MMU通过Segment Unit与Paging Unit两个硬件电路将一个逻辑地址转为物理地址, 具体转换过程如上代码段所示

下面对整个转换过程进行概述

段选择符

[0-1]RPL: 请求者特权级

[2]TI: Table Indicator, 指明是从GDT还是LDT中取出段描述符(Segment Descriptor)

[3-15]index:用来指定从GDT中第index项取出Segment Descriptor

段描述符

对段描述符的解释参考 [不是科普向?] RE: 从零开始的操作系统开发 第二集 中相应内容即可

Logical Addr ===> Linear Addr

逻辑地址的高16位为段选择符(Segment Selector)其余32位(或者64位)为偏移量(Offset)

流程如下

  1. 检查TI确定是从GDT(TI=0)还是从LDT(TI =1)中选择段描述符
  2. 从Segment Selector的index字段计算出段描述符的地址,计算方法 index * 8 (一个Segment Descriptor大小为8) 并与gdtr/ldtr中的内容相加得到Segment Descriptor
  3. 把逻辑地址的offset字段与Segment Descriptor中的Base字段相加,得到Linear Address

 

而Linux中的分段只是一个兼容性考虑,分段和分页的作用是重复的,分页能以更精细的粒度对内存进行管理,因而在Linux中分页是主要的手段,Linux分段中的用户代码段,数据段,内核代码段,数据段,都是以Base = 0 ,因而Linux下的逻辑地址和线性地址是一致的,(因为Base = 0 所以 Linear Address = Base + Offset = Offset) 即逻辑地址的偏移量和线性地址的值是相等的

 

Linear Addr ==> Physical Addr

至此我们已经得到了线性地址,下面需要将线性地址通过分页单元转为物理地址,下面以基本的分页模型对分页进行解释

80×86的常规分页结构为

页目录 页表 页框

如图

如图,Page Directory和PageTable的结构类似,都是由一些flag bit加上Field字段,Field字段表示页框的物理地址,如果是一个PageTable的话,那么这个页框就含有一页数据,Page Directory的话,页框内含有的是一个页的PageTable(页表的大小就是一个PageTable)

 

寻址的方式如下,首先根据CR3寄存器的内容,找到PageDirectory入口的物理地址,然后加上Linear Addr中的DIRECTORY,找到对应的PDE项,并根据此内容找到对应的Page Table的物理地址,并根据Linear Addr中的TABLE找到相应的Page即页框的物理地址,最后将此物理地址加上Linear Addr的OFFSET字段,最后得到物理地址

 

而在64位系统上,如果依旧采用这种最基本的分页结构的话,那么假设页框大小为4K,所以OFFSET位依旧为12bit,然后其余的52bit可以分给PT(Page Table的简称,下同)和PD(Page Directory的简称,下同),这样就会导致我们每个进程的页目录和页表变得非常非常多,超过256000项

因为这个原因,所以对于64位处理器的分页系统,都使用了更多级别的分页级别,如x86_64使用48位寻址,分页为4级线性地址分级为 9 + 9 + 9 + 9 + 12 如下图所示

原理都是一样的,只不过分页级别变多了

以上就是Logical Address ===> Physical Address的转换的大致流程

 

缓存技术

因为CPU Register的读取和内存的读取速度相差甚大,为了缩小这个差距,避免CPU等待过长的时间,使用缓存技术来将内存中的部分数据缓存在高速静态RAM(SRAM)里,即为高速缓存技术(Hardware Cache)

此外,将Logical Address转换为Physical Address也需要多次进行内存的读取(查找各级页表),为了加速此过程,引入了转换后援缓冲器Translation Lookaside Buffer(TLB)技术

Hardware Cache

对高速缓存的原理描述超出本文的范围,这里仅对高速缓存的读写方式进行一定说明:

在更新内存数据的时候,是同时更新高速缓存和内存的数据,还是仅仅更新缓存的数据,根据这个有两种不同的写策略

  • 通写(write-through): 既写RAM也写缓存
  • 回写(write-back): 只写缓存,只有当需要FLUSH的时候才更新RAM

操作系统可以针对不同的页框配置不同的告诉缓存管理策略,通过PCD位和PWT位

  • PCD表示是否对此页框启用告诉缓存
  • PWT表示是否采用write-through的策略

在Linux下,对全部的页框都启用告诉缓存,并且都采用write-back策略

TLB(快表)

TLB的作用是将Logical Address对应的Linear Address缓存起来,以加速对内存的访问

但是这个缓存是有失效期的,最明显的一个失效就是当PD都被替换了的时候,也就是cr3的寄存器内容更新了,硬件上,当cr3更新的时候,TLB的内容会自动失效

如何能够更好的利用TLB来加速访问是对Linux性能影响十分重要的一部分,为了避免多处理器系统上无用的TLB刷新,内核使用一种叫做Lazy TLB的技术,关于Lazy TLB技术的具体实现之后补充

不同的分页方式

上述已经对常规分页进行举例了,下面对不同的几种分页方式进行简单整理

扩展分页

80×86模型的另一种分页模式,页框大小为4MB而不是4KB,用于把大段连续的线性地址转换成相应的物理地址,因为没有了PT只有PD,节省了内存,同时PT—>Phy而不是PT–>PD–>Phy,少了一级转换效率也相应提高,

物理地址扩展(PAE)分页机制

早些时候,内存容量都很少,而当需要大内存(>4G)的服务器&程序出现的时候,上述的寻址方式就不能使用了,因而Intel将内存地址位数从32 –>36位(即将引脚从32个扩展到36个) 而之前的所有的地址转换都是从32位Linear Address转换到32位Physical Address的,需要有一种新的转换机制,将32bit linear addr->36bit phy addr,这种机制即为PAE机制,详情可以参考Intel手册 Vol3A相应的内容

Linux中的实现

对于Linux的具体处理将在之后在整理

 

 

 

将内核编译到自定义的目录下

将内核编译到自定义的目录下

参考wiki: https://wiki.archlinux.org/index.php/Kernels/Traditional_compilation

使用ArchLinux作为构建Linux的环境

方法如下 假设我们的customized directory为kernel-build

我们必须在kernel-tree目录结构下对内核进行编译,假设kernel-tree目录名为linux-kernel

下面是具体的操作:

在 make menuconfig 的时候,最好对内核版本进行修改,使之不会在make modules_install的时候覆盖掉现有内核的modules

修改途径为General Setup里的Local Version项

这样执行之后,应该在arch/ARCH/boot/下面存在bzImage内核镜像,并且modules应该被安装在了/lib/modules/<linux-version>-<localversion> 下

下一步操作, 修改linux.preset文件,并创建initrd文件

修改linux.preset文件, 修改完毕后的内容类似下面

我们需要修改ALL_kver, default_image, fallback_image三个选项,使其将image保存到指定的目录下

然后使用

创建initrd, initrd-fallback,目前kernel-build目录应该有如下文件:

然后使用

就可以运行我们自定义的Linux Kernel了,可以使用uname -a查看Kernel Version~

不过到这里我们仅仅完成了一个RAMDisk的Kernel,没有任何的文件系统被挂载,只有一个rescue filesystem和busybox的一些东西能使用,为了构建一个能够正常使用的Linux Image,包含一个Distro应该有的程序,下面将通过Linux From Scratch项目一步步构建一个可用的自己的Distro