参考
系统调用
用户态切换到内核态方法:
中断、异常、系统调用
定义
系统调用: 操作系统用户态进程与硬件设备进行交互的一组接口
当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。
系统调用通过软中断实现
封装例程(wrapper routine,唯一目的就是发布系统调用, 一般每个系统调用对应一个封装例程
API:库再用这些封装例程定义出给用户的API,API只是一个函数定义
调用过程
应用程序->封装例程->系统调用处理程序->系统调用服务例程
传参
系统调用也需要输入输出参数
实际的值: system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即系统调用号 由使用eax寄存器传递
用户态进程地址空间的变量的地址
- 甚至是包含指向用户态函数的指针的数据结构的地址
实现方法:
在系统调用汇编指令之前,系统调用的参数被写入CPU的寄存器。然后,在进入内核态调用系统调用服务例程之前,内核再把存放在CPU寄存器中的参数拷贝到内核态堆栈中。因为毕竟服务例程是C函数,它还是要到堆栈中去寻找参数的
使用寄存器传递参数具有如下限制:
1)每个参数的长度不能超过寄存器的长度,即32位
2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp) ?超过6个怎么办?
验证参数
在内核打算满足用户的请求之前,必须仔细的检查所有的系统调用参数
1) 如write()系统调用,fd参数是一个文件描述符,sys_write()必须检查这个fd是否确实是以前已打开文件的一个文件描述符,进程是否有向fd指向的文件的写权限,如果有条件不成立,那这个处理程序必须返回一个负数
2) 只要一个参数指定的是地址,那么内核必须检查它是否在这个进程的地址空间之内,有两种验证方法:
a 验证这个线性地址是否属于进程的地址空间(费时. 大多数情况下,不必要)
b 仅仅验证这个线性地址小于PAGE_OFFSET(高效. 可以在后续的执行过程中,很自然的捕获到出错的情况. 从linux2.2开始执行第二种检查)
在内核中,可以访问到所有的内存, 要防止用户将一个内核地址作为参数传递给内核,这将导致它借用内核代码来读写任意内存
传递返回值
服务例程的返回值是将会被写入eax寄存器中
这个是在执行“return”指令时,由编译器自动完成的
系统调用号
内核利用了一个系统调用分派表(dispatch table)。 这个表存放在sys_call_table数组中,有若干个表项(2.6.26中,是356):
第n个表项对应了系统调用号为n的服务例程的入口地址的指针
访问进程的地址空间
系统调用服务例程需要非常频繁的读写进程地址空间的数据
get_user/put_user/ copy_to_user/ copy_from_user etc.
内核对进程传递的地址参数只进行粗略的检查
访问进程地址空间时的缺页,可以有多种情况
- 合理的缺页:来自虚存技术. 页框不存在或者写时复制
- 由于错误引起的缺页(内核对进程传递的地址参数只进行粗略的检查)
- 由于非法引起的缺页
非法缺页的判定
内核中,只有少数几个函数/宏会访问用户地址空间
对于一次非法缺页,一定来自于这些函数/宏
可以将访问用户地址空间的指令的地址一一列举出来,当发生非法缺页时,根 据引起出错的指令地址来定位
在缺页异常do_page_fault中,若最后发现是非法缺页,就会执行下面的操作->找到修正代码
Linux Time
定时测量
- 获得当前的时间和日期. 系统调用:time(), ftime()以及gettimeofday()
- 维持定时器 settimer(), alarm()
定时的硬件设备
硬件电路: 基于固定频率振荡器和计数器
硬时钟
1, 实时时钟Real time clock,RTC
Linux本身只使用RTC获得时间和日期
对应的设备文件为/dev/rtc
2, 时间戳计数器Time stamp counter,TSC
外:在80x86微处理器中,有一个CLK输入引线.接收外部振荡器的时钟信号
内:很多80x86微处理器都引入了一个TSC(一个64位的、用作时间戳计数器的寄存器)
它在每个时钟信号(CLK)到来时+1
rdtsc指令用于读该寄存器
Linux中
Linux在系统初始化的时候必须确定时钟信号CLK的频率(即CPU的实际频率)
tsc_calibrate(根据在一个相对较长的时间间隔内(约5ms)所发生的TSC计数的个数进行计算)
那个间隔由可编程间隔定时器给出
3, 可编程间隔定时器Programmable interval timer, PIT
经过适当编程后,可以周期性的给出时钟中断
通常是8254 CMOS芯片
Linux将PIT编程为: 100Hz、1000Hz, 通过IRQ0发出时钟中断, 每若干毫秒(100Hz为10ms)产生一次时钟中断,即一个tick
4, CPU本地定时器 5, 高精度事件定时器 6, ACPI电源管理定时器
Linux的计时体系结构
- 更新自系统启动以来所经过的时间
- 更新时间和日期
- 确定当前进程的执行时间,考虑是否要抢占
- 更新资源使用统计计数
- 检查到期的软定时器
在单处理器系统中,所有定时活动都由IRQ0上的时钟中断触发,包括
- 在中断中立即执行的部分,和
- 作为下半部分延迟执行的部分
计时体系结构中的关键数据结构和变量
- 系统时钟system timer
时钟中断发生源
- Jiffies变量
记录系统自启动以来系统产生的tick数, 每次时钟中断+1
jiffies,32位.约50天就溢出
关于jiffies_64.数十亿年才会溢出
- 计时时钟源
时钟源抽象,参见数据结构clocksource
是系统时钟源,定义了系统时钟源的接口
缺省时钟源,Jiffies时钟源
- Xtime变量
存放当前时间和日期
时间纪元: 1970年1月1日(UTC)午夜
基本上每个tick更新一次,根据时钟源来更新xtime的秒数和纳秒数
Linux内核中与时间有关的程序
timer_interrupt->do_timer_interrupt_hook
a->do_timer(jiffies)->update_times(Xtime)
b->update_process_times
实现CPU分时、更新系统时间、维护软定时器
定时器是一种软件功能,它允许在将来的某个时刻调用某个函数
大多数设备驱动程序利用定时器完成一些特殊工作
由于软定时器在下半部分处理,内核不能保证定时器正好在时钟到期的时候被执行,会存在延迟,不适用于实时应用
Linux中存在两类定时器:
1, 动态定时器.内核使用(驱动常用)
动态定时器被动态的创建和撤销,当前活动的动态定时器个数没有限制
创建并激活一个动态定时器
- 创建一个新的timer_list对象
- 调用init_timer初始化,并设置定时器要处理的函数和参数
- 设置定时时间
- 使用add_timer加入到合适的链表中
- 通常定时器只能执行一次,如果要周期性的执行,必须再次将其加入链表
动态定时器应用之delayed work
动态定时器应用之schedule_timeout
延迟函数: udelay, ndelay
2, 间隔定时器.由进程在用户态创建
与定时测量相关的系统调用及相关服务例程
与定时测量相关的系统调用
- - time()
- :返回从1970年1月1日凌晨0点开始的秒数
- - ftime()
- :返回从1970年1月1日凌晨0点开始的秒数以及最后一秒的毫秒数
- 数据结构为timeb
- - gettimeofday()
- :返回从1970年1月1日凌晨0点开始的秒数
- ,对应于sys_gettimeofday()
- - settimer()
- :间隔定时器
- ,频率:周期性的触发定时器(若为0,只触发一次)
- - alarm()
- 引起SIGALARM信号
与时钟相关的命令
date:显示或者更改系统时钟
使用time获得时钟
使用ctime改变时钟格式
进程地址空间
进程最多能访问4GB的线性地址空间
但进程在访问某个线性空间之前,必须获得该线性空间的许可
因此,一个进程的地址空间是由允许该进程访问的全部线性地址组成
内核使用线性区资源来表示线性地址空间
每个线性区由起始线性地址、长度和一些存取权限描述
线性区的开始和结束都必须4KB对齐
进程获得新线性区的一些典型情况:
- 刚刚创建的新进程
- 使用exec系统调用装载一个新的程序运行
- 将一个文件(或部分)映射到进程地址空间中
- 当用户堆栈不够用的时候,扩展堆栈对应的线性区 ……
1 内核态和用户态分配内存的不同
内核态
内核中的函数以直接了当的方式获得动态内存
- 内核是操作系统中优先级最高的成分。
- 内核信任自己
- 采用页面级内存分配和小内存分配
用户态
给用户态进程分配内存时,请求被认为是不紧迫的,用户进程不可信任
当用户态进程请求动态内存时,并没有立即获得实际的物理页框,而仅仅获得对一个新的线性地址区间的使用权
这个线性地址区间会成为进程地址空间的一部分,称作线性区(memory areas)
2 线性区(memory area)
比如0x08048000——0x0804C000这段线性地址空间被分配给了一个进程,进程就可以访问这段地址空间
进程只能访问某个有效的memory area。进一步讲,这个area可以被标志为只读或者不可执行(nonexecutable)
如果进程试图访问一个有效的area之外的地址或者用不正确的方式访问一个有效的area,内核将通过段异常(segmentation fault)杀死这个进程
线性区中可以包含各种内容
- 可执行文件代码段的内存映射,就是.text section
- 数据段的内存映射,.data section
- zero page的内存映射用来包含未初始化的全局变量,.bss section
- 为库函数和链接器附加的代码、数据、bss段
- 文件的内存映射
- 共享内存的映射
- 匿名内存区域的映射,比如通过malloc()函数申请的内存区域
进程地址空间中所有有效的线性地址都确定的存在于一个area中, memory areas不重叠
进程中每个单独的area对应一个不同内存区: 堆栈、二进制代码、全局变量、文件映射等等
每个线性区由一个vm_area_struct结构来表示
- 这个结构描述了一段给定的内存区间
- 区间中的地址都有同样的属性,比如同样的存取权限和相关的操作函数
- 用这个结构可以表示各种线性区,比如映射可执行的二进制代码的线形区、用作用户态堆栈的线形区等等
线性区的存取权限
vm_flags域描述有关这个线性区全部页的信息。例如,进程访问每个页的权限是什么。还有一些标志描述线性区自身,例如它应该如何增长
- VM_READ, VM_WRITE, VM_EXEC
- VM_SHARED
- VM_RESERVED
- VM_GROWSUP
线性区的链表和红黑树
通过内存描述符中的两个域mmap和mm_rb都可以访问线性区。事实上,它们都指向了同一个vm_area_struct结构,只是链接的方式不同
mmap指向的线性区链表用来遍历整个进程的地址空间
- 红黑树mm_rb用来定位一个给定的线性地址落在进程地址空间中的哪一个线性区中
- mmap_cache用来缓存最近用过的线性区
处理线性区
(内核进程需要对一个线性区进行处理,比如确定一个给定线性地址是否存在于一个线性地址空间中)
- find_vma(),查找一个线性地址 两个参数:进程内存描述符的地址mm和线性地址addr
- find_vma_intersection(),查找一个与给定地址区间重叠的线性区
- get_unmapped_area(),查找一个空闲的地址区间 arch_get_unmapped_area, shm_get_unmapped_area
insert_vm_struct(),向内存描述符链表中插入一个线性区
mmap()和do_mmap(),创建一个线性区
- munmap()和do_munmap(), do_munmap()函数从进程地址空间中删除一段线性空间
3 缺页异常(i386中14号异常)
如前所述,内核只是通过mmap()等调用分配了一些线性地址空间给进程,并没有真正的把实际的物理页框分配给进程
当进程试图访问这些分配给它的地址空间时,比如一段线性地址空间映射的是二进制代码,则进程被调度执行的时候会跳转到这个地址上去执行。
此时,并没有物理页框对应于这些线性地址,从而会引发一个缺页异常
缺页异常处理程序处理缺页异常。
缺页异常处理程序do_page_fault
它可以判断出这是不是一个合法的缺页异常,如果是,则负责给这段线性地址分配一些物理页框并把磁盘中对应的文件写入这些物理页框
这样进程得以正常运行。
程序的执行
操作系统是如何通过可执行文件的内容建立进程的执行上下文的?
程序以可执行文件的形式存放在磁盘上
库(静态库 vs 共享库)可供很多程序使用的一些例程的集合
命令行参数、环境变量等
1 可执行文件
可执行文件是一个普通的文件,它描述了如何初始化一个新的进程上下文
Fork + execve
用户使用shell来执行某个程序时,可以指定命令行参数
Shell本身不限制命令行参数的个数,但它受限于命令自身
库
源文件->目标文件->可执行文件
最小的程序也会利用到C库
例如:void main(void) {}
要为main的执行建立执行上下文
在进程结束时,杀死进程(在main的最后插入exit())
静态链接 vs 动态链接
静态链接: 静态库.Gcc的-static选项指明使用静态库
动态链接:共享库
程序段和进程的线性区
在逻辑上,Unix程序的线性地址空间被划分为各种段(segment)
- 正文段,text
- 数据段,data
- Bss段
- 堆栈段
此外,还有共享库和文件的映射,他们映射在其他线性区
2 可执行格式
Linux标准的可执行格式 ELF:Executable and Linking Format
旧版的可执行文件格式 a.out:Assembler OUT put format
其他 MS-DOS的exe文件, UNIX BSD的COFF文件
Linux对可执行文件格式的处理
在系统启动时,所有编译进内核的可执行格式都被注册: register_binfmt/unregister_binfmt
在系统运行过程中,也可以注册一个新的可执行文件格式
使用linux_binfmt对象管理
Linux通过可执行文件的扩展名或者存放在文件前128字节的magic数来识别文件格式
3 Exec函数
系统调用:execve->sys_execve
do_execve->search_binary_handler(找到一个可以识别执行文件的handler)->load_binary(对于elf格式的文件:load_elf_binary)
do_execve->open_exec
用一个指定的可执行文件所描述的上下文代替进程的上下文
文件系统
1 Unix文件系统概述
1.1 文件
Unix文件是以字节序列组成的信息载体
内核不解释文件的内容
文件的组织
文件被组织成一个树状的命名空间
- 文件:叶结点
- 目录:根节点(根目录“/”)和中间节点 ; 目录使用一个目录名标识。 ; 目录节点包含它下面的文件及子目录的所有信息
文件名和目录名
- 不能使用“/”和字符“\0”,其他ASCII字符都OK
- 长度:不同的文件系统有不同的限制,通常<256个字符
- 同一个目录下,不允许文件重名;不同目录下无妨
当前工作目录
Unix的每个进程都有一个当前工作目录,current working directory,属于进程的执行上下文
绝对路径:用来在命名空间中指定一个特定的文件,以“/”打头,表示以根目录作为起点,\形如 “/home/chenxl/sample/test.c”
相对路径:以当前工作目录作为起点 例如当前目录为“/home/chenxl”,则上述文件的相对路径可以是“sample/test.c”
“.”和“..”
- 前者表示当前工作目录
- 后者表示当前工作目录的父目录
- 若当前工作目录是根目录“/”,那么“.”和“..”相等
2 硬链接和软链接
硬链接(hard link)
- 一般情况下,一个常见的文件名代表了到对应文件的一个硬链接
- 一个文件可以有不同的硬链接,他们可以在同一个目录下,也可以在不同的目录下,因此一个文件可以有不同的文件名
下面的命令可以创建一个硬链接.其中p1指明一个现有的文件的路径名,p2指明新建立的硬链接的路径名
$ ln p1 p2
硬链接的限制
- 不允许用户给目录创建硬链接
- 只有在同一个文件系统的文件之间才能创建硬链接
软链接(symbolic link)
- 符号链接是一种特殊的文件(短文件),它包含另一个文件的任意一个路径名
- 可以指向任意一个文件系统的任意文件,甚至一个不存在的文件
下面的命令可以创建一个软链接.p2指明新建立的软连接的路径名。在实现上,文件系统抽出p2的目录部分,在此目录下创建一个符号链接文件
$ ln -s p1 p2
3 文件类型
Unix文件的类型可以是
- 基本类型:常规文件(regular file)、目录、符号链接
- 设备和驱动相关:面向块的设备文件、面向字符的设备文件
- 用于进程间通信:管道和命名管道、套接字
4 文件描述符与索引节点
文件系统处理文件所需要的所有信息都包含在索引节点inode中
每个文件都有自己的inode
一般而言,inode应当包含如下信息
- 文件类型
- 与文件相关的硬链接的个数
- 以字节为单位的文件的长度
- 设备标识符(即包含文件的设备的标识符)
- 在文件系统中标识文件的inode号
- 文件拥有者的UID
- 文件的GID
- 一些时间戳:inode状态的改变时间、文件的最后访问时间、最后修改时间
- 访问权限和文件模式
5 访问权限和文件模式
文件潜在的用户分为3种类型
- 文件所有者
- 同组用户
- 其他用户
访问权限:读、写、执行
每类用户都有这三种权限 文件的访问权限使用9个不同的bit来表示
此外还有三种附加标记 suid、sgid、sticky
- Suid:运行该文件时,将以文件所有者的权限来运行,如果文件所有者是root的,容易成为漏洞
执行某个可执行文件的时候,进程通常具有进程拥有者的UID,但是若可执行文件设置了SUID标记,则进程将拥有文件所有者的UID
- Sgid:只对目录有效,在这个目录下创建的所有文件拥有与目录所属组相同的组
进程的GID将不是进程拥有者的GID,而是文件所有者所在组的GID
- Sticky:例如tmp目录下的文件,只有文件所有者能够删除。
告诉内核,就算一个程序运行结束了,也暂时将程序保留在内存中
在一个文件被创建的时候:
- 一个文件的属主UID就是创建这个文件的进程的UID
- 一个文件的属主GID可能是: 创建这个文件的进程的GID 、若文件的父目录设置了SGID标记,则该文件将继承父目录的GID
6 文件操作的系统调用
- 打开/关闭int open(const char *pathname, int flags);
- 打开/关闭int open(const char *pathname, int flags, mode_t mode);
- 创建int creat(const char *pathname, mode_t mode);
- 读/写ssize_t write(int fd, const void *buf, size_t count);
- 删除int unlink(const char *pathname);
- 定位off_t lseek(int fildes, off_t offset, int whence);
- 更名int rename(const char oldpath, const char newpath);
2 Linux的虚拟文件系统
2.1 虚拟文件系统VFS的作用
VFS是一个软件层,用来处理与Unix标准文件系统相关的所有系统调用。 是用户应用程序与文件系统实现之间的抽象层
能为各种文件系统提供一个通用的、统一的接口
Linux与其他类Unix系统一样,采用虚拟文件系统VFS来达到支持多种文件系统格式的目标
VFS在一个简单文件复制操作中的作用
从一种文件系统复制到另一文件系统。对于cp命令而言,它不需要知道/floppy/TEST和/tmp/test分别是什么文件系统类型。它通过VFS提供的系统调用接口进行文件操作
2.2 VFS支持的文件系统类型
基于磁盘的文件系统:它们管理在本地磁盘分区中可用的存储空间
Linux使用的文件系统:ext2、ext3、ReiserFS Unix家族的文件系统:SYSV文件系统,UFS,MINIX文件系统以及VERITAS VxFS 微软公司的文件系统:MS-DOS、VFAT以及NTFS ISO9660CD-ROM文件系统和通用磁盘格式的DVD文件系统 其他有专利权的文件系统,如HPFS、HFS、AFFS、ADFS 起源于非Linux系统的其他日志文件系统,JFS,XFS
网络文件系统:用于访问属于其他网络计算机的文件系统所包含的文件
NFS、Coda、AFS、SMB、NCP
特殊文件系统
不同于上述两大类 不管理具体的磁盘空间 /proc
各种不同的文件系统通过mount(挂载、安装)到根文件系统中
- 在Linux中,根文件系统即根目录所代表的文件系统
- 通常是ext2文件系统
VFS的基本思想:引入一个通用文件模型,这个模型能够表示所有支持的文件系统
- 对于一个具体实现的文件系统,在处理时,需要将其进行概念上的转换, 例如,在通用文件模型中,目录被看成是普通文件
- 在实现上, read()->sys_read->file数据结构->f_op->MS_DOS文件操作指针(其中的read操作)
- 类似面向对象的概念
2.3 通用文件模型有下列对象类型组成
- 超级块对象(superblock object) 存放文件系统相关信息:例如文件系统控制块
- 索引节点对象(inode object) 存放具体文件的一般信息:文件控制块/inode
- 文件对象(file object) 存放已打开的文件和进程之间交互的信息
- 目录项对象(dentry object) 存放目录项与文件的链接信息
2.4 VFS所处理的系统调用
- mount、umount:挂载/卸载文件系统
- sysfs :获取文件系统信息
- statfs、fstatfs、ustat :获取文件系统统计信息
- chroot :更改根目录
- chdir、fchdir、getcwd :操纵当前工作目录
- mkdir、rmdir :创建/删除目录
- getdents、readdir 、link 、unlink 、rename :对目录项进行操作
- readlink 、symlink :对软链接进行操作
- chown 、fchown 、lchown :更改文件所有者
- chmod 、fchmod 、utime :更改文件属性
- open、close、create …
上述大部分操作之需要与通用文件模型中的一些对象打交道,而不需要真正操作具体的文件系统和文件,因此可以把VFS看成是一个“通用”的文件系统,在必要时依赖某种具体的文件系统
2.5 VFS的数据结构
2.5.1 一个具体的文件系统:超级块对象:super_block****
- 所有超级块链表:super_blocks :s_list域
- 文件系统特有信息:s_fs_info域,例如ext2_sb_info
- 脏标志:s_dirt域
- 文件系统特有方法s_op:super_operations数据结构及域,例如ext2_sops
每种文件系统具有自己的super_block结构
每个文件系统具有一个所属文件系统类型的super_block实例
2.5.2 一个具体的文件:Inode对象:inode
Inode特有的方法 inode_operations
文件特有的方法 file_operations
2.5.3 目录项对象:dentry
目录项操作:dentry_operations
2.5.4 一个打开文件:文件对象:file
文件操作指针f_pos
文件对象特有的方法
专用高速缓存:“filp”,filp_cachep
2.5.5 与进程相关的文件
文件系统相关信息fs_struct
打开文件相关信息files_struct
2.6 文件系统类型
特殊文件系统
用来为系统程序员、系统管理员等提供一种容易的方式来操作内核的数据结构并实现操作系统的特殊特征
procfs / sysfs etc.
常用的特殊文件系统
文件系统类型的注册.在系统初始化期间,register_filesystem()用来注册编译时指定的每个文件系统 相应的文件系统对象被插入到file_systems链表中
2.7 文件系统安装
文件系统的挂载
一个文件系统的根目录是系统目录树的根目录,那个这个文件系统就是根文件系统
2.8 路径名查找
标准查找操作
父路径名的查找
符号链接的查找 2.9 VFS系统调用的实现
xxx->sys_xxx
open->sys_open
2.10 文件加锁
当多个进程访问同一个文件时,会出现同步问题:写同一个文件的同一个位置、 对同一个文件的同一个位置,1读1写、 或者更复杂的情况
Unix提供对文件的加锁机制,可避免上述冲突。 POSIX标准规定了基于fcntl()系统调用的文件加锁机制
- 强制锁 vs 劝告锁
劝告锁(advisory lock):需要进程主动参与 Fcntl、flock、lockf
强制锁(mandatory lock):内核强制检查
- 读锁 vs 写锁
读锁:多个进程可以读共享
写锁:只能一个进程写,并且与读锁互斥
Linux支持所有的文件加锁方式
3 Ext2文件系统简介
EXT2文件系统是EXT文件系统的升级,在Linux中得到了广泛的使用。
3.1 EXT2文件系统的磁盘组织
除了引导扇区之外,EXT2磁盘分区被顺序划分为若干个磁盘块组(Block Group)。
每个块组由若干个磁盘块,按照相同的方式组织,具有相同的大小。
EXT2磁盘块组中的磁盘块按顺序被组织成:
- 一个用作超级块的磁盘块。 在这个磁盘块里,存放了文件系统超级块的一个拷贝;
多个块组中的超级块形成冗余。在某个或少数几个超级块被破坏时,可用于恢复被破坏的超级块信息。
- N个记录组描述符的磁盘块;
组描述符用来描述一个磁盘块组的相关信息
- 1个记录数据块位图的磁盘块;
- 1个记录索引结点位图的磁盘块;
EXT2中所有的索引结点大小相同,都是128个字节。
- N个用作索引结点表的磁盘块;
EXT2的一个磁盘块组中的索引结点存储在一组连续的磁盘块中,形成一个索引结点表。
这组磁盘块中的第一个磁盘块的块号存储在超级块的bg_inode_table数据项中。
根据磁盘块的大小,可以计算出每个磁盘块能容纳多少个索引结点
根据索引结点的总个数,可以计算出索引结点表所需要占用的磁盘块的个数。
关于索引节点中的i_block[]
ext2的索引结点中使用了组合索引方式。
前12项用作直接索引
第13项用作间接索引
第14项用作二次间接索引
第15项用作三次间接索引
- N个用作数据块的磁盘块。
EXT2的空闲盘块分配算法采用了位图法
每个位(bit)都对应了一个磁盘块
2个位图分别占用一个专门的磁盘块。
根据磁盘块的大小,可以计算出每个块组中最多能容纳的数据块个数和索引节点块个数。
3.2 EXT2文件系统的目录项和支持的文件类型
在EXT2中,目录是一种特殊的文件,这种文件的数据块中存放了该目录下的所有目录项
EXT2在目录项中存放了文件的类型信息。文件类型可以是0~7中的任意一个整数。它们分别代表如下含义:
0:文件类型未知;
1:普通文件类型;
2:目录;
3:字符设备;
4:块设备;
5:有名管道FIFO;
6:套接字;
7:符号链接。
3.3 创建一个ext2文件系统
mke2fs的缺省参数
磁盘块大小:1024字节
分片:目前不支持,因此与磁盘块一样
分配inode的个数:1/8192B
永久保留的块的个数:5%
创建流程
- 初始化超级块和组描述符
- Optionally, 检查是否有坏块,若有创建坏块列表
- 对每个块组,保留所有用来存放超级块、组描述符、inode表、2个位图的磁盘块
- 初始化每个块组中的位图
- 初始化每个块组中的inode表
- 创建 /root 目录
- 创建 lost+found 目录(供e2fsck 使用,与坏块相关)
- 为上述两个目录而更新位图信息
- 若有坏块,则将其在 lost+found 目录中组织起来
3.4 Ext2提供的各种对象方法
超级块对象方法:super、inode write ...
索引节点对象方法:设置属性,目录rmdir mkdir 等
文件对象方法:read write open
3.5 管理ext2的磁盘空间
涉及到如下操作:
创建/删除一个索引节点
数据块的寻址
文件空洞
分配/释放一个数据块
文件内块号 vs. 逻辑块号
根据数据在文件中的偏移可以计算逻辑块号:
- 首先计算出文件内块号 =(偏移f-1)/块大小的商 +1
- 根据索引信息,查询到逻辑块号
文件空洞:
A file hole is a portion of a regular file that contains null characters and is not stored in any data block on disk
文件空洞可以节省磁盘空间
Ext2通过数据块的动态分配来实现这一点:当且仅当一个进程要写数据到文件中的时候才真正分配磁盘块
进程间同步和通信
1 进程间通信(IPC,Inter-Process Communication)
Unix系统提供的基本的IPC包括:
- 1、管道和FIFO(有名管道)
- 2、消息
- 3、信号量
- 4、共享内存区
- 5、套接字
1.1 管道(pipe)
管道是半双工的,数据只能向一个方向流动;
一个进程将数据写入管道,另一个进程从管道中读取数据
数据的读出和写入:写入的内容每次都添加在管道**缓冲区**的末尾,每次都是从缓冲区的头部读出数据.
需要双方通信时,需要建立起两个管道;
只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
在shell中使用管道的例子: |(前输出到后输入) >(输出重定向) <(输入重定向)
管道可看成是被打开的文件,但并没有真实的文件与之对应
创建一个管道
pipe()系统调用用来创建一个新的管道
#include <unistd.h>
int pipe(int filedes[2]);
- filedes[0]只能用于读,称为管道读端;
- filedes[1]只能用于写,称为管道写端。
- 若试图从写端读,或者向读端写都将导致错误发生。
一般文件的I/O函数都可用于管道,如close、read、write等
管道只能在具有亲缘关系的进程之间进行通信,通过fork传递管道的描述符
管道的一个重大限制是它没有名字,因此只能用于具有亲缘关系的进程间通信
1.2` 有名管道(named pipe或FIFO)
特殊的文件类型:
- 1,严格遵循先入先出的读写规则
- 2,类似管道,在文件系统中不存在数据块,而是与一块内核缓冲区相关联
- 3,有名字,FIFO的名字包含在系统的目录树结构中,可以按名访问
FIFO的操作
mkfifo
fopen fwrite fclose fread
以及:open,close,read,write等普通文件操作
1.2 消息队列
消息队列就是一个消息的链表
可以把消息看作一个记录,具有特定的格式以及特定的优先级。
对消息队列有写权限的进程可以按照一定的规则向消息队列添加新消息;
对消息队列有读权限的进程则可以从消息队列中读走消息。
消息队列的创建
int msgget(key_t key, int msgflg)
发送消息
int msgsnd(int msqid, //目标消息队列
struct msgbuf *msgp, //待发送的消息
int msgsz, //消息的大小
int msgflg); //标志
接收消息
int msgrcv(int msqid, //msqid为消息队列描述字
struct msgbuf *msgp, //消息返回后存储这里
int msgsz, //指定消息内容的长度
long msgtyp, //请求读取的消息类型
int msgflg);
消息队列的其他操作
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
1.3 信号量
Semphore,用来对资源进行并发控制访问
semop用来获取或释放信号量对应的资源
1.4 共享内存
允许两个或多个进程通过把公共数据放入一个共享内存区来访问它们
shmget:获得或创建一个共享内存区的IPC标志符
shmctl:控制
shmat : 将一个共享内存区“附加”到一个进程上, 使得进程可以访问共享内存区的内容 进程通过shmaddr指定并获得共享内 存区在该进程中的起始地址
shmdt: 将指定位置的共享内存区从进程中分离出去
1.5 套接字socket
套接字不仅可以用来实现网络间的进程通信,也可以用来实现本地的进程间通信
相关调用包括:
Socket
Listen
Bind
Connect/accept
Send/recv,read/write
Close
…
Linux中的信号
1 信号
信号是很短的消息(标准信号没有给参数、消息或是其他相随的信息留有空间)
通常使用一个数字来标识一个信号
信号可以被发送到一个进程或一组进程。
1.1 信号的产生和处理方式
软件中断的概念
信号是典型的异步事件(当然也有一些事件是同步错误或异常)
大多数产生信号的事件对进程而言是随机出现
进程必须告诉内核“在某个信号发生时,应该执行如下操作”,这点跟中断处理例程相似
跟硬件中断一样,任何动作,包括终止进程,都只能由接收到信号的进程来执行,也就是在本进程的上下文中执行
1.2 信号的作用
a让进程知道已经发生了一个特定的事件
b强迫进程执行它自己代码中的信号处理程序
- 很多应用程序提供自己的信号处理程序
- 系统也会定义一些缺省的信号处理程序
1.3 信号的生成
a 异常
当一个进程出现异常(比如试图执行一个非法指令,除0,浮点溢出等),内核通过向进程发送一个信号来通知进程异常的发生
b 其他进程
一个进程可以通过kill或是sigsend系统调用向另一个进程或一个进出组发送信号。一个进程也可以向自身发送信号
c 终端
某些键盘字符如ctrl+c等会向终端的前台进程发送信号
d 作业控制
发送信号给那些想要读或写终端的后台进程。比如shell使用信号来管理前台和后台进程
e 配额限制
当一个进程使用超过分配给它的cpu时间或是文件大小的限制,内核发送一个信号给这个进程
f 通知
一个进程也许要求能被通知某些事件的发生。比如设备已经就绪等待I/O操作
g 闹钟
定时器产生的信号,由内核发送给进程
1.4 信号传递的两个不同阶段
a 信号产生
内核更新进程描述符中跟信号相关的数据结构来表示一个信号被发送给了这个进程
b 信号传递
内核强迫目标进程通过以下方式对信号作出反映:
- 或改变目标进程的执行状态,
- 或开始执行一个特定的信号处理程序,
- 或者两者都是
1.5 挂起信号
已经产生但还没有传递的信号称为挂起信号。
任何时候,一个进程仅存在给定类型的一个挂起信号,同一进程同种类型的其他信号不被排队,只被简单的丢弃。
信号的挂起时间长度往往不可预知,原因在于:
- 信号通常只被current进程传递
- 进程可以选择阻塞某种信号。这种情况下,在取消阻塞之前进程将不接收这个信号
- 当进程执行一个信号处理程序函数时,通常屏蔽相应的信号,即自动阻塞这个信号直到处理程序结束。因此,所处理的信号的另一次出现不能中断信号处理程序
1.6 信号的应答方式和响应时机
进程以三种方式对一个信号做出应答
1,显式的忽略这个信号(多数信号都可以使用这种方式进行处理。)
2,执行系统默认的缺省操作,可以是:
Terminate:进程被杀死
Dump:进程被杀死,且如果可能,创建包含进程上下文的可用于调试的core文件
Ignore:简单的忽略信号
Stop:进程被停止,状态置为TASK_STOPPED
Continue:如果进程被挂起,则状态置为TASK_RUNNING。否则忽略该信号
3,捕获信号
为了执行用户希望的对某个事件的处理,可以由用户指定某个信号的处理函数。
有两种信号不可以被显式的忽略、捕获或阻塞:SIGKILL和SIGSTOP。因为它们向超级用户提供一种终止或停止进程的可靠的方法
内核在如下时机检查进程的信号
1,从系统调用/中断返回到用户态之前,在ret_from_intr中执行这个检查
这个检查几乎在每个定时中断时都发生(约10ms)
代码在i386\kernel\entry_32.S中
2,进程从一个可中断的事件醒来后
处理这样的信号,即信号可能在进程运行期间的任一时刻请求把进程切换到一个信号处理函数,并在这个函数返回以后恢复原来进程的执行
2 信号的产生
内核通过调用send_sig send_sig_info force_sig等
函数来产生信号。这些函数只是更新目标进程的进程描述符相关的域。但在条件满足的情况下它们可以唤醒进程让目标进程接收信号
3 传递信号
内核在返回到用户态时调用do_signal()来处理非阻塞的挂起信号:
参数:struct pt_regs *regs; //pt_regs结构,指向当前进程内核态堆栈中保存的寄存器
do_signal()一位一位的检查当前被挂起的非阻塞信号,其编号由dequeue_signal()返回,对应于上面介绍的action结构中指定的处理方法:
- 如果是SIG_IGN(忽略信号)
- 如果是SIG_DFL(缺省操作)
- 如果信号有一个专门的处理程序,do_signal就调用handle_signal()强迫执行该处理程序
4 Handle_signal
信号处理程序是用户态进程所定义的函数,并且包含在用户态的代码段中
Handle_signal运行在内核态,而信号处理程序运行在用户态
问题:
- 1,必须返回用户态执行信号处理程序
- 2,必须按照原来进入内核的方式返回用户态
- 3,一旦返回用户态,内核堆栈就被清空,如何保存内核堆栈的内容
Linux采用的解决办法:
- 把保存在内核态堆栈中的上下文拷贝到当前进程的用户态堆栈中
- 建立好信号处理程序所需的堆栈环境
- 当信号处理程序运行结束时,调用sigreturn()系统调用把上面保存的内核堆栈的内容再拷贝回内核堆栈
- 然后正常返回
关于系统调用的重新执行
5 与信号处理相关的系统调用
kill(pid, sig)系统调用
发送信号,对应于sys_kill()
对于pid的值
1,如果大于0,发送信号给指定的进程
2,如果=0,把信号发送给同组的所有进程
3,如果=-1,把信号发送给除0号、1号以及current进程之外的所有进程
4,如果小于-1,把信号发送给指定的进程组中的所有的进程
sigaction(sig, act, oact)系统调用
允许用户为信号指定(改变)一个操作,对应于sys_sigaction() 参数:
sig,指明是哪一个信号
act,指定新的操作
oact,可选,用来存放旧的操作
signal(sig, handler)系统调用
设置信号处理程序为handler,对应于sys_singal()
Linux I/O
1 总线,IO
总线:CPU、RAM、I/O设备之间需要某些数据通路来保证信息的流动
总类:ISA、 EISA、 VESA、PCI以及MCA等等
三种基本类型
- 数据总线(pentium,64位)
- 地址总线(pentium,32位)
- 控制总线
当总线用于CPU与I/O设备之间的连接时,成为I/O总线
编址类型:
- 统一编址(典型arm)
- 独立编址(典型PC)
在x86处理器中,只使用了32位地址总线中的16位对I/O设备进行寻址。使用64位数据总线中的8、16、32位传送数据
I/O设备与CPU之间的连接层次为: CPU->I/O端口->I/O接口->设备控制器
2 I/O端口(I/O port)
每个I/O端口8位,由于只使用16位地址总线访问,因此I/O地址空间一共提供65536个I/O端口
在端口地址对齐的情况下,连续的I/O端口可以看成16位/32位端口
I/O端口访问方式
- 特定的指令用来访问I/O端口:in,ins,out,outs
- I/O端口的另外一种访问方法:直接映射到物理地址空间。可以使用存储器操作指令,如mov,and,or等等
I/O端口中的寄存器
CR SR IR OR
Linux中访问I/O端口的操作
inb/w/l
outb/w/l
etc
I/O端口的分配
不同的设备使用各自不同的端口
内核使用资源信息来记录端口分配信息
在这里,一个资源表示I/O端口地址的一个范围
相关的操作
任何设备驱动程序都可以使用下列三个函数来进行资源的请求和释放 request_resource、allocate_resource、release_resource
3 I/O接口
I/O接口是处于一组I/O端口和对应的设备控制器之间的一种硬件电路
- I/O端口->设备:将I/O端口中的值转换成设备所需要的命令和数据
- 设备->I/O端口:检测设备状态的变化,更新端口中相应的状态寄存器
- 连接到PIC上,代表设备发出中断请求
3.1 类型:
a 专用I/O接口
专用于一个特定的硬件设备。
在一些情况下,设备控制器与这种I/O接口处于同一块卡中。
可以是内部设备(PC机箱内部),也可以是外部设备
- 键盘接口,连接到键盘控制器上
- 图形接口,和图形卡中的控制器封装在一起
- 磁盘接口,连接到磁盘控制器
- 总线鼠标接口,连接到鼠标控制器
- 网络接口,与网卡中的控制器封装在一起
b 通用I/O接口
现代PC都包含连接很多外部设备的几个通用I/O接口
- 并口:传输单位1个字节
- 串口:逐位传送,内部包含一个UART(通用异步收发器,字节<-->位序列)
- PCMCIA接口
- SCSI接口:把PC主总线连接到次总线(SCSI总线)的电路
- USB口:通用总线接口。可以代替上述并口、串口、SCSI接口
4 设备控制器
复杂的设备需要一个设备控制器(device controller)来驱动
2个重要作用
- 对I/O接口接收到的高级命令进行解释,并通过向设备发送适当的电信号来控制设备执行特定的操作
- 对从设备接收到的电信号进行解释和转换,并修改状态寄存器
典型的设备控制器,例如磁盘控制器
有些简单的设备没有设备控制器:PIC, PIT
5 I/O共享存储器
很多硬件设备都有自己的存储器,通常称之为I/O共享存储器(I/O Shared Memory),如显存
映射I/O共享存储器的地址
根据设备和总线类型的不同,可以在三个不同的物理地址范围之间进行映射
- 对于连接到ISA总线上的大多数设备 0xa0000~0xfffff(640KB~1MB)
- 对于使用VESA局部总线的一些老设备(图形卡) 0xe00000~0xffffff(现在基本不生产)
- 对于连接到PCI总线的设备 映射到RAM物理地址4GB的顶端
I/O共享存储器的访问
对于物理地址1M之内的I/O共享存储器访问:
直接访问3G以上的对应线性区间 addr+3G
对于高端I/O共享存储器访问:
没有直接映射在3G以上的线性区间
需要为其创建一块非连续线性区,并将其映射到高端I/O共享存储器的物理地址上
ioremap/iounmap,类似vmalloc
ioremap_nocache
io_mem=ioremap(某个物理起始地址,长度)
访问io_mem+相对于起始地址的偏移处
访问I/O共享存储器的一些体系结构相关的接口
readb、readw、readl
writeb、writew、writel
memcpy_fromio、memcpy_toio
memset_io
例如访问0xfc000000I/O单元
io_mem=ioremap(0xfb000000,0x2000000)
t2=readb(io_mem+0x1000000)
6 DMA(直接存储器访问,Direct Memory Access)
所有的PC都包含一个DMAC(DMA控制器)
- 一种辅助处理器
- 用来控制在RAM和I/O设备之间传送数据
设置并激活DMAC
DMAC自行传送数据
数据传送结束后,DMAC发出一个中断请求
- 当CPU和DMAC并发访问同一个存储单元时,通过存储器仲裁器解决冲突
使用者:慢速设备
如,磁盘驱动器
7 设备驱动程序模型
Linux为硬件设备的驱动程序开发者提供一种统一的模型
模型可由sysfs目录结构见一斑
7.1 模型数据结构
Kobject
kobject是驱动程序模型中的一个核心数据结构,与sysfs文件系统自然的邦定在一起。每个kobject对应sysfs文件系统中的一个目录
kobject往往被嵌入到设备驱动程序模型中的组件中,如bus、device和driver程序的描述符
Kobject的作用是,为所属“容器”提供
- 引用计数器
- 维持容器的层次列表或组
- 为容器的属性提供一种用户态查看的视图
kset
Kset是同类型kobject结构的一个集合体,通过kset数据结构可将kobjects组织成一棵层次树
subsystem
7.2 设备驱动程序模型的组件
设备device:device_type对象;device对象
驱动程序driver:device_driver对象(对应device对象)
总线bus:bus_type(对应device对象和device_driver对象);bus_register();
类class:class
7.3 设备文件
Unix类操作系统都是基于文件概念的
文件是以字符序列而构成的信息载体,因此一个I/O设备也可以当作文件来处理。与普通文件交互的系统调用也可以直接用于I/O设备
对内核而言,一个设备文件的名字是无关紧要的,关键在于设备文件的类型及其主次设备号
设备文件的分类
根据设备驱动程序的基本特性,设备文件可以分为:
- 字符设备:要么不可以随机访问,例如声卡;如果可被随机访问(往往通过顺序访问方式实现),但随着数据的位置的不同,其访问时间会相差很大,例如磁带
- 块设备:数据可以被随机访问,访问任何位置的数据时间大致相同。典型例子:硬盘、软盘
网络: 网卡不与文件相关联,使用专门的处理方式
7.4 动态分配设备号
驱动程序指定设备号的分配范围,而不是一个精确的值。由内核分配一个合适的设备号范围给驱动程序
需要一个标准的方法将驱动程序使用的设备号输出到用户态应用程序中 即设备驱动程序模型中:
把主次设备号存放在/sys/class目录下的dev属性中
7.5 动态的创建设备文件
Linux可以动态的创建设备文件
udev用户态工具集
- 系统启动时,/dev目录下是空的
- udev程序扫描/sys/class目录来寻找dev文件,根据这里的信息在/dev目录下建立必要的设备文件
- 并根据配置文件为其分配一个文件名,并创建一个符号链接
7.6 设备文件的VFS处理
进程访问普通文件时,通过文件系统访问磁盘分区中的数据块
当进程访问设备文件时,看看VFS的具体操作
VFS在设备文件打开时使用与设备相关的函数调用替换其缺省的文件操作 。这些设备相关函数调用对硬件设备进行操作
过程:
- 在解析路径名后,将建立索引节点对象、目录项对象和文件对象
- 若发现是一个设备文件,则调用init_special_inode来进行替换,并且设置inode->i_rdev为对象传来的idev。比如字符设备:inode->i_fop= &def_chr_fops
- 接着open调用了def_chr_fops中的chrdev_open。先找到i_rdev对应的kobj,再由kobj找到container cdev,将cdev的fops斌给filp->f_op,最后使用filp可对应驱动的open函数。
8 设备驱动程序
这是一个软件层,使得硬件设备能够响应预定义好的编程接口,就是一组控制设备的VFS函数接口:open,read,lseek,ioctl等
工作:
- 设备的VFS函数接口的具体实现由设备驱动程序提供
- 注册
- 初始化
- 并在进行数据传送的时候监控I/O操作
8.1 注册设备驱动程序
注册一个设备驱动程序意味着
- 分配一个新的device_driver描述符,
- 将其插入到设备驱动程序模型的数据结构中,
- 并把它与对应的设备文件连接起来
注册时机
- 如果设备驱动程序被静态编译进内核,则注册发生在内核初始化阶段
- 如果作为一个内核模块来编译,则在装入模块的时候注册(并在卸载模块时注销)。在这种情况下,当模块卸载时,驱动程序要注销自己
注册函数
字符设备
char_device_struct chrdevs数组
register_chrdev/unregister_chrdev
register_chrdev_region/alloc_chrdev_region/unregister_chrdev_region +cdev_add
块设备
register_blkdev /unregister_blkdev
各总线设备,总线驱动会提供相关接口
8.2 设备驱动程序的初始化
对设备驱动程序进行注册与初始化是两件不同的事情
注册应当尽早:使得用户可以使用设备文件
初始化应当推迟到最后可能的时候
原因:初始化就意味着需要分配系统中的稀缺资源,例如:
- 1,中断向量(动态分配的情况下)
- 2,用于DMA传送的缓冲区的页框
- 3,包括DMA通道本身
为了确保资源在需要时能够获得,在获得后不再被请求,设备驱动程序通常使用
引用计数器
- Open,++
- release,--
在open时,若++前为0,则驱动程序必须分配资源并激活硬件设备上的中断和DMA
在release时,若--后为0,则禁止中断和DMA并释放所分配的资源
8.3 监控I/O操作
I/O操作的持续时间通常不可预知,可能与各种因素相关,例如
- 机械装置的状态,如对于磁盘来讲,磁头的当前位置
- 或实际的随机事件,例如数据包何时到达网卡
- 以及人为因素,例如人对键盘、鼠标的使用,以及发现打印机卡纸时的操作
为此,设备驱动程序必须通过某种监控手段监控I/O操作终止或超时
两种可用的技术
- 轮询模式(polling mode) CPU重复检查(轮询)设备的状态寄存器,直到寄存器的值表明I/O操作已经完成为止
- 中断模式(interrupt mode) 如果I/O控制器能够通过IRQ线发出I/O操作结束的信号,就可以使用中断模式
8.4 设备的VFS函数接口的具体实现
Struct file*filp,在这个数据的私有数据项中,VFS已经将其转换成设备驱动程序的私有的信息???
9 字符设备驱动程序
9.1 字符设备驱动程序的数据结构和相关接口
struct cdev
cdev_alloc()分配一个cdev描述符
cdev_add()在设备驱动程序模型中注册一个cdev描述符
9.2 分配设备号
为了记录目前已经分配了哪些字符设备号,内核使用散列表chrdevs
- 表的大小不超过设备号的范围
- 两个不同的设备号范围可能共享一个主设备号。由于设备号范围不重叠,因此次设备号应该完全不同
chrdevs包含255个表项
分配设备号:新设备驱动采用这种方法
alloc_chrdev_region
register_chrdev_region
+cdev_add
10 块设备驱动程序
典型的块设备驱动程序都有很高的平均访问时间
- 例如磁盘的每次操作都需要几个ms,主要是为了定位磁头,一旦定位后,就可以以稳定的高速率传输数据(几十MB/秒)
定义:相邻的数据
指当数据以相邻的方式存放在磁表面时,一次单独操作就可以访问它们
内核对块设备处理程序的支持具有以下特点:
- 通过VFS提供统一接口
- 对磁盘数据进行有效的预读
- 为数据提供磁盘高速缓存
在块设备处理所涉及的多个内核组件中,每个组件采用不同的长度来管理磁盘数据
- 扇区:硬盘块设备控制器按照扇区的大小来传递数据。按Linux惯例,大小为512个字节。有的设备可以有更大的扇区大小,由设备驱动程序进行转换
- 块:文件的逻辑存储单位。Linux要求必须是2的幂,但不能超过一个页框的大小。在80x86中,可以是512、1024、2048和4096字节
- 段:一个内存页或者内存页的一部分,包含磁盘上物理相邻的数据块
- 页:磁盘高速缓存作用于一个页,每个页正好装在一个物理页框中
10.1 通用块层
通用块层是一个内核组件,处理来自系统中的所有块设备发出的请求
该层的存在,使得内核可以
- 将数据缓冲区放在高端内存
- 通过一些附加的手段,实现“零-复制”
- 管理逻辑卷:几个磁盘分区,即使位于不同的块设备中,也可以被看作是一个单一的分区
- 发挥大部分新磁盘控制器的高级特性
通用块层的核心数据结构是bio描述符,用来描述块设备的I/O操作
磁盘是一个由通用块层处理的逻辑块设备
通常一个磁盘对应于一个硬件块设备,例如硬盘、软盘或光盘
磁盘也可以是一个虚拟设备,
- 可以建立在几个物理磁盘分区上
- 或者一些RAM专用页中的内存区上
磁盘是由gendisk对象描述的
通常磁盘被划分成几个逻辑分区。
每个块设备文件要么代表整个磁盘,要么代表磁盘中的某一个逻辑分区
例如
- 一个主设备号3,次设备号0的设备文件/dev/had代表的可能是一个主的IDE磁盘,
- 而/dev/hda1和/dev/hda2则代表该磁盘上的前2个分区,它们的主设备号都是3,而次设备号则是1和2
一般磁盘中的分区是通过连续的次设备号来区分的
磁盘的分区表保存在hd_struct结构的数组中
当内核发现一个新的磁盘时, 调用alloc_disk,分配并初始化一个新的gendisk对象,(若新磁盘被划分为若干个分区,则还会分配并初始化hd_struct类型的数组),然后调用add_disk将新磁盘插入到通用块层的数据结构中
提交请求:当向通用块层提交一个I/O请求时:
- 首先调用bio_alloc分配一个bio描述符,然后进行初始化
- 然后调用generic_make_request,这是通用块层的主要入口点
10.2 I/O调度程序
块设备请求及其优化
- 虽然块设备驱动程序可以一次传送一个单独的数据块,但是内核并不会为每个要访问的数据块都执行一次I/O操作
- 内核试图把几个块合并在一起,作为一个整体来处理,从而减少磁头的平均移动时间
实现
为读写一个磁盘块的请求生成块设备请求
但推迟这个请求执行的时间
这是提高块设备性能的关键机制
当请求发生时,内核检查是否能通过稍微扩展前一个一直处于等待状态的请求而满足新的请求,从而减少定位的时间,提高效率
每个块设备驱动程序都维护着自己的请求队列;
每个物理块设备应当有一个请求队列。请求可以以提高磁盘性能的方式进行排序
低级的设备驱动程序一般采用如下策略:
- 处理请求队列上的第一个请求,并设置设备控制器,以便在数据传送完成时可以产生一个中断,然后就停止
- 当设备控制器产生中断时,中断处理程序就激活下半部分。
- 下半部分将被处理的请求删除,并继续1
请求队列由一个大的描述符request_queue表示
- 实际上,请求队列是一个双向链表,其元素是请求描述符
- I/O调度程序将提供几个预先确定好的元素的排序方式
每个块设备的待处理请求都是用一个请求描述符request来表示的
10.3 块设备驱动程序
块设备驱动程序是Linux块子系统中的最底层组件
它们从I/O调度程序中获得请求,然后按照要求处理这些请求
每个块设备驱动程序对应一个device_driver描述符
每个磁盘都与一个device描述符关联
块设备描述符block_device
注册和初始化块设备驱动程序
- 自定义驱动程序描述符
- 预订主设备号
- 初始化自定义描述符
- 初始化gendisk描述符
- 初始化块设备操作表
- 分配和初始化请求队列
- 设置中断处理程序
- 注册磁盘
策略例程
策略例程是快设备驱动程序的一个函数或一组函数
与硬件块设备之间相互作用,以满足调度队列中所汇集的请求
通过请求队列描述符中的request_fn方法可以调用策略例程
策略例程的简单实现方法
- 对于调度队列中的每个元素,与块设备驱动程序相互作用共同为请求服务,等待知道数据传送完成,然后把已经服务过的请求从队列中删除,继续处理调度队列中的下一个请求
上述方法效率不高。现在,很多块设备是用如下策略
- 策略例程处理队列中的第一个请求,并设置块设备控制器,以便在数据传送完成时可以产生一个中断。然后策略例程就终止。
当磁盘控制器产生中断时,中断服务例程重新调用策略例程
a策略例程要么为当前请求再启动一次数据传送 b要么当请求的所有数据已经传送完成时,把请求从调度队列中删除然后开始处理下一个请求
中断处理程序
块设备驱动程序的中断处理程序是在DMA数据传送结束时被激活的。
检查是否已经传送完所有的数据块
a是:调用策略例程处理调度队列中的下一个请求
b否则:更新请求描述符的相应字段,并调用策略例程处理还没有完成的数据传送