深入理解计算机系统笔记6:系统级I/O

  输入/输出(I/O) : 是指主存外部设备(如磁盘,终端,网络)之间复制数据的过程。输入操作是从I/O设备复制数据到主存,而输出操作是从主存复制数据到I/O设备。

  所有I/O设备都被模型化为文件,而所有的输入和输出都被当做相应文件的读和写。这种将设备优雅地映射成文件,允许Unix内核引出一个简单,低级的应用接口,叫做Unix I/O。这使得所有的输入输出都能以一种统一且一致的方式来执行。

  所有高级语言的运行时(runtime)都提供了执行I/O功能的机制。例如,C语言中提供了包含像printf()和scanf()等这样的标准I/O库函数,C++语言中提供了如 <<(输入)和>>(输出)这样的重载操作符。从高级语言程序中通过I/O函数或I/O操作符提出I/O请求,到设备响应并完成I/O请求,涉及到多层次I/O软件和I/O硬件的协作。 I/O子系统也采用层次结构。  

  各类用户的I/O请求需要通过某种方式传给OS:

  • 最终用户:键盘、鼠标通过操作界面传递给OS
  • 用户程序:通过函数(高级语言)转换为系统调用传递给OS

  I/O软件被组织成从高到低的四个层次,层次越低,则越接近设备而越远离用户程序。这四个层次依次为:

  1. 用户层I/O软件(I/O函数调用系统调用)
  2. 与设备无关的操作系统I/O软件
  3. 设备驱动程序
  4. I/O中断处理程序

  大部分I/O软件都属于操作系统内核态程序,最初的I/O请求在用户程序中提出。从用户I/O软件切换到内核I/O软件的唯一办法是“异常”机制:系统调用(自陷) 。

用户I/O软件

  用户软件可用以下两种方式提出I/O请求:

  (1)使用高级语言提供的标准I/O库函数。例如,在C语言程序中可以直接使用像fopen、fread、fwrite和fclose等文件操作函数,或printf、putc、scanf和getc等控制台I/O函数。 程序移植性很好!

但是,使用标准I/O库函数有以下几个方面的不足:

  • 标准I/O库函数不能保证文件的安全性(无加/解锁机制)
  • 所有I/O都是同步的,程序必须等待I/O操作完成后才能继续执行(串行)
  • 有些I/O功能不适合甚至无法使用标准I/O库函数实现,如,不提供读取文件元数据的函数(元数据包括文件大小和文件创建时间等)
  • 用它进行网络编程会造成易于出现缓冲区溢出等风险

  (2)使用OS提供的API函数或系统调用。例如,在Windows中直接使用像CreateFile、ReadFile、WriteFile、CloseHandle等文件操作API函数,或ReadConsole、WriteConsole等控制台I/O的API函数。对于Unix或Linux用户程序,则直接使用像open、read、write、close等系统调用封装函数。

例子,用户进程请求读磁盘文件操作

用户进程使用标准C库函数fread,或Windows API函数ReadFile,或Unix/Linux的系统调用函数read等要求读一个磁盘文件块。

用户程序中涉及I/O操作的函数最终会被转换为一组与具体机器架构相关的指令序列,这里我们将其称为I/O请求指令序列。例如,若用户程序在IA-32架构上执行,则I/O函数被转换为IA-32的指令序列。

每个指令系统中一定有一类陷阱指令(有些机器也称为软中断指令或系统调用指令),主要功能是为操作系统提供灵活的系统调用机制。

在I/O请求指令序列中,具体I/O请求被转换为一条陷阱指令,在陷阱指令前面则是相应的系统调用参数的设置指令。

