21

记一次网络读过程(简化版)

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzA4NzQ5OTkxNw%3D%3D&%3Bmid=2247483837&%3Bidx=1&%3Bsn=1ee92179fe4242446be5f44d966bb366
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

对于科学来说,重要的是探索,而令科学拥有重大意义的是理解。

——《理性的边界》诺桑·亚诺夫斯基

1、序言

平时接触到的网络程序比较多,但是表面上直接接触的最多是编程语言库封装的一些函数,大多数开发同学对函数库的内部实际运行的机制并不了解,在不了解的情况下就难以敬畏,本文以一次网络读为例将系统调用、内核态用户态、网络模型、硬中断、软中断等知识串联起来,但是重点是知识的衔接,涉及到各部分的细节本次不会展开,等后续有机会再做详述。

2、write和read函数

编写网络程序,最常用的函数是莫过于read和write了,比如redis中就用这两个函数进行网络数据的读写的:

// networking.c

// 写

nwritten = write(fd, o->buf + c->sentlen, objlen - c->sentlen);

// 读

nread = read(fd, c->querybuf+qblen, readlen);

3、硬中断和软中断

在介绍系统调用前,我们先看看什么是中断,中断本质上是一种特殊的电信号,由外部发送给处理器,处理器接收到中断后,会马上向操作系统反映此信号的到来,然后就由操作系统回调之前准备好的中断处理程序,中断处理程序通俗点讲就是一种回调函数。中断分为硬中断和软中断,他们最大的区别在于中断是由硬件发起的还是软件发起的。

中断解决什么问题呢?对于硬中断来说,硬中断是由硬件向处理器发起的,目的在于通知处理器“你交给我的任务我完成了”,为什么以这种方式协作呢?简单的讲就是硬件慢,而处理器快,处理器交给硬件任务之后不能一直等吧,这样多傻,因此处理器和硬件之间的协作方式是异步的,处理器不会等待硬件,由硬件完成任务之后通知处理器。

对于软中断来说,整个过程类似,只是中断发起方式应用程序而不是硬件,当应用程序希望调用系统调用的时候,会向处理器发起软中断指令和中断号,所有系统调用对应的软中断号都是0x80,0x80这个软中断号对应的中断处理程序是系统调用的公共的入口程序,内核会根据应用程序传递过来的具体的系统调用号来分发至具体的系统调用程序来处理,具体下文会介绍。

  4、系统调用

我们再看看什么是系统调用?这要从linux操作系统的“双态”讲起,Linux操作系统的体系架构分为用户态和内核态,内核从本质上看是一种软件,控制计算机的硬件资源,比如:CPU资源、存储资源、I/O资源等。用户态即上层应用程序的活动空间,处于安全考虑,上层应用层不能够直接访问这些资源,必须依托于内核才能访问,为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口,这些访问接口即系统调用。应用程序每次调用系统调用,都要通过“软中断”来实现用户态到内核态的切换,那么什么是“软中断”呢?

e2QBRfE.png!web

图1 Unix/Linux的体系结构

5、read函数实现

以read函数库实现为例,read函数具体实现会将系统调用号和参数放入寄存器,然后调用int 0x80指令发起软中断,如下示例所示:

; NASM

; read(int fd, void *buffer, size_t nbytes)

mov eax, 3 // read系统调用号为3

mov ebx, fd

mov ecx, buffer

mov edx, nbytes

int 0x80

6、系统调用中断处理程序

在操作系统启动的时候就会注册软中断处理程序,如下:

// traps.c

void __init trap_init(void)

{

set_system_gate(IA32_SYSCALL_VECTOR, ia32_syscall);//注册软中断号为0x80的处理程序为ia32_syscall,也就是当发起int 0x80指令的时候,都会回调ia32_syscall

cpu_init();

}

// hw_irq.h

#define IA32_SYSCALL_VECTOR 0x80

当应用程序向处理器发起int 0x80指令的时候,内核就会回调上面注册的系统调用处理程序,系统调用处理程序如下,是由汇编语言编写的:

ENTRY(ia32_syscall)

swapgs //修改运行级别

sti // 开中断

movl %eax,%eax

pushq %rax

cld

/* note the registers are not zero extended to the sf.

this could be a problem. */

SAVE_ARGS

GET_THREAD_INFO(%r10)

bt $TIF_SYSCALL_TRACE,threadinfo_flags(%r10)

jc ia32_tracesys

ia32_do_syscall:

cmpl $(IA32_NR_syscalls),%eax

jae ia32_badsys

IA32_ARG_FIXUP

call *ia32_sys_call_table(,%rax,8) # xxx: rip relative // 此处调用系统调用的处理例程

movq %rax,RAX-ARGOFFSET(%rsp)

jmp int_ret_from_sys_call

这里是系统调用的公共入口,具体哪个系统调用会基于ia32_sys_call_table来做分发,从而执行具体的系统调用,注意这里可以看到用户态到内核态的切换是涉及上下文的切换的,具体会涉及一些列保存和恢复现场的工作,这也能解释为什么大家经常会说上下文切换会影响性能,每次系统调用会设计用户态->内核态和内态核->用户态的切换。

7、read系统调用的几种I/O模式

下图表明了read典型的执行过程,read系统调用的任务是将数据通过copy_to_user从内核空间拷贝到用户空间:

6zIFJjm.png!web

图2 read执行过程

read过程可以有不同的策略,不同的策略对应着大家经常说的I/O模型,具体如下:

1、阻塞读: 如果输入缓冲区有数据,那么read会读取并返回,如果输入缓冲区没有数据,那么默认情况下read必须阻塞等待,直到至少有一个字节达到,这种就是阻塞模式;

2、非阻塞读: 对于设置了O_NONBLOCK的情况,read即使没有读到数据,也会立即返回,使用非阻塞读的应用程序通常也使用poll、select、epoll系统调用,这种就是非阻塞模式;

3、异步模式: 以上两种情况都要应用程序主动发起,比如以轮训的方式来读数据,有没有更好的方案呢?设备驱动层面可以提供异步通知的机制,应用程序可以在数据可用时收到一个信号,而不需要不停地使用轮训来关注数据,基于异步通知机制,就可以实现异步I/O模型,异步I/O模型是一种更聪明的方式;

8、中断驱动的IO程序

驱动程序中一般会有缓冲区,用于保存从硬件接收的数据,那么缓冲区的数据是哪里来的呢?前面硬中断讲过,对于输入来说,硬件准备好数据会通过中断通知处理器,由处理器来回调中断处理程序来将数据读到缓冲区中,缓存区的数据是由中断处理程序从硬件读取的,当然这只是理论上的模型,实际动作取决于设备使用的是I/O端口、内存映射还是DMA。

9、总结

本文通过一次read过程,串联了系统调用、内核态用户态、网络模型、硬中断、软中断等知识,其中过程包括:

1、read和write函数: 开发同学最常用的编程函数;

2、硬中断和软中断及系统调用: 中断解决的问题和形式以及系统调用的背景知识;

3、read函数的实现和系统调用中断处理程序: 系统调用需要基于软中断发起,涉及用户态到内核态的切换,系统调用入口是一种中断处理程序;

4、read系统调用的几种I/O模式: 读数据的过程采取不同的策略形成了不同的I/O模型,核心实现是将数据从内核缓存区拷贝到用户缓冲区;

5、中断驱动的IO程序: 数据从硬件拷贝到内核缓冲区;

本文侧重知识的串联,每个环节的具体实现和关注点本文并没有阐述,后续有机会再做详细分享,谢谢。

10、参考文献

jIRJR32.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK