深入理解Linux内核(一)——Linux操作系统基础概念

文章目录

  • 前言
  • 操作系统基本概念
    • 多用户系统
    • 用户和组
    • 进程
    • 内核体系结构
  • Unix文件系统概述
    • 文件
    • 硬链接和软链接
    • 文件类型
    • 文件描述符与索引节点
    • 访问权限和文件模式
  • 文件操作的系统调用
    • 打开文件
    • 访问打开文件
    • 关闭文件
    • 更名及删除文件
  • Unix内核概述
    • 进程/内核模式
    • 进程实现
    • 可重入内核
    • 进程地址空间
    • 同步和临界区
    • 非抢占式内核
    • 禁止中断
    • 信号量
    • 自旋锁
    • 避免死锁
    • 信号和进程间通信
    • 进程管理
    • 僵死进程
    • 进程组和登录会话
  • 内存管理
    • 虚拟内存
    • 随机访问存储器(RAM)的使用
    • 内核内存分配器(KMA)
    • 进程虚拟地址空间处理
    • 高速缓存
    • 设备驱动程序

前言

基于深入理解Linux内核 第三版 陈莉俊译

操作系统基本概念

任何计算机系统都包含一个名为操作系统的基本程序集合。在这个集合里,最重要的程序称为内核(Kernel)。当操作系统启动时,内核被装入到RAM中,内核中包含了系统运行所必不可少的很多核心过程(procedure)。

操作系统必须完成主要的两个重要目标:

  • 与硬件部分交互,为包含在硬件平台上的所有低层可编程部件提供服务。
  • 为运行在计算机系统上的应用程序(用户程序)提供执行环境。

类Unix操作系统把与计算机物理组织相关的所有底层细节都对用户运行的程序进行隐藏。当程序想要使用硬件资源时,必须项操作系统发出请求。内核对这个请求进行评估,如果允许使用这个资源,那么,内核代表应用程序与相关的硬件部分进行交互。

为了实现这种机制,现代操作系统依靠特殊的硬件特性来禁止用户程序直接与底层硬件部分交互,或者禁止直接访问任意的物理地址。特别是,硬件为CPU引入了至少两种不同的执行模式:用户程序的非特权模式和内核的特权模式。Unix把它们分别称为用户态内核态

多用户系统

多用户系统就是一台能并发和独立地执行分别属于两个或多个用户地若干应用程序地计算机
“并发”意味着几个应用程序能同时处于活动状态并竞争各种资源,如CPU,内存,硬盘等。
“独立”意味着每个应用程序能执行自己地任务,而无需考虑其他用户的应用程序在做什么。
当然从一个应用程序切换到另一个应用程序,会使每一个应用程序的速度有所减慢,从而影响用户看到的响应时间。

多用户操作系统必须包含以下几个特点

  • 核实用户身份的认证机制
  • 防止有错误的用户程序妨碍其他应用程序在系统中运行的保护机制。
  • 防止有恶意的用户程序干涉或窥探其他用户的活动保护机制。
  • 限制分配给每个用户的资源数的计帐机制。

为了确保实现这些安全保护机制,操作系统必须利用与CPU特权模式相关的硬件保护机制,否则,用户程序将能直接访问系统电路并克服强加于它的这些限制。

用户和组

在多用户操作系统中,每个用户在机器上都有私用空间;典型地,他拥有一定数量地磁盘空间来存储文件、接收私人邮件信息等等。操作系统必须保证用户空间地私有部分仅仅对其拥有者可见。

所有用户由一个唯一的数字来标识,这个数字叫用户标识符(UID)。通常一个计算机系统只能由有限的人使用。当其中一个用户开始一个工作会话时,操作系统会有一个认证机制。

为了和其他用户有选择地共享资料,每个用户是一个或多个用户组成员,组由唯一的用户组标识符(user group ID)标识。每个文件也恰好与一个组相对应。

任何地类Unix操作系统都有一个特殊用户,root(超级用户)。操作系统对root用户不使用通常地保护机制,可以访问任意一个文件,干涉每一个正在执行地用户程序。

