操作系统-I/O(6)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软件被组织成从高到低的四个层次,层次越低,则越接近设备而越远离用户程序。这四个层次依次为:
(1)    用户层I/O软件(I/O函数调用系统调用)
(2)    与设备无关的操作系统I/O软件   此层开始属于OS,OS在I/O系统中极其重要
(3)    设备驱动程序
(4)    I/O中断处理程序
大部分I/O软件都属于操作系统内核态程序,最初的I/O请求在用户程序中提出。从用户I/O软件切换到内核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提供中断服务程序来处理。
 
因此,各类用户的I/O请求需要通过某种方式传给OS:
  • 最终用户:键盘、鼠标通过操作界面传递给OS
  • 用户程序:通过函数(高级语言)转换为系统调用传递给OS
 
系统调用(陷阱)是特殊异常事件,是OS为用户程序提供服务的手段。
OS提供一组系统调用,为用户进程的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(printf最终也是调用了write)、close等系统调用封装函数(系统级I/O函数)。
注意点:C标准库中提供的函数并没有涵盖所有底层操作系统提供的功能;不同的C标准库函数可能调用相同的系统调用;此外,C标准I/O库函数、UNIX/Linux 和Windows的API函数所提供的I/O操作功能并不是一一对应的。
例如,它们的参数中对文件的标识方式不同:函数read() 和write() 的参数中指定的文件用一个整数类型的文件描述符来标识;而C 标准库函数fread()和fwrite() 的参数中指定的文件用一个指向特定结构的指针类型来标识。
 
API与系统调用有什么区别?
应用编程接口(API)与系统调用两者在概念上不完全相同,它们都是系统提供给用户程序使用的编程接口,但前者指的是功能更广泛、抽象程度更高的函数,后者仅指通过软中断(自陷)指令向内核态发出特定服务请求的函数。
系统调用封装函数是 API 函数中的一种。
API 函数最终通过调用系统调用实现 I/O。一个API 可能调用多个系统调用,不同 API 可能会调用同一个系统调用。但是,并不是所有API 都需要调用系统调用。
从编程者来看,API和系统调用之间没有什么差别。
从内核设计者来看,API 和系统调用差别很大:API 在用户态执行, 系统调用封装函数也在用户态执行,但具体服务例程在内核态执行。
 
用户程序总是通过某种I/O函数或I/O操作符请求I/O操作。
例如,用户进程读一个磁盘文件记录时,可调用C标准I/O库函数fread(),或Windows API函数ReadFile,或Unix/Linux的系统调用封装函数read()来提出I/O请求。不管是C库函数、API函数还是系统调用封装函数,最终都通过操作系统内核提供的系统调用来实现I/O。
即,用户程序中涉及I/O操作的函数最终会被转换为一组与具体机器架构相关的指令序列,这里我们将其称为I/O请求指令序列。
每个指令系统中一定有一类陷阱指令(有些机器也称为软中断指令或系统调用指令),主要功能是为操作系统提供灵活的系统调用机制。
在I/O请求指令序列中,具体I/O请求被转换为这条陷阱指令,在陷阱指令前面则是相应的系统调用参数的设置指令。
当CPU 执行到系统这条陷阱指令时, 会从用户态陷入到内核态;转到内核态执行后, CPU 根据陷阱指令执行时EAX 寄存器中的系统调用号,选择执行一个相应的系统调用服务例程;
在系统调用服务例程的执行过程中可能需要调用具体设备的 驱动程序;在设备驱动程座执行过程中启动外设工作,外设准备好后发出中断请求,CPU响应中断后,就调出中断服务程序执行,在中断服务程序中控制主机与设备进行具体的数据交换。
标准I/O库函数比系统调用封装函数抽象层次高,后者属于系统级I/O函数。与系统提供的API函数一样,前者是基于后者实现的。
两者关系如图所示:
printf()函数的调用过程如下:
以下是write封装函数的原型:
ssize_t write(int fd, const void * buf, size_t n);
//fd是文件描述符,每个文件用一个int型的文件描述符来标示文件
//buf是要写的字符串的首地址。buf是void指针,可以通过强制类型转换变成任何类型的指针
//n是要写的字符个数,size_t是unsigned int
//返回值是真正写的字符个数,ssize_t是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    //检查返回值,假定最大出错码为131(所有正数都小于FFFFFF83H)
9  jbe .L1              //若无错误,则跳转至.L1(按无符号数比)
10 negl   %eax          //将返回值取负送EAX
11 movl   %eax, error   //将EAX的值送error
12 movl   $-1, %eax     //将write函数返回值置-1
13 .L1:
14 popl   %ebx
15 ret
Linux 中有一个系统调用的统一人口,即系统调用处理程序system_call()。CPU 执行陷阱指令后,便转到system_call()的第一条指令执行。
进入system_call后根据调用号是4跳转到sys_read服务例程,然后在Linux内核中单向调用20次以上。
内核执行write的结果在EAX中返回,正确时为所写字符数(最高位为0),出错时为错误码的负数(最高位为1)
某函数调用了printf(),执行到调用printf()语句时,便会转到C语言I/O标准库函数printf()去执行;
printf()通过一系列函数调用,最终会调用函数write(); 
调用write()时,便会通过一系列步骤在内核空间中找到write对应的系统调用服务例程sys_write来执行。
在system_call中根据系统调用号知道要转到sys_write执行。

猜你喜欢

转载自www.cnblogs.com/yangyuliufeng/p/9354979.html