绪论
Linux是一个真正的Unix内核,但他不完全是Unix操作系统,他不包含全部Unix应用程序。
如
- 文件系统实用程序
- 窗口系统、图形化界面
- 系统管理员命令
- 文本编辑程序
- 编译程序
与Unix的比较
现有便准只指定 application programming interface API, 也就是一个定义好的环境,但没有对内核内部施加限制
为定义通用用户接口,类Unix内核多采用相同设计思想特征
Linux有Unix操作系统全部特点,如
- 虚拟内存
- 虚拟文件系统
- 轻量级进程
- Unix信号量
- SVR4进程间通信
- 支持对称多处理器
Linux如何和其他商用Unix内核竞争?
采纳集中不同Unix内核最好的特征与设计选择
- 单块内核结构
- 编译后静态链接的传统Unix内核
动态load和unload部分内核代码,这种部分代码一般叫模块(module) - 内核线程
内核线程是可以被独立调度的执行环境。线程切换比进程切换代价小得多,因为前者在同一个地址空间执行
Linux以十分有限的方是用内核线程周期性执行内核函数,但不代表基本执行上下抽象。 - 多线程应用支持
程序应用多用相对独立执行流设计。但执行流共享多数数据结构。。
多线程用户程序 consists of 许多轻量级进程
对同一地址空间、物理内存页、打开文件操作
Linux把轻量级进程当基本执行上下文,按照clone()
系统调用处理 - 抢占式内核
- 多处理器支持
- 文件系统
默认Ext2
避免崩溃检查 Ext3
小文件 ReiserFS - STREAMS
Linux特别的优点
- Free
- 成分自定
- 可运行于地段便宜硬件平台上
- 速度快
- 稳定
- 内核小
- 可运行于低端硬件上
- 与别的操作系统兼容
- 技术支持良好
基本概念
完成两个主要目标
- 与硬件部分交互,为低层可编程部件服务
- 为运行在计算机系统上应用程序提供执行环境
为此设定用户态(禁止访问物理地址,或程序直接与低层硬件交互)与内核态
多用户系统
一台并发、独立执行分别属于两个或者多个用户的若干应用程序计算机
并发:多个应用程序处于活跃状态竞争资源(CPU、内存、硬盘
独立:每个程序可执行自己的任务
特点
- user身份认证
- 防止程序妨碍其他程序运行的保护机制
- 防止程序干涉、窥视其他用户活动的保护机制
- 限制分配资源记账机制
用户与组
OS要保证user 空间四有部分只对拥有者可见
用户用一个数字表示,用户标识符(UID)
共享材料则加入一个组,也有用户组标识符表示(UGID)
特殊用户root有一切权限,访问每一个文件、干涉每一个用户程序活动
进程
程序运行的一个实例,或运行程序的“执行上下文”。
传统操作系统中:进程在地址空间执行单独指令序列。 地址空间(允许进程引用内存地址的集合)
现代操作系统:允许多个执行流进程、同一地址空间执行多个指令序列
多用户系统:允许并发活动(多道程序系统or 多处理系统)
如何区别程序与进程?
多进程并发执行同一程序,一个进程可顺序执行几个程序
单处理器系统只能一个进程占用CPU,调度程序决定哪个进程可以执行。多用户系统进程必须是抢占式——OS记录进程占有CPU实践,周期性激活调度函数
若没有用户登录与程序运行,仍然有系统进程检测外围设备
类Unix操作系统使用进程\内核模式,每个进程认为它是系统中唯一进程且独占OS服务,只要进程发出系统调用,硬件将用户态转换为内核态(OS在进程执行上下文中起作用)
内核体系结构
单块结构:每个内核层集成到内核程序中,代表当前进程在内核态运行
微内核:内核只有小的函数集合(同步原语、调度程序、通信机制)实现以前OS实现的功能(内存分配、设备驱动、系统调用)
微内核
- 劣势:OS不同层次显示消息传递花费代价
- 优势:程序员通过模块化方法,定义明确清晰的软件接口与其他层交互。 且容易一直到其他体系结构上(因为硬件相关内容封装在微内核代码中)
Linux利用微内核的优点且不影响ing能,提供模块(module)
本质上是目标文件(多数由一组函数组成实现文件系统、驱动程序),运行的时候可以连接到内核或接触内核链接。
区别于微内核,模块不是作为特殊进程执行,而代表当前进程在内核态进行
模块的优点:
- 模块化方法(模块运行时链接或接触链接,程序员要给出明确软件接口)
- 平台无关性
- 节省内存使用
- 无性能损失(模块目标代码链接内容等价于静态链接内核的目标代码)
文件概述
文件
内核不解释文件内容。
用户视角,文件组织在树结构命名空间
除叶节点外,树所有节点为目录名
命名:
除了/
与\0
以外的ASCII字符序列组成,通常不超过255字符长度。
根目录按照惯例名字为/
同一目录下文件不重名
Unix每个进程有一个当前工作目录,属于进程执行上下文
标识文件的时候,进程使用路径名。
if第一个字符是斜杠,为绝对路径,因为起点是根目录
else是相对路径,起点是进程当前目录
表示文件名时候,用.
表示当前工作目录,..
表示父目录
硬软连接
ln P1 P2
为路径P1标识文件创建一个路径名为P2的硬链接
- 不允许给目录创建硬链接,否则会产生环形图
- 同一文件系统中的文件才可以创建链接
克服上述限制,引入软连接(符号链接)
ln -s P1 P2
创建一个路径名为P2的新软连接,P2指向路径名P1
执行命令,文件系统创建出P2目录部分,在那个目录下创建名为P2的符号连接类型新项,其包含路径名P1
文件类型
- 普通文件
- 目录
- 符号链接
- 面向块的设备文件 block-oriented device file
- 面向字符的设备文件 character
- 管道pipe与命名管道(也叫FIFO
- 套接字
描述符与索引节点
除了设备文件和特殊文件,文件都有字符序列组成,内容不含控制信息,如文件长度或EOF符
文件所需要的所有信息包含在**索引节点(inode)**当中
包含如下属性
- 文件类型
- 文件相关硬链接个数
- 字节为单位的文件长度
- 标识符(文件、设备的标识符)
- 文件系统中标识文件的索引节点号
- 拥有者UID
- 用户组ID
- 时间戳(状态改变、最后访问、最后修改)
- 访问权限与模式
访问权限与文件模式
用户分类
- 文件所有者
- 同组用户
- 其余用户
三类权限:读、写、执行
因此需要九种不同的二进制标记。还有三种附加标记
suid(Ser User ID) sgid(Set Group ID) 以及sticky定义文件模式
suid:
进程执行文件 需要保持进程拥有者的 UID
如果设置了可执行文件suid标志位,进程获得文件拥有者的UID
sgid
进程执行文件 需要保持进程拥有者所在组的 ID
设置sgid获取ID
sticky
设置sticky标志位可执行文件相当于向内核发出请求,程序执行结束后将它保留在内存(已过时
文件操作的系统调用
打开
fd = open(path, flag, mode)
path:打开文件的路径
flag:指定打开方式(读写追加)
mode:新创建文件的访问权限
创建“打开文件对象”,返回文件描述符的标识符
“打开文件对象”内容如下
- 文件操作的数据结构;表示文件当前位置的
offset
字段,从该位置进行下一个操作(文件指针) - 一些内核函数指针。 函数的集合由
flag
决定
POSIX语义
- 文件描述符:进程与打开文件的交互,包括交互相关数据。
同一打开文件对象可由同一进程几个文件描述符标识 - 几个进程同时打开同一文件
文件系统给每个文件分配一个单独打开文件对象和单独的文件描述符。Unix不提供同步机制。但系统调用如flock()
可让进程在文件上对I/O操作同步
除了open()
,可以使用create()
系统调用,他们非常相似,都由内核处理
访问打开的文件:
普通UNIX可以顺序 or 随机 访问。
对于设备文件与命名管道文件,通常只能顺序访问。
两种访问方是,内核把文件指针存放在打开文件对象中—— 当前位置是下一次读写操作的位置
顺序访问是默认的,read()
和write()
总从文件指针当前位置开始读或写,所以需要改变指针位置。使用lseek
函数
newoffset = lseek(fd, offset, whence)
# fd:打开文件的文件描述符
# offset 有符号的整数值,表示指针新位置
# whence 指定newoffset的计算方式,可以是offset+0(表示从头移动),也可以是offset+当前文件位置或文件末尾
文件描述符可以参考这个文章彻底弄懂 Linux 下的文件描述符(fd)
read()
也要调用如下参数
nread = read(fd, buf, count)
# 描述符
# buf:进程地址空间的缓冲区地址,数据就在这个缓冲区
# count:所读字节数
内核尝试从fd文件中读取count个字节,他的起始位置是打开文件的offset字段的当前值。可能会遇到文件结束,空管道的情况从而导致无法读出count个字节。
返回的nread为实际度的字节数。原来的值加上nread会更新文件指针。write()
可以直接参考read()
关闭文件
res = close(fd);
释放fd打开的对象
更名与删除文件
没对这个文件内容起作用,而是对一个或多个目录内容起作用
res = rename(oldpath, newpath);
改文件连接名字
res = unlink(pathname);
减少文件连接数,删除目录项。(当连接数为0才是真正意义上的删除
Unix内核概述
进程/内核模式
CPU课运行在用户态下,也可运行在内核态。
有一些CPU可以有两种以上执行状态。
程序执行:多数为用户态
- 不可直接访问内核数据结构
- 不可直接访问内核程序
程序只有需要内核提供服务才会转换到内核态,完成相应服务后又转回用户态
进程是动态的实体,通常只有优先生存期。需要由内核的一组例程完成 创建、撤销、同步进程等任务。
内核不是进程,而是进程的管理者。
进程/内核模式假定: 请求内核服务 的进程 使用 system call的特殊编程机制——调用识别请求参数在执行CPU指令转换
Unix内有内核线程的特权进程,可以
- 以内核态运行在内核地址空间
- 不与user直接交换,无需终端设备
- system启动时创建,直到关闭一直活跃
单处理器系统:只有一个进程执行
用户态
−
−
系统调用
−
>
内核态
−
−
定期中断
−
>
用户态
用户态 --系统调用->内核态 --定期中断->用户态
用户态−−系统调用−>内核态−−定期中断−>用户态
此时若进程1在内核态且定时中断,进程切换就会发生,会让进程2进入用户态,然后进程2等待中断处理请求,得到信号后进程2进入内核态处理中断。
还有几种激活内核例程的方法
- 进程调用系统调用
- 执行进程的CPU发出exception信号——如无效指令
- 外围设备向CPU发出interrupt信号通知事件发生——请求、状态变化、IO操作
中断信号需要中断处理程序(在内核)处理
中断不可预知,因为外围设备与CPU异步操作 - 内核线程被执行
进程实现
内核管理进程 --> 进程有进程描述符
暂停进程:
相关处理器内容存在进程描述符,包括
- 程序计数器PC
- 栈指针寄存器SP
- 通用寄存器
- 浮点寄存器
- 包含CPU状态信息的处理器控制寄存器
- 跟踪进程对RAM访问的内存管理寄存器
内核决定恢复进城的时候,他要用进程描述符中字段装在CPU寄存器
(用来返回“这个进程”被停止时候的环境)
让程序计数器指回下一条要执行的指令
若进程不再CPU执行而在等待事件,Unix内核会区分多种等待状态,由进程描述符队列实现
可重入内核
若干个进程可同时在内核态下执行
单处理器:只有一个进程,许多进程在内核态下被阻塞
example: 内核的某一个进程发出读磁盘请求,磁盘控制器处理这个请求,并恢复执行其他进程(此时发出请求的进程不占用内核资源,转换为堵塞)。设备满足读请求后,发出中断通知内核表示这个进程可以恢复执行
方法一:可重入函数(只修改局部变量)
方法二:包含非重入函数,利用锁机制保证一次只有一个进程执行一个非重入函数
硬件中断发生-->可重入内核挂起当前执行进程(无论是否处于内核态)
提高发出中断的设备控制器的吞吐量
设备发出中断--> 等待CPU应答
若内核可以快速应答,设备控制器在CPU处理中断的时候仍能执行其他任务
不发生意外,CPU从第一条指令到最后一条指令顺序执行内核控制路径,以下情况将交错执行
- 进程(用户态)调用系统调用 -> 内核路径证明无法立刻满足 -> 选择一个新的进程投入运行 ----> 进程切换发生。。。第一个内核路径没完成 and CPU开始执行其他内核路径,导致两个不同进程在执行
- 运行一个内核控制路径,检测异常( 如缺页 ) -> 第一个被挂起 (若分配新页过程结束后),这个控制路径可以恢复执行。 两个路径可以表示同一进程
- CPU运行启用中断的内核控制路径,一个硬件中断发生。内核路径没执行完的话 -> CPU会开一个新的内核控制路径处理中断。等中断处理程序终止,第一个内核控制路径恢复
(这个时候这两个路径都在同一进程的上下文中,CPU时间都算入该进程) - 支持抢占式调度内核中,更高优先级进程加入就绪队列
一些例子
- 用户态运行一个进程(user
- 异常处理程序活系统调用处理程序(excp
- 运行一个中断处理程序(intr
进程地址空间
user态进程 : 私有栈、数据区、代码区
kernel态进程:数据与代码,但使用其他私有栈
内核控制路径也可以引用自己的私有内核栈
有时进程之间共享地址空间,这些共享由进程提出,也可以由内核自动完成
example:一个程序被几个用户同时使用,则该程序只被装入内存一次,指令被需要的用户共享。但数据不被共
存,此时共享的地址空间被内核自动完成
Linux支持mmap
系统嗲用,允许存放在快设备上文件或信息的一部分映射到进程部分地址空间
同步区与临界区
内核控制路径操作数据结构的时候被挂起,其他内核控制路径不能对数据结构操作,除非设置成“一致性”状态。
全局变量V包含某个系统资源可用橡树
第一个内核控制路径A读这个变量确定有可用资源,此时另一个内核控制路径B 被激活来读V,V的值为1,然后B再对V减一并使用这个资源。然后A恢复执行,但A读V的值,认为V是1,然后也对V进行了-1操作,最后导致V显示为-1.
两个内核控制路径使用相同的资源项
计算结果取决于调度两个以上进程时,代码是不正确的,存在竞争条件
通常使用元组操作保证读V-1是单独、不可中断的操作
下面讨论同步控制内核路径
非抢占式内核
多用于单处理器系统,在多处理系统上比较低效,运行不同CPU上两个内核控制路径并发访问相同数据结构
禁止中断
进入一个临界区之间禁止所有硬件中断,离开的时候再重新启用中。
不是最佳方法:临界区较大会导致长时间持续禁止中断,导致硬件活动处于冻结状态。而且多处理器系统禁止本地CPU中断时不够的,还需要使用其他技术
信号量
一个数据结构相关计数器,所有内核线程访问这个数据结构前,要先检查信号量
其组成如下
- 一个整数变量
- 一个等待进程链表
- 两个原子方法
down()
与up()
down就是对信号量-1.小于0的话就让正在运行的进程加入链表中,然后阻塞该进程。当值变化为自然数的时候再激活进程
把上述“进程”改为内核访问路径即可
自旋锁
信号量在多处理器系统中不总是解决同步问题的最佳方案。系统不允许不同CPU上运行内核控制路径同时访问某些数据结构
若修改数据结构需要时间比较短,那可能会导致检查信号量、挂起信号量这些操作耗时过场,此时可能其他内核控制路径已经释放信号量
于是多操作系统使用了自旋锁,类似于信号量但没有进程链表。进程发现锁被其他进程锁的时候,不停的while循环直到指令打开
单操作系统大概率直接被系统挂起
死锁
p1占用a需求b,p2占用b需求a,导致循环等待
信号与进程通信
Unix 信号可以把系统时间报告给进程,多用符号常量表示,如SIGTERM
- 异步通告
用户终端按下中断减额(CTRL+C)向前台发射信号 - 同步错误或异常
进程访问内存非法地址,内核向进程发送SIGSEGV
信号
进程可以以两种方式对接收到的信号
- 忽略该信号
- 异步执行指定的过程——信号处理程序
若进程不指定选择对接方式,内核根据信号编号执行操作
- 终止进程
- 执行的上下文和进程地址空间内容写入一个文件(核心转储, coredump)后终止进程
- 忽略信号
- 挂起进程
- 恢复被暂停执行的进程
进程管理
Unix在进程和执行程序之间作出清晰划分
fork()
——创建新进程
_exit()
——终止一个进程
exec()
——装入一个新程序,当系统调用执行后,进程在装入程序的全新地址空间恢复运行
调用fork()的进程称为父进程,新进程是其子进程,进程数据结构包含两个指针,分别指向其父进程与子进程
实现fork()可以将父程序数据代码拷贝到子进程——费时
所以使用写时复制技术,把页复制延迟到最后时刻(父或子需要写一个页才复制这个页
_exit()
系统调用终止一个进程且内核对这个系统嗲用处理是通过释放进程拥有的资源且向父进程发送SIGCHLD信号实现的
僵死进程
父进程查询子进程是否终止wait4()
系统调用允许进程等待,直到骑宠一个子进程结束;返回已终止子进程的进程标识符(Process ID, PID)
内核执行这个系统调用检查子进程是否终止
此处引入僵死进程为了表示终止的进程:
父进程执行完wait4()
钱,进程一直停留在那种状态。
系统调用处理程序从进程描述符字段获取资源使用数据。
if 得到数据
释放进程描述符
else if 没有子进程结束
该进程设置为等待状态直到子进程结束
若父进程未调用wait4()
就直接终止?
解决方法:使用叫init
特殊进程系统。当该进程终止,内核改变所有子进程描述符指针,让他们称为init
的子进程,被其监控,且init
也会发出wait4()
系统调用,副作用为除掉所有僵死进程
进程组和登陆对话
引入进程组概念表示“作业”抽象
如shell执行如下语句
ls | sort | more
为上面三个进程创建了一个组,让他们看起来像是一个实体
进程描述符包括一个 字段,字段并包含了进程组ID
进程组中可以有一个PID与进程组ID相同的进程,称之为领头进程
新创建进程插入其父进程进程组中
内存管理
虚拟内存
优点
- 若干个进程并发执行
- 应用程序所需内存大于可用物理内存也可运行
- 程序中部分代码装入内存时,也可以执行
- 允许所有进程访问可用物理内存的子集
- 进程可以是库函数、程序的单独内存映像
- 程序可重定位
- 程序员可以编写与机器无关的代码,因为他们不关心物理内存组织结构
、
RAM随机访问储存器的使用
RAM分为i两部分,一部分放内核映像(内核代码和内核静态数据结构)其余部分用虚拟内存系统处理
- 满足内核对缓冲区、描述符、动态内核数据结构请求
- 满足进程对一般内存区的请求以及对文件内存映射请求
- 借助高速缓存从磁盘以及其他缓冲设备获取较好性能
可用的RAM有限,需要在平衡请求的种类数量。
到达阈值的时候,可用页框回收算法释放其他内存
虚拟内存还需要解决内存碎片,通常要求内核使用物理上连续内存区域
内核内存分配器
KMA是一个子系统,它试图满足系统中所有部分对内存的请求
好的KMA具有以下特点
- 快,因为它由所有内核子系统调用
- 减少内存浪费
- 减轻内存碎片问题
- 与其他内存管理子系统合作来借用释放页框
根据不同算法技术有如下KMA
- 资源图分配算法
- 2次幂空闲链表
- McKusickKarels分配算法
- 伙伴系统Buddy
- mach的Zone分配算法
- Dynix分配算法
- Solaris的Slab分配算法
进程虚拟地址空间处理
进程虚拟地址空间包括了 所有虚拟内存地址(进程可以引用)。
内核通常用一组内存区描述符描述进程虚拟地址空间,如进程通过exec()
调用某个开始程序执行的时候,内核分配进程虚拟空间组成如下
- 程序可执行代码
- 程序初始化数据
- 未初始化数据
- 初始程序栈(用户态栈
- 所需共享库的可执行代码、数据
- 堆(动态申请的内存
内存分配策略:调页
进程可在页没在内存的时候开始执行
if 进程访问不存在的页
MMU产生异常
异常处理程序找到受影响内存区
分配空闲页
用适当数据初始化
if 进程调用malloc()或brk()
系统调用动态请求内存,内核仅仅修改进程堆内存区大小
only if 引用虚拟内存地址产生异常的时候
才给进程分配页框
虚拟内存一般用更高效的方法写时复制
高速缓存
物理内存的优势之一
由于硬盘访问对比RAM访问时间过长,因此早期UNIX系统使用策略:尽可能推迟磁盘时间
比如当访问数据:先检查是否在缓存中,在的话就不必再访问磁盘
sync()
把所有脏的缓冲区(缓冲区内容与对应磁盘块内容不一致,也就是被修改过的)数据写入磁盘强制同步避免数据丢失
设备驱动程序
内核通过device driver与IO设备交互
设备驱动程序包含在内核中控制的多个设备的数据结构和函数组成,包括硬盘键盘鼠标网络接口以及连接到SCSI总线上的设备等。
- 可以把特定设备代码封装在特定模块中
- 厂商可在不了解内核源代码,只知道接口规范情况下增加新的设备
- 内核统一对待所有设备,通过相同接口访问设备
- 设备驱动模式写成模块,然后可以动态把他们装进内核,不必重新启动系统。也可以动态卸载模块减少储存在RAM内核映像的大小
当用户程序操作硬件设备的时候,程序就用系统调用和/dev目录下发出请求。
实际上设备文件是设备驱动程序接口中用户可见部分。每个设备文件配备专门设备驱动程序,执行硬件设备的请求