IA-32/Linux的系统调用

  通常,系统调用被封装成用户程序能直接调用的函数,如exit()、read()和open(),这些是标准C库中系统调用对应的封装函数

  Linux中系统调用所用参数通过寄存器传递,传递参数的寄存器顺序依次为:EAX(调用号)、EBX、ECX、EDX、ESI、EDI和EBP,除调用号以外,最多6个参数。

  封装函数对应的机器级代码有一个统一的结构: 总是若干条传送指令后跟一条陷阱指令。传送指令用来传递系统调用的参数,陷阱指令(如int $0x80)用来陷入内核进行处理。

  例如,若用户程序调用系统调用write(1, “hello, world!\n”,14),将字符串“hello, world!\n”中14个字符显示在标准输出设备文件stdout上,则其封装函数对应机器级代码(用汇编指令表示)如下:

movl  $4, %eax           //调用号为4,送EAX
movl  $1, %ebx   //标准输出设备stdout的文件描述符为1,送EBX
movl  $string, %ecx  //字符串“hello, world!\n”首址送ECX
movl  $14, %edx //字符串的长度为14,送EDX
int    $0x80  //系统调用

系统I/O软件

  OS在I/O子系统中的重要性由I/O系统以下三个特性决定:

(1)共享性。I/O系统被多个程序共享,须由OS对I/O资源统一调度管理,以保证用户程序只能访问自己有权访问的那部分I/O设备,并使系统的吞吐率达到最佳。

(2)复杂性。I/O设备控制细节复杂,需OS提供专门的驱动程序进行控制,这样可对用户程序屏蔽设备控制的细节。

(3)异步性。不同设备之间速度相差较大,因而,I/O设备与主机之间的信息交换使用异步的中断I/O方式,中断导致从用户态向内核态转移,因此必须由OS提供中断服务程序来处理。

  那么,如何从用户程序对应的用户进程进入到操作系统内核执行呢? 通过系统调用!如:INT $0x80

系统调用和API

  OS提供一组系统调用,为用户进程的I/O请求进行具体的I/O操作。

° 应用编程接口(API)与系统调用两者在概念上不完全相同,它们都是系统提供给用户程序使用的编程接口,但前者指的是功能更广泛、抽象程度更高的函数,后者仅指通过软中断(自陷)指令向内核态发出特定服务请求的函数。

° 系统调用封装函数是 API 函数中的一种。

° API 函数最终通过调用系统调用实现 I/O。一个API 可能调用多个系统调用,不同 API 可能会调用同一个系统调用。但是,并不是所有API 都需要调用系统调用。

° 从编程者来看,API 和 系统调用之间没有什么差别。

° 从内核设计者来看,API 和 系统调用差别很大。API 在用户态执行,系统调用封装函数也在用户态执行,但具体服务例程在内核态执行。

  系统调用(陷阱)是特殊异常事件,是OS为用户程序提供服务的手段。Linux提供了几百种系统调用,主要分为以下几类:进程控制、文件操作、文件系统操作、系统控制、内存管理、网络管理、用户管理、进程通信等。系统调用号是系统调用跳转表索引值,跳转表给出系统调用服务例程首址。

系统调用及其参数传递

  在用户态,当进程调用一个系统调用时,CPU切换到内核态,并开始执行一个被称为系统调用处理程序的内核函数 例如,IA-32中,可以通过两种方式调用Linux的系统调用

•执行软中断指令int $0x80
•执行指令sysenter(老的x86不支持该指令)

  内核实现了许多系统调用,因此,用一个系统调用号(存放在EAX中)来标识不同的系统调用。除了调用号以外,系统调用还需要其他参数,不同系统调用所需参数的个数和含义不同,输入参数通过通用寄存器传递,若参数个数超出寄存器个数,则将需传递参数块所在内存区首址放在寄存器中传递(除调用号以外,最多6个参数)传递参数的寄存器顺序:EAX(系统调用号)、EBX、ECX、EDX、ESI、EDI和EBP。 返回参数为整数值。正数或0表示成功,负数表示出错码

  用户程序总是通过某种I/O函数或I/O操作符请求I/O操作。例如,读一个磁盘文件记录时,可调用C标准I/O库函数fread(),也可直接调用系统调用封装函数read()来提出I/O请求。不管是C库函数、API函数还是系统调用封装函数,最终都通过操作系统内核提供的系统调用来实现I/O。

