分类:
2012-09-02 12:20:53
原文地址:深入理解Linux内核(1)---基本概念 作者:leon_yu
1.Linux与其他著名的商用UNIX比较
①单块结构的内核(monolithic kernel)
它是一个庞大的,复杂的自我完善(do-it-yourself)程序,由几个逻辑上独立的成分构成。这点上是相当传统的,大多商用UNIX也是单块结构,但另外的是apple的Mac Os X和GNU的Hurd,用的微内核的方法
②编译并静态链接的传统Unix内核
可以动态安装和卸载部分内核代码(比如驱动)
③内核线程
内核线程是一个能被独立调度的执行环境(context),线程之间切换比普通进程的上下文切换话费代价少许多,因为线程在同一个地址空间执行。内核线程可以跟应用程序有关,也可以无关。
④支持多线程
Linux定义了自己的轻量级进程版本,商用unix变体都基于内核线程,而Linux吧轻量级进程当作基本的执行上下文,通过非标准的clone()系统调用来处理它们。
⑤抢占式(preemptive)内核
⑥多处理器支持
Linux 2.6以几乎最优化的方式使用SMP
⑦文件系统
支持Ext2,Ext3,jfs等
⑧STREAMS
大部分Unix内核包含了SRV4引入的STREAM I/O子系统,并且已变成编写驱动,网络协议的首选接口,但Linux没有此类子系统
2.Linux与商业Unix竞争的优势
①Linux是免费的
②Linux的所有成分都可以充分的定制
③Linux可以运行在低档、便宜的意见平台上
④Linux是强大的
⑤Linux开发者都是非常出色的程序员
⑥Linux内核非常小,而且紧凑
⑦Linux与很多通用操作系统高度兼容
⑧Linux有很好的技术支持
3.基本概念
操作系统必须完成两个主要目标:管理所有硬件,为所有app提供运行环境。Linux分用户态(User Mode)和内核态(Kernel Mode),以避免用户直接操作硬件。
⑴多用户系统(multiusers system)
多用户系统就是一台能并发(concurrently)和独立(independently)运行若干app的计算机。
并发:多个app可以竞争各种资源,如CPU,内存,硬盘等
独立:应用执行自己的任务,与其他应用无关。
多用户系统必须包含以下特点:
①核实用户身份的认知机制
②防止有错误的app妨碍其他app在系统运行的保护机制
③防止有恶意的app干涉或窥视其他用户的活动的保护机制
④限制分配给每个用户的资源数的计帐机制
Unix是实行系统资源硬件保护的多用户系统
(2)用户和组
在多用户系统中,每个用户在机器上都有私用空间,私有的权限。特别是必须保证,没有用户能够开发一个侵犯其他用户私有空间的系统app.
每个用户都有唯一的用户标志符(User ID,UID),隶属于一个或多个组的一名成员,组由唯一的用户组标志符(user group ID)标识,每个文件都属于特定的用户和组,可以设置其相应权限。
Root用户拥有一切权利,几乎无所不能,可以访问任何文件,可以干涉任何正在执行的用户程序活动。
(3)进程
资源分配的基本单位,多道程序系统(multiprogramming)或多处理系统(multiprocessing)同时执行的进程数是有限的,由调度程序(scheduler)决定那个进程执行。
多用户系统必须是可抢占式的(preemptable),操作系统记录下每个进程占有CPU时间,并周期性地激活调度程序。
Unix是具有抢占式的多处理操作系统,即使没有用户登录,没有程序运行,也有几个系统进程在监视外围设备,比如监听终端等待用户登录。
类Unix操作系统采用进程/内核模式,每个进程都自认为是系统中唯一的进程,可以独占OS所提供的服务,只要进程发出系统调用,硬件就会把特权模式由用户态变成内核态,然后进程以非常有限的目的开始一个内核过程的执行,这样,操作系统在进程的执行上下文中作用,以满足进程请求,一旦这个请求得到满足,内核过程将迫使硬件返回用户态,然后进程在系统调用的下一条指令继续执行。
(4)内核体系结构
大部分Unix内核是单块结构:每一个内核层都被集成到整个内核程序中,并代表当前进程的内核态下运行。相反,微内核(microkernel)只需要内核有一个很小的函数集,它要求程序员采用模块化的方法,因为任何操作系统层都是一个相对独立的程序,这种程序必须通过清晰明确的软件接口与其他层交互,容易移植,且能比单块内核更充分利用RAM,因为暂且不需要执行的系统进程可以被调出或撤销,但微内核一般比单块内核效率低,因为操作系统不同层次之间显示的消息传递要花费一定的代价。
为了达到微内核理论上的很多优点又不影响性能,Linux内核提供了模块(module)。模块是一个目标文件,其代码可以运行时连接到内核或卸载,通常用来实现文件系统,驱动程序或其他内核上层功能。与微内核操作系统的外层不同,模块不是作为一个特殊的进程执行的,相反,与任何其他静态链接的内核函数一样,它代表当前进程在内核态下执行。
使用模块化主要优点包括:
①模块化方法:可以在运行时动态加载或卸载,因此系统程序员必须提供良好定义的软件接口以访问由模块处理的数据结构,这使得开发新模块变得容易。
②平台无关性:即使模块依赖于某些特殊的硬件特点,但它不依赖于某个固定的硬件平台
③节省内存使用:当需要模块功能时,把它链接到运行的内核中,否则将该模块解除,这种机制对于小型嵌入式系统非常有用
④无性能损失:模块的目标代码一旦被链接到内核,其作用与静态链接的内核的目标代码完全等价,因此当模块函数被调用时,无需显示地进行消息传递。
(5)文件
Unix文件是以字节序列组成的信息载体(container),内核不解释文件的内容,很多库函数实现了更高级的抽象,例如由字段构成的记录,从用户观点来看,文件被组织在一个树结构的命名空间中,
除了叶节点外,树的所有节点都表示目录名,目录节点包含它下面文件及目录的所有信息。文件和目录名由处”/”和空字符”\0”之外的任意ASCII字符序列组成,大多数文件系统对文件名长度都有限制,通常不超过255字符。
Unix的每个进程都有一个当前工作目录,它属于进程执行上下文(execution context),标志出进程所用的当前目录。分绝对路径和相对路径。
(6)硬链接和软连接
包含在目录中的文件名就是一个文件的硬链接(hard link),或简称链接(link),在同一目录或不同目录中,同一文件可以有几个链接,因此对应几个文件名。
$ ln p1 p2
用来创建一个新的硬链接,即为路径p1标识的文件创建一个路径为p2的硬链接。
硬链接有两个限制:
①不允许给目录创建硬链接,防止把目录树变为环形图
②只有在同一个文件系统的文件之间才能创建链接。
这带来很大限制,因为现代Unix系统可能包含多种文件系统,这些文件系统位于不同的磁盘或分区,用户无法知道他们之间的物理划分,为了克服这些限制,因此引入了软连接(soft link)也叫符号链接(symbol link),符号链接是短文件,可以指向任何路径名,甚至指向不存在文件。
$ ln –s p1 p2
创建一个路径名为P2的新软链接,指向p1
(7)文件类型
Unix文件可以是下列类型之一:
普通文件(regular file)
目录
符号链接
面向快的设备文件(block-oriented device file)
面向字符的设备文件(character-oriented device file)
管道(pipe)和命名管道(named pipe)(也叫FIFO)
套接字(socket)
前三个是Unix文件系统的基本类型,设备文件与驱动程序相关,访问文件时即直接访问I/O设备,管道和套接字是用于进程间通信的特殊文件。
(8)文件描述符与索引节点
Unix对文件的内容描述和描述文件的信息给出了清楚的区分,除了设备文件和特殊文件外,每个文件都有字符序列组成,文件内容不包含任何控制信息,如文件长度,权限等。
文件系统处理文件需要的所有信息包含在一个名为索引节点(inode)的数据结构中,每个文件都有自己的索引节点,文件系统用索引节点来标志文件。
虽然文件系统及内核函数对索引节点的处理可能随Unixixt不同有所不同,但一般都必须至少提供在POSIX标准中指定的如下属性:
文件类型
与文件相关的硬链接个数
以字节为单位的文件长度
设备标志符
在文件系统中标志文件的索引节点号
文件拥有者的UID
文件的用户组ID
几个时间戳,表示索引节点改变时间、最后访问时间及最后修改时间
访问权限和文件模式
(9)访问权限和文件模式
文件的潜在用户分为三种类型
文件拥有者用户
同组用户,不包括所有者
其他用户
有三种访问权限—读、写及执行。还有三种附加的标记
Suid:(set User ID)进程执行一个文件时通常保持进程拥有者UID,如果设置了可执行文件的suid标志位,进程就获得了该文件拥有者的UID
Sgid(Set Group ID):进程执行一个文件时保持进程组的组ID,如果设置了可执行文件的sgid标志位,进程就获得了该文件用户组的ID
Sticky:
当文件由一个进程创建时,文件拥有者ID就是该进程UID,而其组用户ID可以是进程创建者的ID,也可以是父目录的ID,取决于副目录的sgid标志位
(10)文件操作的系统调用
当用户访问一个普通文件或目录内容时,他实际上是访问存储在硬件块设备上的一些数据,从这个意义上说,文件系统是硬盘分区物理组织的用户及视图。
①打开文件 fd= open(path,flag,mode)
这个系统调用创建一个“打开文件”对象,并返回文件描述符(file descriptor).
这个对象包括:
文件操作的一些数据结构,如打开方式,偏移等/
进程可以调用的一些内核函数指针
文件描述符表示进程与打开文件之间的交互,而打开文件对象包含了与这种交互相关的数据。同一个文件对象也许由几个文件描述符标志,并且Unix文件系统在同一文件上发出的I/O操作之间不提供任何形式的同步机制。
②访问文件
Lseek,read,write,close(fd)
③更名及删除
Res = rename(oldpath,newpath),改变了文件链接的名字
Res=unlink(pathname),减少文件连接数,删除了相应目录项,当连接数为0时,文件才被真正删除
(11)进程/内核模式
CPU既可以运行在用户态,也可以运行在内核态下,实际上一些CPU可以运行两种以上执行状态,如Intel80x86微处理器有四种不同的执行状态,ARM有其中执行状态,但所有标准的Unix内核都仅仅利用了内核态和用户态。
进程是动态的尸体,在系统内通常只有有限的生命期。
内核本身并不是一个进程,而是进程的管理者。
除用户进程外,Unix系统还包括几个内核线程(kernel thread)的特权进程,它们有以下特点:
它们以内核态运行在内核地址空间
他们不与用户直接交互,因此不需要终端设备
他们通常在系统启动时创建,然后一直处于活跃状态知道系统关闭
Unix内核的工作远不止处理系统调用,实际有4中方式激活内核例程
①进程调用系统调用
②CPU发出异常(exception)信号
③外围向CPU发送中断(interrupt)信号
④内核线程被执行。
为了让内核管理进程,每个进程由一个进程描述符(process description)表示,这个进程描述符包含有关进程当前状态的信息。
当内核暂停一个进程执行时,就把几个相关寄存器内容保存进进程描述符(PC,SP,通用寄存器,浮点寄存器,处理器状态字),当内核决定恢复执行一个进程时,用进程描述符中合适的字段来装载CPU寄存器。
(12)可重入内核
所有的Unix内核都是可重入的(reentrant),这意味着若干进程可以同时在内核下执行,提供可重入的一种方式是,编写内核函数只改变局部变量不改变全局结构,尽管如此(一些实时内核就是如此实现),可重入内核可以包括非重入函数,并且利用鎖机制保证一次只有一个进程执行的非重入函数。
内核控制路径(kernel control path):表示内核处理系统调用、异常或中断所执行的质量序列。
当下属事件之一发生时,CPU交错执行内核控制路径:
①进程调用一个系统调用,相应内核控制路径证实该请求无法立即满足,内核控制路径调用调度程序选择一个新的进程投入运行,结果进程切换发生,第一个内核控制路径没完成,而CPU又重新开始执行其他内核控制路径,这种情况下,两条控制路径代表两个不同的进程在执行。
②当运行一个内核控制路径时,CPU检测到一个异常
③当CPU正在运行一个启动了中断的内核控制路径,硬件中断发生
④在支持抢占式调度的内核中,CPU正在运行,而一个更高优先级的进程加入就绪队列,则中断发生。
(13)进程地址空间
每个进程运行在它的私有地址空间,在用户态下运行的进程设计有私有栈,数据区和代码区,当在内核态运行时,进程访问内核的数据区和代码区,但是用另外私有的栈。
因为内核是可重入的,因此几个内核控制路径(对应不同进程相关)可以轮流执行,这种情况下,每个内核控制路径都引用它自己的私有内核栈。
尽管看起来每个进程访问一个私有地址空间,但有时进程之间也共享部分地址空间。
①用户空间的进程间内存共享。
②同一个程序由几个用户同时使用,则这个程序只被装入内存一次,其指令由所有需要它的用户共享。
③mmap()系统调用,允许块设备上文件映射到进程的部分地址空间。
(14)同步和临界区
当调度多个进程访问共享数据时,存在一种竞争条件(race condition),实现可重入内核需要利用同步机制。
①非抢占式内核
在寻找彻底、简单地解决同步问题的方案中,大多数传统的Unix内核都是非抢占式的。
如果内核支持抢占,那么在应用同步机制时,确保进入临界区前禁止抢占,退出临界区时开启抢占。
②禁止中断
单处理器系统上,在进入临界区时,禁止所有硬件中断,离开临界区时再重启中断。
但在多处理器系统中禁止本地CPU中断时不够的,所有还是要采用其他的同步技术。
③信号量
广泛使用的一种机制是信号量(semaphore),它在单处理器,多处理器系统中都有效。可以把信号量看作一个对象,它包括:
一个整数变量
一个等待进程的链表
两个原子方法:down()和up()
每个要保护的数据结构都有它自己的信号量,其初始值为1.down()方法对信号量值减1,up()对信号量值加1,若信号量值小于0,则加入到等待链表中,且当前进程挂起,若大于0,则正常访问数据。
④自旋锁
在多处理系统中,信号量并不总是解决同步的最好办法。自旋锁与信号量类似,但没有链表,当一个进程发现锁被另一个进程持有时,它不停的“旋转”,执行一个紧凑的循环指令制导锁打开。
在单处理器环境下,自旋锁是无效的,因为当一个进程等待锁时,永远得不到锁释放,系统将挂起。
⑤避免死锁
只要涉及到内核设计,当所用内核信号量数量较多时,死锁就成为一个突出问题。Linux通过按规定的循序请求信号量来避免死锁,例如哲学家就餐问题。
(15)信号与进程间通信
Unix信号(signal)提供了把系统事件报告给进程的一种机制,有两种系统事件:
异步通告---例如当用户在终端按下中断建(CTRL+C),即向前台发送一个中断信号SIGINT
同步错误或异常---例如当进程访问非法内存地址时,内核向这个进程发送一个SIGSEGV信号
一般来说,进程可以有两种方式对信号做出反应:忽略该信号,异步地执行一个指定过程(信号处理函数)
POSIX标准定义了大约20种不同信号,其中有2个是用户自定义的。
如果进程不指定选择何种方式,内核就根据信号的编号执行一个默认操作,5种可能的默认操作是:
①终止进程
②将执行上下文或进程地址空间内容写入一个文件(核心转储,core dump)并终止进程
③忽略信号
④挂起进程
⑤如果进程曾被暂停,则恢复它的执行。
AT&T的Unix System V引入了在用户态下进程间通信机制:信号量,消息队列及共享内存,统称为System V IPC。共享内存是进程间交换和共享数据的最快方式。
(16)进程管理
Unix在进程和正在执行的程序之间做出了清晰划分。Fork()和_exit()分别用来创建和终止一个进程,而exec()类系统调用则是装入一个新程序,该新程序替换了当前进程的正文、数据、堆和栈段,进程ID不变。
实现fork()是将父进程数据和代码完全复制,这会相当费时,当前以来硬件分页单元的内核采用写时复制(Copy-On-Write)技术,及把页的复制延迟到最后一刻(子进程需要时才写进页)。
_exit()系统调用终止一个进程,内核对这个系统调用处理是释放内核拥有的资源并向父进程发送SIGCHILD信号(默认操作为忽略)
(17)僵尸进程(zombie precess)
父进程调用wait4()等待其中一个子进程结束,它返回已终止子进程的进程标志符(Precess ID,PID),如果没有子进程退出,该进程就设置成等待状态,一直到子进程结束。
若在父进程调用wait4()之前,子进程就死掉了,则成为僵死进程。
解决方法是init进程,它在系统初始化时创建,当一个进程终止时,内核改变其所有现有子进程的进程描述符指针,收养所有这些进程为init的子进程。Init监控所有子进程的执行,并且按常规发布wait4(),这样可以除掉所有僵尸进程。
(18)进程组和登陆会话
现代Unix引入进程组(process group)的概念,以表示一种作业(job)的抽象。
$ls |sort |more
Shell支持进程组,例如bash,为三个相应进程ls,sort,more创建一个进程组,每个进程组有一个领头进程(其PID与进程组ID相同的进程),新创建的进程最初被插入到父进程的进程组中。
登陆会话(login session):一个登陆会话包含指定终端已经开始工作会话的那个进程的所有后代进程。进程组中的所有进程必须在同一个登陆会话中。只有一个进程组一直处于前台,它可以访问中断,其他活动着的进程组在后台,后台进程访问中断时,将受到SIGTTIN或SIGTTOUT信号,可以用bg和fg把一个进程放在后台或前台。
(19)内存管理
①虚拟内存(virtual memory)
虚拟内存有多个有点和用途:
若干个进程可以并发地执行
应用程序所需内存大于可用物理内存时也可以运行
程序只有部分代码装入内存时,进程也可以执行它
允许每个进程访问可用物理内存的子集
进程可以共享库函数或程序的一个单独内存映像
程序可以重定位,即可以在任何地址执行
程序员可以编写机器无关代码
现在的CPU包含了能自动把虚拟地址转换成物理地址的硬件电路,用页表来指定虚拟地址和物理地址的对应关系。
②随机访问存储器(RAM)的使用
内核把RAM分为两个部分,一部分专门用于放内核映像,其他部分由虚拟内存系统来处理。
虚拟内存系统必须解决的一个主要问题是内存碎片。
③内核内存分配器(Kernel Memory Allocator,KMA)
它试图满足系统中所有部分对内存的请求,一个好的KMA应具有下列特点:
必须快
必须把内存的浪费减到最小
必须努力减轻内存的碎片问题(fragmentation)
必须能与其他内存管理子系统合作,以便借用和释放页框。
基于各种不同的算法技术,已经提出了几种KMA,包括
资源分配算法(allocator)
2的幂次方空闲链表
McKusick-Karels分配算法
伙伴(Buddy)系统
Mach的区域(Zone)分配算法
Dynix分配算法
Solaris的Slab分配算法
④进程虚拟地址空间处理
内核分配给进程的虚拟地址空间由以下内存组成:
程序的可执行代码
程序的初始化数据
程序的未初始化数据
初始程度栈(即用户态栈)
所需共享库的可执行代码和数据
堆(用于程序动态请求内存)
所有现代Unix操作系统采用了请求调页(demand paging)的内存分配策略。
⑤高速缓存
最早在Unix系统实现的一个策略是,尽可能地推迟写磁盘的时间,从磁盘读入内存的数据集市进程不再使用它们,他们也留在RAM中,当一个进程请求访问磁盘时,内核首先检查所请求数据是否在内存中,若在(叫做缓存命中),内核就不用访问磁盘了。
Sync()系统调用把所有“脏”的缓冲区(缓冲与磁盘内容不一样),写入磁盘来强制磁盘同步。
(20)设备驱动程序
设备驱动包含在内核中,由一个或多个设备的数据结构和函数组成,通过特定的接口,每个驱动程序与内核的其余部分相互作用。