复习记录
Unix的一些特点
简洁、在unix中,所有的东西都被当做文件对待、unix的内核和相关的系统工具软件用C语言编写(移植能力的基础)、unix的进程创建非常迅速。
linux内核
通常一个内核由负责响应中断的中断服务程序,负责管理多个进程从而分享处理器时间的调度程序,负责管理进程地址空间的内存管理程序和网络、进程间通信等系统服务程序共同组成。
Unix内核通常需要硬件系统提供页机制(MMU)以管理内存
Linux是一个单内核,也就是说,Linux内核运行在单独的内核地址空间上。但依然汲取了微内核的精华:模块化设计、抢占式内核、支持内核线程、动态加载内核模块
内核版本号
注意:从版本号可以反映出该内核是一个稳定版本还是一个处于开发中的版本。
内核源码树的根目录描述
内核编程的差异
- 不能使用标准C函数库
- 只能使用GNU C
- 缺乏像用户空间那样的内存保护机制
- 难以执行浮点运算
- 内核给每个进程只有一个很小的定长堆栈
- 时刻注意同步和并发
- 要考虑可移植的重要性
GNU C
-
内联函数
优点:函数会在它所调用的位置上展开,可以消除函数调用和返回所带来的开销(寄存器存储和恢复)
缺点:占用更多的缓存空间或者占用更多的指令缓存
-
内联汇编
gcc编译器支持在C函数中嵌入汇编指令,通常使用asm()指令嵌入汇编代码
-
分支声明
对于条件选择语句,gcc内建了一条指令用于优化
执行线程
每个线程都有一个独立的程序计数器、进程栈和一组进程寄存器
fork()系统调用从内核返回两次:一次回到父进程,另一次回到新产生的的子进程
进程描述符及任务结构
内核把进程的列表放在叫做任务队列的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符(process descriptor)的结构,该结构定义在<linux/sched.h>文件中。进程描述符中包含的数据:打开的文件、进程的地址空间、挂起的信号、进程的状态、以及其它信息
Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的
写时拷贝
写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。资源的复制只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。
fork()
Linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。fork()、vfork()和__clone()库函数都根据各自需要的参数标志去调用clone(),然后用clone()去调用do_fork()。
do_fork完成了创建中的大部分工作,它的定义在kernel/fork.c文件中。该函数调用copy_process()函数,然后让进程开始运行。
vfork()
不拷贝父进程的页表项,子进程和父进程共享相同的(虚拟 or 物理)地址空间,也就是说,子进程可以改变父进程的数据段。另外,子进程要显示的调用exit(1),否则父进程可能会一直被阻塞。
clone()参数标志
线程终结
当线程调用了do_exit()之后,尽管线程已经僵死不能再运行了,但是系统还保留了它的进程描述符。这样做可以让系统有办法在子线程终结后仍能获得它的信息。当最终需要释放进程描述符时,release_task()会被调用,完成一下工作:
孤儿进程
如果父进程在子线程之前退出,就给子线程在当前线程组内找一个线程作为父亲,或者让init做它的父进程
CFS调度器
Linux的CFS调度器没有直接分配时间片到进程,它是将处理器的使用比例划分给了进程。这样一来,进程所获得的处理器时间其实是和系统负载密切相关的。这个比例进一步还会受进程nice值的影响。CFS称为公平调度器是因为它确保给每个进程公平的处理器使用比。CFS的做法是允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程。
系统调用
系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个:为用户空间提供了一种硬件的抽象接口、保证了系统的稳定和安全、为进程运行在虚拟系统中,提供方便。
asmlinkage long sys_getpid(void)
函数声明中的asmlinkage限定词,这是一个编译指令,通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。
通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序
绑定一个系统调用的最后步骤
- 首先,在系统调用表的最后加入一个表项。从0开始算起,系统调用在该表中的位置就是它的系统调用号
- 对于所支持的各种体系结构,系统调用号都必须定义在<asm/unistd.h>中
- 系统调用必须被编译进内核映象。这只要把它放进kernel下的一个相关文件就可以了,比如sys.c,它包含了各种各样的系统调用
设备驱动程序是用于对设备进行管理的内核代码
注册中断处理程序
驱动程序可以通过request_irq()函数注册一个中断处理程序(它被声明在文件<linux/interrupt.h>中),并且激活给定的中断线,以处理中断:
/* request_irq: 分配一条给定的中断线 */
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev
)
同一个中断处理程序绝对不会被同时调用以处理嵌套的中断
下半部执行的关键在于当它们运行的时候,允许响应所有的中断
软中断和tasklet
软中断是一组静态定义的下半部接口,有32个,可以在所有处理器上同时执行(即使两个类型相同也可以)。tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制。两个不同类型的tasklet可以在不同的处理器上同时执行,但类型相同的tasklet不能同时执行。
ksoftirqd
重新触发的软中断不会立即被处理,作为改进,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级上运行,这能避免它们跟其它重要的任务抢夺资源。
每个处理器都有一个这样的线程。所有线程的名字都叫做ksoftirqd/n,区别在于n,它对应的是处理器的编号。为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程。一旦线程被初始化,它就会执行类似下面这样的死循环:
for( ; ; ) {
if (!softirq_pending(cpu))
schedule();
set_current_state(TASK_RUNNING);
while(softirq_pending(cpu)) {
do_softirq();
if (need_resched())
schedule();
}
set_Current_state(TASK_INTERRUPTIBLE);
}
只要有待处理的软中断(由softirq_pending()函数负责发现),ksoftirq就会调用do_softirq()去处理它们。如果有必要的话,每次迭代后都会调用schedule()以便让更重要的进程得到处理的机会。当所有需要执行的操作都完成以后,该内核线程就将自己设置为TASK_INTERRUPTIBLE状态。
工作队列
工作队列子系统是一个用于创建内核线程的接口,通过它所创建的进程负责执行由内核其它部分排到队列里的任务。它创建的这些内核线程称作worker thread。工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列提供了一个缺省的worker thread来处理这些工作。
加锁粒度用来描述加锁保护的数据规模
原子位操作
位操作函数是对普通的内存地址进行操作的。它的参数是一个指针和一个位号
spin lock(自旋锁)
自旋锁可以使用在中断处理程序中(此处不能再使用信号量,因为它们会导致睡眠)。在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(在当前处理器上的中断请求),否则,中断处理程序就会打断正持有锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自旋,等待该锁重新可用,但是锁的持有者在这个中断处理程序完毕前不可能运行,这就是双重请求死锁。
用户抢占发生在:
- 从系统调用返回用户空间时
- 从中断处理程序返回用户空间时
内核抢占发生在
- 中断处理程序正在执行,且返回内核空间之前
- 内核代码再一次具有可抢占性的时候
- 如果内核中的任务显式地调用schedule()
- 如果内核中的任务阻塞