进程

所有的操作系统都使用一种基本的抽象:进程。
一个进程可以定义为:“程序执行时的一个实例”,或者一个运行程序的执行上下文
在传统的操作系统中,一个进程在地址空间中执行一个单独的指令序列。地址空间是允许进程引用的内存地址集合。现代操作系统允许具有多个执行流的进程。也就是说,在相同的地址空间可执行多个指令序列

运行进程并发活动的系统称为多道程序系统或多处理器系统。区分程序和进程是非常重要的:几个进程能并发的执行同一程序,而同一个进程能顺序地执行几个程序。

每个进程都自以为他是系统中唯一的进程,可以独占操作系统所提供的服务。只要进程发出系统调用,硬件就会把特权模式由用户态变为内核态,然后进程以非常有限的目的开始一个内核过程的执行。当内核过程完成,进程又退回到用户态,然后进程从系统调用的下一条指令继续执行。

内核体系结构

单块内核:从整体上把内核作为一个大过程来实现,同时也运行在一个单独的地址空间上。因此单内核通常以单个静态二进制文件的形式存放于磁盘中。而进程管理、内存管理等是其中的一个个模块,所有内核模块都在这样的一个大内核地址空间上运行。模块之间可以直接调用相关的函数。效率高,紧凑性强。大多数的Unix系统都设计为单内核。Linux也是一个单内核,也就是说,Linux内核运行在单独的内核地址空间上。

微内核
微内核并不作为一个单独的大过程来实现,微内核的功能被划分为多个独立的进程程,进程程之间保持独立并运行在各自的地址空间上。
微内核是一种功能更贴近硬件的核心软件,它一般仅仅包括基本的内存管理、同步原语、进程间通信机制、IO操作和中断管理(只是将OS中最核心的功能加入内核),这样做有利于提高可扩展性和可移植性,但微内核与文件管理、设备驱动、虚拟内存管理、进程管理等其他上层模块之间需要有较高的通信开销。

所以为了达到微内核理论上的很多优点而又不影响性能,linux内核提供了模块(module)。模块是一个目标文件,其代码可以运行时链接到内核或从内核解除链接。这种目标代码通常由一组函数组成,用来实现文件系统、驱动程序或其他内核上层功能。与微内核操作系统外层不同,模块不是作为一个特殊的进程执行。相反,与任何其他静态链接的内核函数一样,它代表当前进程在内核状态下执行。

使用模块化的主要优点:
模块化方法:
因为任何模块都可以在运行时被链接或解除链接,因此,系统程序员必须提出明确的软件接口以访问由模块处理的数据结构。这使得开发新模块变得容易。
平台无关性:
即使模块依赖于某些特殊的硬件特点,但它不依赖于某个固定的硬件平台。
节省内存使用:
当需要模块功能时,把它链接到正在运行的内核中,否则,将该模块解除链接。这种机制对于小型嵌入式系统是非常有用的。
无性能损失:
模块的目标代码一旦被链接到内核,其作用与静态链接的内核目标代码完全等价。因此,当模块的函数被调用时,无需显式地进行消息传递。

Unix文件系统概述

Unix操作系统的设计集中反应在其文件系统上

文件

Unix文件是以字节序列组成的信息载体,内核不解释文件的内容。从用户的角度看,文件被组织在一个树结构的命名空间中。如下
深入理解Linux内核(一)——Linux操作系统基础概念_第1张图片

除了叶节点之外,树的所有节点都表示目录。与树的根相对应的目录叫根目录。
绝对路径:由/开头,即从顶层的根目录开始的路径,就是绝对路径
相对路径:由./或…/开头,即从当前路径或父目录路径开始的路径,称为相对路径。

硬链接和软链接

硬链接创建命令
ln p1 p2
即为由路径p1标识的文件创建一个路径为p2的硬链接。作用就是为了防止误删系统中的重要文件。其本质就是一个inode结点。和源文件的inode节点一模一样的。删除了源文件路径,依旧可以通过该硬链接访问文件,文件并不会删除。