Linux系统中printf()函数的执行过程:

° 某函数调用了printf(),执行到调用printf()语句时,便会转到C语言I/O标准库函数printf()去执行;
° printf()通过一系列函数调用,最终会调用函数write();
° 调用write()时,便会通过一系列步骤在内核空间中找到write对应的系统调用服务例程sys_write来执行。
在system_call中如何知道要转到sys_write执行呢? 根据系统调用号!

image

Linux系统下的write()封装函数

  用法:ssize_t write(int fd, const void * buf, size_t n);

  size_t 和 ssize_t 分别是 unsigned int 和 int,因为返回值可能是-1。

1 write:
2     pushl  %ebx             //将EBX入栈(EBX为被调用者保存寄存器)
3     movl  $4, %eax         //将系统调用号 4 送EAX
4     movl  8(%esp), %ebx     //将文件描述符 fd 送EBX
5     movl  12(%esp), %ecx     //将所写字符串首址 buf 送ECX
6     movl  16(%esp), %edx     //将所写字符个数 n 送EDX
7     int $0x80             //进入系统调用处理程序system_call执行
8     cmpl  $-125, %eax     //检查返回值(所有正数都小于FFFFFF83H)
9     jbe  .L1                 //若无错误,则跳转至.L1(按无符号数比)
10   negl  %eax             //将返回值取负送EAX
11   movl   %eax, error       //将EAX的值送error
12   movl   $-1, %eax         //将write函数返回值置-1
13 .L1:                 内核执行write的结果在EAX中返回,正确时
14    popl   %ebx        为所写字符数(最高位为0),出错时为错误
15    ret                码的负数(最高位为1)

  用户程序中的I/O函数,C标准I/O库函数比系统调用封装函数抽象层次高,后者属于系统级I/O函数。与系统提供的API函数一样,前者是基于后者实现的。

image

文件

  所有I/O操作通过读写文件实现,所有外设,包括网络、终端设备,都被看成文件。 所有物理设备抽象成逻辑上统一的“文件”使得用户程序访问物理设备与访问真正的磁盘文件完全一致。例如,fprintf/fwrite(主要是磁盘文件) 和printf (stdout) 都通过统一的write函数陷入内核,差别则由内核处理!

  UNIX系统中,文件就是一个字节序列。 通常,将键盘和显示器构成的设备称为终端(terminal),对应标准输入、和标准(错误)输出文件;像磁盘、光盘等外存上的文件则是普通文件 。

  根据文件的可读性,文件被分成ASCII文件和二进制文件两类。 ASCII文件也称文本文件,可由多个正文行组成,每行以换行符‘\n’ 结束,每个字符占一个字节。标准输入和标准(错误)输出文件是ASCII文件。 普通文件可能是文本文件或二进制文件。

文件的创建和打开

文件的读/写

文件的定位和关闭

读取目录内容

共享文件

I/O重定向

用RIO包健壮地读写

标准I/O流 某种意义上而言是全双工的,因为程序能够在同一个上执行输入输出。

