Unix内核概述
Unix内核提供了应用程序可以运行的执行环境。因此,内核必须实现一组及相应的接口。应用程序使用这些接口,而且通常不会与硬件资源直接交互。
进程/内核模式
如前所述,CPU既可以运行在用户态下,也可以运行在内核态下。实际上,一些CPU可以有两种以上的执行状态。例如,Intel80x86微处理器有四种不同的执行状态。但是,所有标准的Unix内核都仅仅利用了内核态和用户态。
当一个程序在用户态下执行时,它不能直接访问内核数据结构或内核的程序。然而,当应用程序在内核态下运行时,这些限制不再有效。每种CPU模型都为从用户态到内核态的转换提供了特殊的指令,反之亦然。一个程序执行时,大部分时间都处于在用户态下,只有需要内核所提供的服务时才切换到内核态。当内核满足了用户程序的请求后,它让程序又回到用户态下。
进程是动态的实体,在系统内通常只有有限的生存限。创建、撤消及同步现有进程的任务都委托给内核中的一组例程来完成。
内核本身并不是一个进程,而是进程的管理者。进程/内核模式假定:请求内核服务的进程使用所谓系统调用的特殊编程机制。每个系统调用都设置了一组识别进程请求的参数、然后执行与硬件相关的CPU指令完成从用户态到内核态的转换。
除用户进程之外、Unix系统还包括几个所谓内核线程的特权进程,它们有以下特点:
它们以内核进行在内核地址空间。
它们不与用户直接交互,因此不需要终端设备。
它们通常在系统启动时创建,然后一直处于活跃状态到系统关闭。
在单处理器中,任何时候只有一个进程在运行,它要么处于用户态,要么处于内核态。如果进程运行在内核态,处理器就执行一些内核例程。图1-2举例说明了用户态与内核态之间的相互转换。处于用户态的进程1发出系统调用之后,进程切换到内核态,系统调用被执行。然后,直到发生定时中断且调度程序在内核态被激活,进程1才恢复在用户态下执行。进程切换发生,进程2在用户态开始执行,直到硬件发出中断请求。中断的结果是,进程2切换到内核态并处理中断。
Unix内核做的工作远不止处理系统调用。实际上,可以有几种方式激活内核例程:
进程调用系统调用。
正在执行进程的CPU发出一个异常信号,异常是一些反常情况,例如一个无效的指令。内核代表产生异常的进程处理异常。
外围设备向CPU发出一个中断信号以通知一个事件的发生,如一个要求注意的请求、一个状态的变化或一个I/O操作已经完成等。每个中断信号都是由内核中的中断处理程序来处理的。因为外围设备与CPU异步操作,因此,中断在不可预知的时间发生。
内核线程被执行。因为内核线程运行在内核态,因此必須认为其相应程序是内核的一部分。
进程实现
为了让内核管理进程,每个进程由一个进程描述符表示,这个描述符包含有关进程当前状态的信息。
当内核暂停一个进程的执行时,就把几个相关处理器寄存器的内核保存在进程描述符中。这些寄存器包括:
程序计数器和栈指针寄存器
通用寄存器
浮点寄存器
包含CPU状态信息的处理器控制寄存器
用来跟踪进程对RAM访问的内存管理寄存器
内核决定恢复执行一个进程时,它用进程描述符中合适的字段来装载CPU寄存器。因为程序计数器中所存的值指向下一条将要执行的指令,所以进程从它停止的地方恢复执行。
当一个进程不在CPU上执行是、它正在等待某一事件,Unix内核可以区分很多等待状态,这些等待状态通常由进程描述符队列实现。每个队列对应一组等待特定事件的里程。
可重入内核
所有的Unix内核都是可重入的,这意味若干个进程可以同时在内核态下执行。当然,在单处理器系统上只有一个进程在真正运行,但是有许多进程可能在等待CPU或一I/O操作完成时在内核态下阻塞。例如,当内核代表某一进程发出一个读磁盘请求后,就让磁盘控制器处理这个请求并且恢复执行其它进程。当设备满足了读请求时,有一个中断就会通知内核,从而以前的进程可以恢复执行。
提供可重入的一种方式是编写函数,以便这些函数只能修改局部变量,而不能改变全局数据结构,这样的函数叫可重入函数。但是可重入内核不仅仅局限于这样的可重入函数。相反,可重入内核可以包括非重入函数,并且利用锁机制保证一次只有一个进程执行一个非重入函数。
如果一个硬件中断发生,可重入内核能挂起当前正在执行的进程,即使这个进程处于内核态。这种能力是非常重要的,因为这能提高发出中断的设备控制器的吞吐里量。一旦设备已发出一个中断,它就一直等待直到CPU应答它为止。如果内核能够快速应答,设备控制器在CPU处理中断时就能执行其它任务。
现在,让我们看一下内核的可重入性及它对内核组织的影响。内核控制路径表示内核处理系统调用、异常或中断所执行的指令序列。
在最简单的情况下,CPU从第一条指令到最后一条指令顺序地执行内核控制路径。然而,当下述事件之一发生时,CPU交错执行内核控制路径:
运行在用户态下的进程调用一个系统调用,而相应的内核控制路径证实这个请求无法立即得到满足;然后,内核控制路径调用调度程序选择一个新的进程投入运行。结果,进程发生切换。第一个内核控制路径还没有完成,而CPU又重新开始执行其它的内核控制路径。在这种情况下,两条控制路径代表两个不同的进程在执行。
当运行一个内核控制路径时,CPU检测到一个异常。第一个控制路径被挂起,而CPU开始执行合适的过程。在我们的例子中,这种过程能给进程分配一个新页,并从磁盘读它的内容。当这个过程结束时,第一个控制路径可以恢复执行。在这种情况下,两个控制路径代表同一个进程在执行。
当CPU正在执行一个启用了中断的内核控制路径时,一个硬件中断发生。第一个内核控制路径还没有执行完,CPU开始执行另一个内核控制路径来处理这个中断。当这个中断处理程序终止时,第一个内核控制路径恢复。在这种情况下,两个内核控制路径运行在同一进程的可执行上下文中,所花费的系统CPU时间都算给这个进程。然而,中断处理程序无需代表这个程序运行。
在支持抢占式调度的内核中,CPU下在运行,而一个更高优先级的进程加入就绪队列,则中断发生。在这种情况下,第一个内核控制路径还没有执行完,CPU代表高优先级进程又开始执行另一个内核控制路径。只有把内核编译成支持抢占式高度之后,才可能出现这种情况。
图1-3显示了非交错的和交错的内核控制路径的几个例子。考虑以下三种不同的CPU状态:
在用户态下运行一个进程
运行一个异常处理程序或系统调用处理程序
运行一个中断处理程序
进程地址空间
每个进程运行在它的私有地址空间,在用户态下运行的进程涉及到私有栈,数据区和代码区。当在内核态运行时,进程访问内核的数据区和代码区,但使用另外的私有栈。
因为内核是可重入的,因此几个内核控制路径可以轮流执行。在这种情况下,每个内核控制路径都引用它自己的私有内核栈。
尽管看起来每个进程访问一个私有地址空间,但有时进程之间也共享部分地址空间。在一些情况下,这种共享由进程显式地提出;在另外一些情况下,由内核自动完成共享以节约内存。
如果同一个程序由几个用户同时使用,则这个程序只被装入内存一次,其指令由所有需要它的用户共享。当然,其数据不被共享,因为每个用户将有独立的数据。这种共享的地址空间由内核自动完成以节约内存。
进程间也能共享地址空间,以实现一种进程间通信,这就是由SystemV引入并且已经被Linux支持的''共享内存”技术。
最后,Linux支持mmap()系统调用,该系统调用允许存放在块设备上的文件或信息的一部分映射到进程的部分地址空间。内存映射为正常的读写传送数据方式提供了另一种选择。如果同一文件由几个进程共享,那么共享它的每个进程地址空间都包含有它的内存映射。
同步和临界区
实现可重入内核需要利用同步机制:如果内核控制路径对某个内核数据结构进行操作时被挂起,那么,其它的内核控制路径就不应当再对该数据结构进行操作,除非它已被重新设置成一致性状态。否则,两个控制路径的交互作用将破坏所有存储的信息。
例如,假如全局变量V包含某个系统资源的可用项数。第一个内核控制路径A读这个变量,并且确定仅有一个可用资源项。这时,另一个控制路径B被激活,并读同一个变量V,V的值仍为1。因此,B对V减1,并开始用这个资源项。然后,A恢复执行。因为A已经读到V的值,于是它假定自己可以对V减1并获取B已经在使用的这个资源项。结果,V的值变为-1,两个内核控制路径使用相同的资源项有可能导致灾难性的后果。
当某个计算结果取决于如果调度两个或多个进程时,相关代码就是不正确的。我们说存在一种竞争条件。
一般来说,对全局变量的安全访问通过原子操作来保证。在前面的例子中,如果两个控制路径读V并减1是一个单独的、不可中断的操作,那么,就不可能出现数据讹误。然而,内核包含的很多数据结构是无法用单一操作访问的。例如,可单一的操作从链表中删除一个元素是不可能的,因为内核一次至少访问两个指针。临界区是这样的一段代码,进入这段代码的进程必须完成之后另一个进程才能进入。
这些问题不仅出现在内核控制路径之间,也出现在共享公共数据的进程之间。几种同步技术已经被使用。以下将集中讨论怎样同步内核控制路径。
非抢占内核
在寻找彻底,简单的解决问题的方案中,大多数传统的Unix内核都是非抢占式的:当进程在内核态执行时,它不能被任意挂起,也不能被另一个进程代替。因此,在单处理器系统上,中断或异常处理程序不能修改的所有内核数据结构,内核对它们的访问进了是安全的。
当然,内核态的进程能自愿放弃CPU,但是这种情况下,它必须确保所有的数据结构都处于一致性状态。此外,当这种进程恢复执行时,它必須重新检查以前访问过的数据结构的值,因为这些数据结构有可能被改变。
如果内核支持抢占,那么在应用同步机制时,确保进入临界区前禁止抢占,退出临界区时启用抢占。
非抢占能力在多处理器系统上是低效的,因为运行在不同CPU上的两个控制路径本可以并发地访问相同的数据结构。
禁止中断
单处理器系统上的另一种同步机制是:在进入一个临界区之前禁止所有硬件中断,离开时再重新启用中断。这种机制尽管简单,但远不是最佳的。如果临界区比较大,那么在一个相对较长的时间内持续禁止中断就可能使所有的硬件活动处于冻结状态。
此外,由于在多处理器系统中禁止本地CPU上的中断是不够的,所以必须使用其它的同步技术。
信号量
广泛使用的一种机制是信号量,它在单处理器和多处理器系统上都有效。信号量仅仅是与一个数据结构相关的计数器。所有内核线程在试图访问这个数据结构之前,都要检查这个信号量。可以把每个信号量看成一个对象,其组成如下:
一个整数变量
一个等待进程的链表
两个原子方法:down()和up()
down()方法对信号量的值减1,如果这个新值小于0,该方法就把正在运行的进程加入到这个信号量链表,然后阻塞该进程。up()方法对信号量的值加1,如果这个新值大于或等于0,则激活这个信号链表中的一个或多个进程。
每个要保护的数据结构都有它自己的信号量,其初始值为1。当内核控制路径希望访问这个数据结构时,它在相应的信号量上执行down()方法。如果信号量的当前值不是负数,刚允许访问这个数据结构。否则,把执行内核控制路径的进程加入到这个信号量的链表并阻塞该进程。当另一个进程在那个信号量上执行up()方法时,允许信号量链表上的一个进程继续执行。
自旋锁
在多处理器系统中,信号量并不总是解决同步问题的最佳方案。系统不允许在不同CPU上运行的内核控制路径同时访问某些内核数据结构,在这种情况下,如果修改数据结构所需的时间比较短,那么信号量可能有些低效的。为了检查信号量,内核必须把进程插入到信号量链表中,然后挂起它。因为这两操作比较费时,完成这些操作时,其它的内核控制路径可能已经释放了信号量。
在这些情况下,多处理器操作系统使用了自旋锁。自旋锁与信号量非常相似,但没有进程链表:当一个进程发现锁被另一个进程锁着时,它就不停地“旋转”,执行一个紧凑的循环指令直到锁打开。
当然,自旋锁在单处理器环境下是无效的。当内核控制路径试图访问一个上锁的数据结构时,它开始无休止循环。因此,内核控制路径可能因为正在修改受保护的数据结构而没有机会继续执行,也没有机会释放这个自旋锁。最后的结果可能是系统挂起。
避免死锁
与其它控制路径同步的进程或内核控制路径很容易进入死锁状态。举一个最简单的死锁的例子,进程P1获得访问数据结构a的权限,进程p2获得访问b的权限,但是p1在等待b,而p2在等待a.进程之间其它更复杂的循环等待情况也可能发生。显然,死锁情形会导致受影响的进程或内核控制路径完全处理冻结状态。
只要涉及到内核设计,当所有内核信号量的数量较多时,死锁就成为一个突出问题。在这种情况下,很难保证内核控制路径在各种可能方式下的交错执行不出现死锁状态。有几种操作系统通按规定的顺序请求信号量来避免死锁。
信号和进程间通信
Unix信号提供了把系统事件报告给进程的一种机制。每种事件都有自己的信号编写,通常用一个符号常量来表示,例如SIGTERM。有两种系统事件:
异步通告
例如,当用户在终端按下中断键时,即向前台进程发出中断信号SIGINT。
同步错误或异常
例如,当进程访问内存非法地址时,内核向这个进程发送一个SIGSEGV信号。
POSIX标准定义了大约20种不同的信号,其中,有两种是用户自定义的,可以当作用户态下进程通信和同步的原语机制。一般来说,进程可以以两种方式接收到的信号做出反应:
忽略该信号
异步地执行一个指定的过程
如果进程不指定选择何种方式,内核就根据信号的编号执行一个默认操作。五种可能的默认操作是:
终止进程
将执行上下文和里程地址空间的内容写入一个文件,并终止进程。
忽略信号。
挂起进程。
如果进程曾被暂停,则恢复它的执行。
因为POSIX语义允许进程暂时阻塞信号,因此内核信号的处理相当精细。此外,SIGKILL和SIGSTOP信号不能直接由进程处理,也不能由里程忽略。
AT&T的UnixSystemV引入了在用户态下其它种类的进程间通信机制,很多Unix内核也使用了这种机制:信号量,消息队列及共享内存。它们被统称为SYSTEMV IPC.
内核把它们作为IPC资源来实现:进程要获得一个资源,可以调用shmget()、setmget()或msgget()系统调用。与文件一样,IPC资源是持久不变的、进程创建者、进程拥有者或超级用户必须显式地释放这些资源。
这里的信号量与本章“同步和临界区”一节中所描述的信号量是相似的,只是它们用在用户态下的进程中。消息队列允许进程利用msgsnd()及msgget()系统调用交换消息,msgsnd()表示把消息插入到指定的队列中,msgget()表示从队列中提取消息。
POSIX标准定义了一种基于消息队列的IPC机制,这就是所谓的POSIX消息队列。它们和Systemv IPC消息队列是相似的,但是,它们对应用程序提供一个更简单的基于文件的接口。
共享内存为进程之间交换和共享数据提供了最快的方式。通过调用shmget()系统调用来创建一个新的共享内存,其大小按需设置。在获得IPC资源标识符后,进程调用shmat()系统调用,其返回值是进程的地址空间中新区域的起始地址。当进程希望把共享内存从其地址空间分离出去时,就调用shmdt()系统调用。共享内存的实现依赖于内核对进程地址空间的实现。
进程管理
Unix在进程和它正在执行的程序之间做出一个清晰的划分。fork()和_exit()系统调用分别用来创建和终止一个进程,而调用exec()类系统调用则是装入一个新程序。当这样一个系统调用执行之后,进程就在所装入程序的全新地址空间恢复运行。
调用fork()的进程是父进程,而新进程是它的子进程。父子进程能互相找到对方,因为描述每个进程的数据结构都包含有两个指针、一个直接指向它的父进程,另一个直接指向它的子进程。
实现fork()一种天真的方式是将父进程的数据与代码都复制,并把这个拷贝赋予子里程。这会相当费时。当前依赖硬件分页单元的内核使用写时复制技术,即把页的复制廷迟到最后一刻。
_exit()系统调用终止一个进程,内核对这个系统调用的处理是通过释放进程所拥有的资源并向父进程发送SIGCHILD信号来实现的。
僵死进程
父进程如何查询其子进程是否终止了呢?wait4()系统调用允许进程等待。直到其中的一个子进程结束,它返回已终止子进程的进程标识符。
内核在执行这个系统调用时,检查子进程是否已经终止。引入僵死进程的特殊状态是为了表示终止的进程。父进程执行完wait4()系统调用之前,进程就一直停留在那种状态。系统调用处理程序从进程描述符字段中获取有关资源使用的一些数据;一旦得到数据,就可以释放进程描述符。当进程执行wait4()系统调用时如果没有子进程结束,内核就通常把该进程设置成等待状态,一直到子进程结束。
很多内核也实现了waitpid()系统调用,它允许进程等待一个特殊的子进程。其它wait4()系统调用的变体也是相当通用的。
在父进程发出wait4()调用之前,让内核保存子进程的有关信息是一个良好的习惯,但是,假设父进程终止而没有发出wait4()调用呢?这些信息占用了一些内存中很有用的位置,而这些位置本来可以为活动着的进程提供服务。例如,很多shell允许用户在后台启动一个命令然后退出。正在运行这个shell命令的进程终止,但它的子进程继续运行。
解决的办法是使用一个名init的特殊进程,它在系统初始化的时候被创建。当一个进程终止时,内核改变其所有现有子进程描述符指针,使这些子里程成为init的孩子。init监控所有子里程的执行,并且按常规发在布wait4()系统调用,其副作用就是除掉所有僵死的进程。
进程组和登录会话
现代Unix操作系统引入了进程组的概念,以表示一种“作业”的抽象。例如,为了执行命令行:
$ls| sort | more
Shell支持进程组,例如bash,为了三个相应的进程ls,sort及more创建了一个新组。shell以这种方式作用于这三个进程,就好像它们是一个单独的实体。每个进程描述符包括一个进程组ID的字段。每一进程组可以有一个领头进程。新创建的进程最初插入到其父进程的进程组中。
现在Unix内核也引入了登录会话。非正式的说,一个登录会话包含在指定终端已经开始工作会话的那个进程的所有后代进程-------通常情况下,登录会话就是shell进程为用户创建的第一条命令。进程组中的所有进程必须在同一登录会话中,一个登录会话可以让几个进程组同时处于活动状态,其中,只有一个进程组一直处于活动状态,其中,只有一个进程组一直处于前台,这意味着该进程组可以访问终端,而其它活动着的进程组在后台。当一个后台进程试图访问终端时,它将收到SIGTTIN或SIGTTOUT信号。在很多shell命令中,用内部命令bg和fg把一个进程组放在后台或者前台。
内存管理
虚拟内存
所有新近的Unix系统都提供了一种有用的抽象,叫虚拟内存。虚拟内存作为一种逻辑层,处于应用程序的内存请求与硬件内存管理单元之间。虚拟内存有很多用途和优点:
若干个进程可以并发地执行。
应用程序所需内存大于可用物理内存时也可以执行
程序只有部分代码装入内存时进程可以执行它
允许每个进程访问可用物理内存的子集。
进程可以共享库函数或程序的一个单独内存映像
程序是可重定位的,也就是说,可以把程序放在物理内存的任何地方。
程序员可以编写与机器无关的代码,因为他们不必关心有关物理内存的组织结构。
虚拟内存子系统的主要成分是虚拟地址空间的概念。进程所用的一组内存地址不同于物理内存地址。当进程使用一个虚拟地址时,内核和mmu协同定位其在内存中的实际物理位置。
现在的CPU包含了能自动把虚拟地址转换成物理地址的硬件电路。为了达到这个目标,把可用RAM划分成长度为4KB或8KB的页框,并且引入一组页表来指定虚拟地址与物理地址之间的对应关系。这些电路使内存分配变得简单,因为一块连续的虚拟地址请求可以通过分配分配一组非连续的物理地址页框而得到满足。
随机访问存储器的使用
所有的Unix操作系统都将RAM毫无疑义地划分为两部分,其中若干字节专门用于存放内核映像。RAM的其它部分通常由虚拟内存系统来处理,并且用在以下三种可能的方面:
满足内核对缓冲区、描述符及其它动态内核数据结构的请求。
满足进程对一般内存区的请求及对文件内存映射的请求。
借助于高速缓存从磁盘及其它缓冲设备获得较好的性能。
每种请求类型都是很重要的。但从另一方面来说,因为可用RAM是有限的,所以必须在请求类型之间做出平衡,尤其是当可用内存没有剩下多少时。此外,当可用内存到临界阀值时,可以调用页框回收算法释放其它内存,那么哪些页框是最适合回收的页框呢?
虚拟内存系统必須解决的一个主要问题是内存碎片。理想情况下,只有当空闲页框太少时,内存请求才失败。然而,通常要求内核使用物理上连续的内存区域,因此,即使有足够的可用内存,但它不能作为一个连续的大块使用时,内存的请求也会失败。
内核内存分配器
内核内存分配器是一个子系统,它试图满足系统中所有部分对内存的请求。其中一些请求来自内核其它子系统,它们需要一些内核使用的内存,还有一些请求来自用户程序的系统调用,用来增加用户进程的地址空间。一个好的KMA应该有下列特点:
必须快,实际上,这是最重要的属性,因为它由所有的内核子系统调用。
必须把内存的浪费减到时最少。
必须努力减轻内存的碎片问题
必须能与其它内存管理子系统合作,以便借用和释放页框。
基于各种不同的算法技术,已经提出了几种KMA,包括:
资源图分配算法
2的幂次方空闲链表
McKusick-Karels分配算法
伙伴系统
Mach的区域分配算法
Dynix分配算法
Solaris的Slab分配算法
进程虚拟地址空间处理
进程的虚拟地址空间包括了进程可以引用的所有虚拟内存地址。内核通常用一组内存区描述符进程虚拟地址空间。例如,当进程通过exec()类系统调用开始某个程序的执行时,内核分配给进程的虚拟地址空间由以下内存区组成:
程序的可执行代码
程序的初始化数据
程序的未初始化数据
初始程序栈
所需要共享库的可执行代码的数据
堆
所有现代Unix操作系统都使用了所谓请求调页的内存分配策略。有了请求调页,进程可以在它的页还没有在内存是就开始执行。当进程访问一个不存在的页时,MMU产生一个异常;异常处理程序找到受影响的内存区,分配一个空闲的页,并用适当的数据把它初始化。同理,当进程通过通过调用malloc()或brk()系统调用动态地请求内存时,内核仅仅修改进程的堆内存区的大小。只有试图引用进程的虚拟内存地址而产生异常时,才给进程分配页框。
虚拟地址空间也使用其它更有效的策略,如前面提到的写时复策略。例如,当一个新进程被创建时,内核仅仅把父进程的页框赋给子进程的地址空间,但是把这些页框标记为只读。一旦父或子进程试图修改页中的内容时,一个异常就会产生。异常处理程序把新页框赋给受影响的进程,并用原来页中的内容初始化新页框。
高速缓存
物理内存的一大优势就是用作磁盘和其它块设备的高速缓存。这是因为硬盘非常慢:磁盘的访问需要数毫秒,与RAM的访问时间相比,这太长了。因此,磁盘通常是影响系统性能的瓶颈。通常,在最早的Unix系统中就已经实现了一个策略是:尽可能地推迟写磁盘的时间,因此,从磁盘读入内存的数据即使任何进程都不再使用它们,它们也继续留在RAM中。
这一策略的前提是有好机会摆在面前:新进程请求从磁盘读或写的数据,就是被撤消进程曾拥有的数据。当一个进程请求访问磁盘时,内核首先检查进程请求的数据是否在缓存中,如果在,内核就可以为里程请求提供服务而不用访问磁盘。
sync()系统调用把所有“脏”的缓冲区写入磁盘来强制磁盘同步。为了避免数据丢失,所有的操作系统都会注意周期性地把脏缓冲区写回磁盘。
设备驱动程序
内核通过设备驱动程序与I/O设备交互。设备驱动程序包含在内核中,由控制一个或多个设备的数据结构和函数组成,这些设备包括硬盘、键盘、鼠标、监视器、网络接口及连接到SCSI总线上的设备。通过特定的接口,每个驱动程序与内核中的其余部分相互作用这种方式具有以下特点:
可以把特定设备的代码在特定的模块中。
厂商可以在不了解内核源代码而只知道接口规范的情况下,就能增加新的设备。
内核以统一的方式对待所有的设备,并且通过相同的接口访问这些设备。
可以把设备驱动程序写成模块,并动态地把它们状进内核而不需要重新启动系统。需再需要时,也可以动态地卸下模块,以减少存储在RAM中的内核映像的大小。
一些用户程序希望操作硬件设备。这些程序就利用常用的、与文件相关的系统调用及在/dev目录下能找到的设备文件向内核发出请求。实际上,设备文件是设备驱动程序接口中用户可见部分。每个设备文件都有专门的设备驱动程序,它们由内核调用以执行对硬件设备的请求操作。