硬链接的限制:

  • 不允许给目录创建硬链接。因为这可能把目录树变为环形图,从而就不可能通过名字定位文件。
  • 只有在同一个文件系统中的文件之间才能创建硬链接。现代Unix系统可能包含了多种文件系统,这些文件系统位于不同的磁盘或分区,用户也许无法知道它们之间的物理划分。

为了克服硬链接的这些限制,就此诞生了软链接
创建命令:
ln -s p1 p2
表示:创建一个p2新的软链接,指向路径名为p1.
软链接就好比Windows下的快捷方式,通过软链接可以导向文件真实存在的路径,删掉软链接并不会真的删除文件。(但是删除源文件,软链接就无效了,这就区别于硬链接)。

适用于任何的文件系统,且可以给目录创建软链接,会自动导向真实路径。

文件类型

Unix文件可以分为以下类型

  • 普通文件(-)
  • 目录(d)
  • 符号链接(l)
  • 面向块的设备文件(b)
  • 面向字符设备的文件(c)
  • 管道和命名管道文件(p)
  • 套接字Socket(s)
    前三种文件是Unix文件系统的基本类型

设备文件与I/O设备以及集成到内核中的设备驱动程序相关。例如,当程序访问设备文件时,它直接访问与那个文件相关的I/O设备

管道和套接字是用于进程间通信的特殊文件

文件描述符与索引节点

Unix对文件的内容和描述文件的信息给出了清楚的区分。除了设备文件和特殊文件系统文件外,每个文件都是由字符序列组成。文件内容不包含任何控制信息,如文件长度或文件结束符(EOF)

文件系统处理文件需要的所有信息包含在一个名为索引节点(inode)的数据结构中。每个文件都有自己的索引节点,文件系统用索引节点来标识文件。

索引节点包含属性:

  • 文件类型
  • 与文件相关的硬链接数
  • 以字节为单位的文件长度
  • 设备标识符(即包含文件的设备的标识符)
  • 在文件系统中的标识文件的索引节点号
  • 文件拥有者的UID
  • 文件的用户组ID
  • 几个时间戳,表示索引节点状态改变的时间、最后访问时间及最后修改时间
  • 访问权限和文件模式

访问权限和文件模式

文件的潜在用户分为三种类型:

  • 作为文件拥有者的用户
  • 同组用户,不包含所有者
  • 所有剩余的用户(其他用户)

有三种类型的权限:读、写、可执行 通过wrx表示,所以一共有9种不同的二进制来标记。
还有三种附加的标记,suid,sgid,sticky用来定义文件的模式。当这些标记用到在可执行文件时有如下含义:
suid: 设置可执行文件的suid标志位,就获得了该文件拥有者的UID
sgid:设置了可执行文件的sgid的标志位,就获得了该文件用户组的ID
sticky:设置了sticky标志位的可执行文件相当于向内核发送一个请求,当程序执行结束后,依然将他保留在内存中。(这个标志已经过时,目前使用基于代码页共享的其他方法)

文件操作的系统调用

当用户访问一个普通文件或目录文件的内容时,他实际上是访问存储在硬件块设备上的一些数据。从这个意义上来说,文件系统是硬盘分区物理组织的用户视图。因为处于用户态的进程不能直接和底层硬件打交道,所以每个实际的文件操作必须在内核态下进行。因此,Unix操作系统定义了几个与文件操作有关的系统调用

打开文件

进程只能访问“打开的”文件。问了打开一个文件,进程调用系统调用:
fd=open(path,flag,mode)
path:打开文件的路径
flag:打开文件的方式(读、写、读/写,追加)
mode:指新创建的文件的访问权限。
返回值是一个文件描述符(文件对象),注意打开文件的同步问题,如果需要上锁,可以通过flock()函数实现上锁。

为了创建一个新的文件,进程也可以调用create()系统调用,他与open()非常相似,都是由内核来处理。

访问打开文件

