一、Linux系统的定义
Linux系统是一个类UNIX的操作系统,与UNIX完全兼容, 在操作系统功能、使用方法等方面极为相似。
1.什么是Linux操作系统
Linux是一个多用户、多任务操作系统
2. Linux与UNIX操作系统的不同点
① 源代码编写方式
② 商业模式
③ 开发模式
3.Linux系统的组成
Linux操作系统包括Linux内核,还包括shell、带有多窗口管理器的 X-Windows图形用户接口、文本编辑器、高级语言编译器等应用软件。
4.Linux系统的特点
① 单体结构内核
② 可抢占式内核
③ 多线程应用程序的支持
④ 多处理机支持
⑤ 支持多种文件系统
二、Linux系统的内核结构
1.Linux内核的组成
Linux内核包含最基础、最核心的概念,提供系统其他部分必须的服务支持。
- 进程调度程序、主存管理程序
- 负责网络、进程间通信的服务程序
- 中断处理程序和设备驱动等核心服务程序
2.Linux系统的核心结构
三、Linux系统的特权与中断处理
1.Linux系统的特权级
Linux系统使用两个级别 (处理机提供四个特权级) :
特权级0 —— 核态 (内核模式)
特权级3 —— 用户态 (用户模式)
2.Linux系统中断处理的上半部和下半部
(1) 为什么要区分上半部和下半部
为了提高中断处理的效率,中断处理程序的执行必须快速、简洁。为此,Linux系统将中断处理程序分为两部分,以缩短关中断时间:
(2) 中断处理程序的上半部
- 上半部是中断处理中有严格时间限制的工作,是关键而紧迫的部分;
- 上半部的工作是不可被打断的,即在屏蔽所有中断的情况下进行的。
例:与硬件设备应答或使硬件复位的工作。
(3) 中断处理程序的下半部 - 下半部处理那些可以稍后完成的工作;
- 下半部的执行是可以打断的,即是在开中断的情况下执行。
(4) 上半部与下半部的分工: - 如果一个任务对时间非常敏感,放在上半部;
- 如果一个任务和硬件相关,放在上半部;
- 如果一个任务要保证不被其他中断(特别是相同的中断)打断,放在上半部;
- 其他所有任务,放在下半部。
3.中断处理下半部的实现机制
主要有tasklet和工作队列两种。
(1)tasklet
① tasklet通过软中断实现: TASKLET_SOFTIRQ
ⅰ 一个软中断被标记后才能执行,称为触发软中断。
ⅱ 待处理的软中断会在以下时机被检查和执行:
- 从一个硬件中断返回时;
- 在ksoftirqd内核线程中;
- 在显式检查和执行待处理的软中断的代码中
tasklet由tasklet_schedule()函数调度 :为允许执行的待处理tasket标识软中断,则下一次调用do_softirq()时就被执行。
(2) 工作队列
工作队列机制将中断处理程序的下半部交给一个内核线程去执行;下半部是在进程上下文 执行,可以睡眠和被重新调度。
① 工作者线程:执行函数worker_thread()
1)默认工作者线程在每个处理机上设置一个:events/n
2)该线程执行由各内核中断处理程序交给它的下半部。
3)工作过程:
ⅰ执行一个死循环;
ⅱ 若工作队列链表不空时,执行链表上的所有工作;
ⅲ 当链表为空时,它进入睡眠状态;
ⅳ 当有下半部插入到队列时,工作者线程被唤醒,将继续处理新加入的下半部 。
四、LInux系统调用
1.系统调用的意义
操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用
- 把用户从底层的硬件编程中解放出来
- 极大的提高了系统的安全性
- 使用户程序具有可移植性
2.API和系统调用
应用编程接口(application program interface, API)和系统调用是不同的
- API只是一个函数定义
- 系统调用通过软中断向内核发出一个明确的请求
Libc库定义的一些API引用了封装例程(wrapper routine,唯一目的就是发布系统调用)
- 一般每个系统调用对应一个封装例程
- 库再用这些封装例程定义出给用户的API
不是每个API都对应一个特定的系统调用。
- API可能直接提供用户态的服务 如,一些数学函数
- 一个单独的API可能调用几个系统调用
- 不同的API可能调用了同一个系统调用
返回值 - 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
-1在多数情况下表示内核不能满足进程的请求
Libc中定义的errno变量包含特定的出错码
3.系统调用程序及服务例程
当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。
- 在Linux中是通过执行int$0x80来执行系统调用的,这条汇编指令产生向量为128的编程异常(回忆,trapinit中系统调用入口的初始化)
- Intel Pentium II中引入了sysenter指令(快速系统调用),2.6已经支持
传参:
内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数
- 使用eax寄存器
所有的系统调用返回一个整数值。
- 正数或0表示系统调用成功结束
- 负数表示一个出错条件
这里的返回值与封装例程返回值的约定不同
- 内核没有设置或使用errno变量
- 封装例程在系统调用返回取得返回值之后设置这个变量
- 当系统调用出错时,返回的那个负值将要存放在errno变量中返回给应用程序
系统调用处理程序也和其他异常处理程序的结构类似
- 在进程的内核态堆栈中保存大多数寄存器的内容(即保存恢复进程到用户态执行所需要的上下文)
- 调用相应的系统调用服务例程处理系统调用
- sys_xxx
- 通过ret_from_sys_call()从系统调用返回
为了把系统调用号与相应的服务例程关联起来,内核利用了一个系统调用分派表(dispatch table)。
这个表存放在sys_call_table数组中,有若干个表项(2.6.26中,是356):第n个表项对应了系统调用号为n的服务例程的入口地址的指针
观察sys_call_table(syscall_table_32.S以及entry_32.S)
4.关于系统调用表的大小
- 初始化系统调用
内核初始化期间调用trap_init()函数建立IDT表中向量128对应的表项,语句如下:
该调用把下列值存入这个系统门描述符的相应字段:
segment selector
内核代码段__KERNEL_CS的段选择符
offset
指向system_call()异常处理程序的入口地址
type
置为15。表示这个异常是一个陷阱,相应的处理程序不禁止可屏蔽中断
DPL(描述符特权级)
置为3。这就允许用户态进程访问这个门,即在用户程序中使用int $0x80是合法的
6. system_call()函数
7、参数传递
系统调用也需要输入输出参数,例如
- 实际的值
- 用户态进程地址空间的变量的地址
- 甚至是包含指向用户态函数的指针的数据结构的地址
system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号
- 一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即__NR_fork)。
- 这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号
- 进入sys_call之后,立即将eax的值压入内核堆栈
很多系统调用需要不止一个参数
- 普通C函数的参数传递是通过把参数值写入堆栈(用户态堆栈或内核态堆栈)来实现的。但因为系统调用是一种特殊函数,它由用户态进入了内核态,所以既不能使用用户态的堆栈也不能直接使用内核态堆栈
回想一下在进入中断和异常处理程序前,在内核态堆栈中保存的pt_regs结构,此时pt_regs结构中的一些寄存器被用来传递参数或者pt_regs结构本身就是参数