然而,对的限制和对套接字的限制,有时会互相冲突

  • 限制一: 跟在输出函数之后的输入函数。
    • 如果中间没有插入对fflush,fseek,fsetpos或者rewind的调用,一个输入函数不能跟在输出函数之后。
      • fflush函数清空与流相关的缓冲区。
      • 后三个函数使用Unix I/Olseek函数来重置当前的文件位置。
  • 限制二: 跟在输入函数之后的输出函数。
    • 如果中间没有插入对fseek,fsetpos或者rewind的调用,一个输出函数不能跟随在一个输入函数之后,除非该输入函数遇到了一个`EOF。

image

Unix I/O 是最通用最底层的 I/O 方法,其他的 I/O 包都是在 Unix I/O 的基础上进行构建的,值得注意的一点是,Unix I/O 中的方法都是异步信号安全(async-signal-safe)的,也就是说,可以在信号处理器中调用。因为比较底层和基础的缘故,需要处理的情况非常多,很容易出错。高效率的读写需要用到缓冲区,同样容易出错,这也就是标准 C 库着重要解决的问题。

标准 C I/O 提供了带缓存访问文件的方法,使用的时候几乎不用考虑太多,但是如果我们想要得到文件的元信息时,就还是得使用 Unix I/O 中的 stat 函数。另外标准 C I/O 中的函数都不是异步信号安全(async-signal-safe)的,所以并不能在信号处理器中使用。最后,标准 C I/O 不适合用于处理网络套接字。

I/O端口的寻址方式

  • 对I/O端口读写就是向I/O设备送出命令或从设备读状态或读/写数据
  • 一个I/O控制器可能会占有多个端口地址
  • I/O端口必须编号后,CPU才能访问它
  • I/O设备的寻址方式就是I/O端口的编号方式

(1)统一编址方式(内存映射方式)

与主存空间统一编址,主存单元和I/O端口在同一个地址空间中。(将I/O端口映射到某个主存地址区域,故也称“存储器映射方式”)例如,RISC机器、Motorola公司的处理器等采用该方案,VRAM(显示存储器)通常也和主存统一编址

(2)独立编址方式(特殊I/O指令方式)

单独编号,不和主存单元一起编,使成为一个独立的I/O地址空间。(因为需专门I/O指令,故也称为“特殊I/O指令方式”)例如,Intel处理器就是独立编址方式

驱动程序与I/O指令

° 控制外设进行输入/输出的底层I/O软件是驱动程序

° 驱动程序设计者应了解设备控制器及设备的工作原理,包括:设备控制器中有哪些用户可访问的寄存器、控制/状态寄存器中每一位的含义、设备控制器与外设之间的通信协议等,而关于外设的机械特性,程序员则无需了解。驱动程序通过访问I/O端口控制外设进行I/O:
• 将控制命令送到控制寄存器来启动外设工作;
• 读取状态寄存器了解外设和设备控制器的状态;
• 访问数据缓冲寄存器进行数据的输入和输出。

° 对I/O端口的访问操作由I/O指令完成,它们是一种特权指令

° IA-32中的I/O指令:in、ins、out和outs
• in和ins用于将I/O端口的内容取到CPU内的通用寄存器中;
• out和outs用于将通用寄存器内容输出到I/O端口。
如 IN AL, DX:DX中存放I/O端口地址,将I/O端口中的内容取到AL中

三种基本I/O方式

  • 程序直接控制方式(最简单的I/O方式)

• 无条件传送:对简单外设定时(同步)进行数据传送
• 条件传送:CPU主动查询,也称程序查询或轮询(Polling)方式

特点:
• 简单、易控制、外围接口控制逻辑少;
• CPU与外设串行工作,效率低、速度慢,适合于慢速设备
• 查询开销极大 (CPU完全在等待“外设完成”)
工作方式:完全串行或部分串行,CPU用100%的时间为I/O服务!

  • I/O Interrupt (中断I/O方式): 几乎所有系统都支持中断I/O方式

• 若一个I/O设备需要CPU干预,它就通过中断请求通知CPU
• CPU中止当前程序的执行,调出OS(中断处理程序)来执行
• 处理结束后,再返回到被中止的程序继续执行

  • Direct Memory Access (DMA方式): 磁盘等高速外设所用的方式

• 磁盘等高速外设成批地直接和主存进行数据交换
• 需要专门的DMA控制器控制总线,完成数据传送
• 数据传送过程无需CPU参与

内核空间I/O软件

所有用户程序提出的I/O请求,最终都通过系统调用实现,通过系统调用封装函数中的陷阱指令转入内核I/O软件执行,内核空间I/O软件实现相应系统调用的服务功能。

内核空间的I/O软件分三个层次
• 设备无关软件层
• 设备驱动程序层
• 中断服务程序层

设备驱动程序层、中断服务程序层与I/O硬件密切相关

image

设备无关I/O软件层

设备驱动程序统一接口
• 操作系统为所有外设的设备驱动程序规定一个统一接口,这样,新设备的驱动程序只要按统一接口规范来编制,就可在不修改操作系统的情况下,添加新设备驱动程序并使用新的外设进行I/O。
• 所有设备都抽象成文件,设备名和文件名在形式上没有差别,设备和文件具有统一的接口,不同设备名和文件名被映射到对应设备驱动程序。
缓冲处理
• 每个设备的I/O都需使用内核缓冲区,因而缓冲区的申请和管理等处理是所有设备公共的,可包含在与设备无关的I/O软件部分错误报告
• I/O操作在内核态执行时所发生的错误信息,都通过与设备无关的I/O软件返回给用户进程,也即:错误处理框架与设备无关。
• 直接返回编程等错误,无需设备驱动程序处理,如,请求了不可能的I/O操作;写信息到一个输入设备或从一个输出设备读信息;指定了一个无效缓冲区地址或者参数;指定了不存在的设备等。
• 有些错误由设备驱动程序检测出来并处理,若驱动程序无法处理,则将错误信息返回给设备无关I/O软件,再由设备无关I/O软件返回给用户进程,如写一个已被破坏的磁盘扇区;打印机缺纸;读一个已关闭的设备等。

打开与关闭文件

• 对设备或文件进行打开或关闭等I/O函数所对应的系统调用,并不涉及具体的I/O操作,只要直接对主存中的一些数据结构进行修改即可,这部分工作也由设备无关软件来处理。
逻辑块大小处理
• 为了为所有的块设备和所有的字符设备分别提供一个统一的抽象视图,以隐藏不同块设备或不同字符设备之间的差异,与设备无关的I/O软件为所有块设备或所有字符设备设置统一的逻辑块大小。
• 对于块设备,不管磁盘扇区和光盘扇区有多大,所有逻辑数据块的大小相同,这样,高层I/O软件就只需处理简化的抽象设备,从而在高层软件中简化了数据定位等处理 。

设备驱动程序

每个外设具体的I/O操作需通过执行设备驱动程序来完成
外设种类繁多、其控制接口不一,导致不同外设的设备驱动程序千差万别,因而设备驱动程序与设备相关
每个外设或每类外设都有一个设备控制器,其中包含各种I/O端口。 CPU通过执行设备驱动程序中的I/O指令访问各种I/O端口
设备所采用的I/O控制方式不同,驱动程序的实现方式也不同
• 程序直接控制:驱动程序完成用户程序的I/O请求后才结束。这种情况下,用户进程在I/O过程中不会被阻塞,内核空间的I/O软件一直代表用户进程在内核态进行I/O处理 。(干等!)
• 中断控制:驱动程序启动第一次I/O操作后,将调出其他进程执行,而当前用户进程被阻塞。在CPU执行其他进程的同时,外设进行I/O操作,此时,CPU和外设并行工作。外设完成I/O时,向CPU发中断请求,然后CPU调出相应中断服务程序执行。在中断服务程序中再次启动I/O操作。
• DMA控制:驱动程序对DMA控制器初始化后,便发送“启动DMA传送”命令,外设开始进行I/O操作并在外设和主存间传送数据。同时CPU执行处理器调度程序,转其他进程执行,当前用户进程被阻塞。DMA控制器完成所有I/O任务后,向CPU发送一个“DMA完成”中断请求信号。

中断服务程序

° 中断控制和DMA控制两种方式下都需进行中断处理
° 中断控制方式:中断服务程序主要进行从数缓器取数或写数据到数缓器,然后启动外设工作
° DMA控制方式:中断服务程序进行数据校验等后处理工作

I/O处理详细内容参考操作系统教材

参考:csapp第十章,袁春风计算机系统基础课程及教材

posted on 2018-05-20 21:04  flysong  阅读(668)  评论(0编辑  收藏  举报

导航