对于普通Unix文件,可以顺序访问,也可以随机访问,而对设备文件和命名管道文件,通常只能顺序地访问。在这两种访问方式中,内核把文件指针存放在打开文件对象中,也就是说,当前位置就是下一次进行读或写地位置。

顺序访问是文件默认访问方式,即read(),write()系统调用总是从文件指针地当前位置开始读/写。为了修改文件指针的值,必须在程序中显式地调用
lseek()系统调用。
nowoffset=lseek(fd,offset,whence)
fd:文件描述符
offset:偏移量,有符号值
whence:表示文件指针新位置,有文件头,当前位置,文件末尾三个选项。

read()系统调用需要以下参数
nread=read(fd,buf,count);
fd:文件描述符
buf:读取出的数据存储缓冲区
count:一次读操作读取的数据字节个数。

write和read相似,当读到文件结束符EOF,read就会返回EOF表示已经读到文件末尾(读完了),同时在读的过程中,文件指针会自动地加1。

关闭文件

当进程无需访问文件时,就调用系统调用
res=close(fd)
fd:文件描述符
当一个进程终止时,内核会关闭其打开的所有仍然打开着的文件

更名及删除文件

系统调用更名:
res=rename(oldpath,newpath);
只是改了文件链接的名字。
删除文件系统调用:
res=unlink(pathname);减少文件链接数,删除了对应的目录项。只有当链接项为0时,文件才被真正删除。

Unix内核概述

Unix内核提供了应用程序可以运行的执行环境。因此,内核必须实现一组服务及相应的接口。应用程序使用这些接口,而且通常不会与硬件资源直接交互。

进程/内核模式

当一个程序在用户态下执行时,他不能直接访问内核数据结构或内核的程序。然而,当应用程序在内核态下运行时,这些限制不再有效。一个程序大部分时间都处于用户态下,只有需要内核所提供的服务时才切换到内核态。当内核态满足了程序的请求后,它让程序又回到用户态下。

进程是动态的实体,在系统内通常只有有限的生存期。创建、撤消及同步现有进程的任务都委托给内核的一组例程来完成。

内核本身并不是一个进程,而是进程的管理者。进程/内核模式假定:请求内核服务的进程使用所谓系统调用的特殊编程机制。每个系统调用都设置了一组识别进程进程请求的参数,然后执行与硬件相关的CPU指令完成从用户态到内核态的转换。

进程实现

为了让内核管理进程,每个进程由一个进程描述符表示,这个进程描述符包含有关进程当前状态的信息。

当内核暂停一个进程执行时,就把几个相关处理器寄存器的内容保存在进程描述符中。这些寄存器包括:

  • 程序计数器(PC)和栈指针(SP)寄存器
  • 通用寄存器
  • 浮点寄存器
  • 包含CPU状态信息的处理器控制寄存器
  • 用来跟踪进程对RAM访问的内存管理寄存器

当内核决定恢复执行一个进程时,它用进程描述符中合适的字段来装载CPU寄存器。因为程序计数器中所存的值指向下一条将要执行的指令,所以进程从它停止的地方恢复执行。

可重入内核

所有的Unix内核都是可重入的,这意味着若干进程可以同时在内核态下执行。

提供可重入的一种方式是编写函数,以便这些函数只能修改局部变量,而不能改变全局数据结构。这样的函数叫可重入函数。

如果一个硬件中断发生,可重入内核能挂起当前正在执行的进程,即使这个进程处于内核态。

内核控制路径表示内核处理系统调用,异常或中断所执行的指令序列。

进程地址空间

每个进程运行在它的私有地址空间。在用户态下运行的进程涉及到私有栈,数据区和代码区。当在内核态运行时,进程访问内核的数据区和代码区,但是使用另外的私有栈。

进程间也能共享一部分地址空间,以实现一种进程间通信,这就是System V引入并且已经被Linux支持的“共享内存”技术。

同步和临界区

实现可重入内核需要利用同步机制:如果内核控制路径对某个内核数据结构进行操作时被挂起,那么,其他的内核控制路径就不应当再对该数据结构进行操作,除非它已经被重新设置成一致性状态。否则,两个控制路径的交互作用将破坏所存储的信息。

非抢占式内核

非抢占式内核就是指的进程在内核态执行时,它不能被挂起,也不能被另一个进程替代。只要进入了内核态,就一直到满足需求退回到用户态为止。但是这种内核设计在多处理器系统上运行是抵消的。

禁止中断

单处理器系统上的另一种同步机制是:在进入一个临界区之前禁止所有硬件中断,离开时再重新启动中断。这种机制尽管简单,但是不是最佳。如果临界区较大,那么在临界区中停留的时间相对较长,这一段较长的时间内持续禁止中断可能使所有的硬件活动处于冻结状态。

此外,由于多处理器系统中禁止本地CPU上的中断是不够的,所以必须使用其他同步技术。

信号量

广泛使用的一种同步技术就是是信号量。信号量仅仅是与一个数据结构相关的计数器。所有内核线程在试图访问这个数据结构之前,都要检查这个信号量。可以把每个信号量看成一个对象,其组成如下:

  • 一个整数变量
  • 一个等待进程的链表
  • 两个原子方法:down()和up()
    down()的方法对信号量的值减一,如果这个新值小于0,该方法就把正在运行的进程加入到这个信号量链表,然后阻塞该进程(即调用调度程序)。up()方法对信号量的值加1,如果这个新值大于或等于0,则激活这个信号链表中的一个或多个进程。

每个要保护的数据结构都有自己的信号量,其初始值为1.

自旋锁

自旋锁和信号量相似,但没有进程链表;当一个进程发现锁被另一个进程锁着时,他就会不停的“旋转”,执行一个紧凑的循环指令直到锁打开。

避免死锁

死锁:进程P1获得了访问数据a的权限,但是在等待进程P2释放b的访问权限,进程P2拿到了b的访问权限,但是在等待P1释放a的访问权限。这样一个互等的状态就是死锁。

信号和进程间通信

Unix信号(signal)提供了把系统事件报告给进程的一种机制。每种事件都有自己的信号编号,通常用一个符号常量来表示。

进程可以用两种方式对接收到的信号做出反应:

  • 忽略信号
  • 异步执行一个指定的过程(信号处理程序)
    如果进程不指定选择何种方式,内核就根据信号的编号执行一个默认的操作。五种默认操作是:
  • 终止进程
  • 将执行上下文和进程地址空间内容写入一个文件(核心转储),并终止进程
  • 忽略信号
  • 挂起进程
  • 如果进程曾被暂停,则恢复它的执行

SIGKILL和SIGSTOP信号不能直接由进程处理,也不能直接忽略

AT&T的Unix System V引入了在用户态下其他种类的进程间通信机制,很多Unix内核也采用了这些机制:信号量、消息队列以及共享内存。它们被统称为System V IPC。

进程管理

Unix在进程和它正在执行的程序之间做出了清晰的划分。fork()和_exit()系统调用发别来表示创建一个新进程和终止一个进程,而调用exec()类系统调用则是装入一个新程序。
exit() 和_exit()函数的区别就是_exit()不会刷新流,exit是一个C库函数,_exit()是一个系统调用

调用fork()进程的是父进程,而新进程是它的子进程。父子进程能相互找到对方,因为描述每个进程的数据结构包含两个指针,一个指向父亲,一个指向子进程。
实现fork()一种天真的方式就是将父进程的数据与代码都复制,并把这个拷贝赋予子进程
_exit()系统调用终止一个进程。内核对这个系统调用的处理就是通过释放进程所拥有的资源并向父进程发送SIGCHILD信号来实现

僵死进程

父进程通过wait()系统调用函数来查询子进程是否已经终止了。wait()系统掉调用允许进程等待,直到其中的一个子进程结束;它返回已终止子进程的进程标识符(PID)。
孤儿进程:就是父进程已经结束了,但是子进程还没结束,则子进程就没了父亲。出现这种情况一般该子进程会被init进程收养,但是有的设置的是被最近的祖宗收养。
僵死进程:子进程结束了,但是父进程没有接收到子进程结束的信号,导致该子进程的资源无法被回收。

进程组和登录会话

每个进程描述符包括一个包含进程组ID的字段。每个进程组可以有一个领头进程(即其PID与这个进程组ID相同的进程)。新创建的进程最初被插入到其父进程的进程组中。

内存管理

虚拟内存

所有新近的Unix系统都提供了一种有用的抽象,叫虚拟内存。虚拟内存作为一种逻辑层,处于应用程序的内存请求与硬件内存管理单元(MMU)之间。虚拟内存有很多用途和优点:

  • 若干进程可以并发地执行
  • 应用程序所需内存大于可用物理内存时也可以运行。
  • 程序只有部分代码装入内存时进程可以执行它
  • 允许每个进程访问可用物理内存的子集。
  • 进程可以共享库函数或程序的一个单独内存映象。
  • 程序是可重定位的,也就是说,可以把程序放在物理内存的任何地方。
  • 程序员可以编写与机器无关的代码,因为它们不必关心有关物理内存的组织结构。

虚拟内存子系统的主要成分是虚拟地址空间的概念。进程所用的一组内存地址不同于物理内存地址。当一个进程使用虚拟地址时,内核和MMU协同定位其在内存中的实际物理位置。

随机访问存储器(RAM)的使用

所有的Unix操作系统都将RAM毫无意义地划分为两部分,其中若干兆字节专门用于存放内核映象。RAM的其余部分通常由虚拟内存系统来处理,并且用在以下三种可能的方面:

  • 满足内核对缓冲区,描述符及其他动态内核数据结构的请求。
  • 满足进程对一般内存区的请求及对文件内存映射的请求。
  • 借助于高速缓存从磁盘及其他缓冲设备获得较好的性能。

内核内存分配器(KMA)

内存内核分配器是一个子系统,它试图满足系统中所有部分对内存的请求。其中一些请求来自内核其他子系统,它们需要一些内核使用的内存。还有一些来自用户程序的系统调用,用来增加用户进程的地址空间。一个好的KMA应该具有如下特点:

  • 必须快,因为它由所有的子系统调用
  • 必须把内存的浪费减到最少
  • 必须努力减轻内存碎片问题
  • 必须能与其他内存管理子系统合作,以便借用和释放页框。

几种KMA算法:

  • 资源图分配算法
  • 2的幂次方空闲链表
  • McKusick-Karels分配算法
  • 伙伴系统
  • Mach的区域分配算法
  • Dynix分配算法
  • Solaris的Slab分配算法

进程虚拟地址空间处理

内核通常用一组内存区描述符描述进程虚拟地址空间。内核分配给进程的虚拟地址空间由以下内存区组成:

  • 程序的可执行代码
  • 程序的初始化数据
  • 程序的未初始化数据
  • 初始程序栈(用户态栈)
  • 所需共享库的可执行代码和数据
  • 堆(由程序动态请求内存)

高速缓存

物理内存的一大优势就是用作磁盘和其他块设备的高速缓存。因为硬盘非常的慢,与RAM的访问时间相比,太长了。所以设置了一个高速缓存Cache。用于提高对磁盘中的数据访问。

通过sync()系统调用把所有"脏"的缓冲区(即缓冲区的内容与对应磁盘块的内容不一样)写入磁盘中来强制磁盘同步。(周期性的)

设备驱动程序

内核通过设备驱动程序与I/O设备交互。设备驱动程序包含在内核中,由控制一个或多个设备的数据结构和函数组成,这些设备包括硬盘,键盘,鼠标,监视器,网络接口以及连接到SCSI总线上的设备。通过特点的接口,每个驱动程序与内核中的其余部分(甚至与其他驱动程序)相互作用这种方式的优点有如下:

  • 可以把特定设备的代码封装在特定的模块中
  • 厂商可以在不了解内核源代码而只知道接口规范的情况下,就能增加新设备。
  • 可以把设备驱动程序写成模块,并动态的加载到内核而不用重新启动内核。

你可能感兴趣的:(深入理解Linux内核,linux,unix)