您的位置:首页 > 运维架构 > Linux

linux内核设计与实现学习摘要

2016-03-10 10:17 288 查看
以前曾经学习过,不过不注意做记录,早忘光了。现在从新来学,以做记录。

1. 内核开发的特点:

1)内核编程时不能访问C库。

2)内核编程时必须使用GNU C,内核并不完全符合ANSI C。

3)内核编程时缺乏像用户空间那样的内存保护机制。

4)内核编程时浮点数很难使用。

5)内核只有一个很小的定长堆栈。32位机8KB,64位机16KB.

6)由于内核支持异步中断,抢占和SMP,因此必须时刻注意同步和并发。常用于解决竞争的办法是自旋锁和信号量。

7)要考虑可移植性的重要性。保持字节序,64位对齐,不假定字长和页面长度。
2.进程管理

进程描述符及任务结构:

进程存放在叫做任务队列(tasklist)的双向循环链表中。链表中的每一项包含一个具体进程的所有信息,类型为task_struct,称为进程描述符(process
descriptor),该结构定义在<linux/sched.h>文件中。



进程状态:


task_struct中的state描述进程的当前状态。进程的状态一共有5种,而进程必然处于其中一种状态:


1)TASK_RUNNING(运行)——进程是可执行的,它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行唯一可能的状态;也可以应用到内核空间中正在执行的进程。

2)TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(也就是说它被阻塞)等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行,处于此状态的进程也会因为接收到信号而提前被唤醒并投入运行。

3)TASK_UNINTERRUPTIBLE(不可中断)——除了不会因为接收到信号而被唤醒从而投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不作响应,所以较之可中断状态,使用得较少。

4)TASK_ZOMBIE(僵死)——该进程已经结束了,但是其父进程还没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4(),进程描述符就会被释放。

5)TASK_STOPPED(停止)——进程停止执行,进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。

需要调整进程的状态,最好使用set_task_state(task, state)函数,在必要的时候,它会设置内存屏障来强制其他处理器作重新排序(SMP)。

进程创建:

在Linux系统中,所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscript)并执行其他的相关程序,最终完成系统启动的整个进程。

Linux提供两个函数去处理进程的创建和执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(每个进程唯一),PPID(父进程的PID)和某些资源和统计量(例如挂起的信号)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。

fork()使用写时拷贝(copy-on-write)页实现。内核在fork进程时不复制整个进程地址空间,让父进程和子进程共享同一个拷贝,当需要写入时,数据才会被复制,使各进程拥有自己的拷贝。在页根本不会被写入的情况下(fork()后立即exec()),fork的实际开销只有复制父进程的页表以及给子进程创建唯一的task_struct。

创建进程的fork()函数实际上最终是调用clone()函数。

创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。linux把所有的线程都当做进程来实现。

比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0);创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES |CLONE_SIGHAND, 0)。

在内核中创建的内核线程与普通的进程之间还有个主要区别在于:内核线程没有独立的地址空间,它们只能在内核空间运行。
内核线程:
内核线程是独立运行在内核空间的标准进程。内核线程和普通进程间的区别在于内核线程没有独立的地址空间(实际上它的mm指针被设置为NULL).

内核线程只能由其他内核线程创建,第一个内核线程由谁来创建呢?
在现有内核线程中创建一个新的内核线程的方法:
int kernel_thread(int(*fn)(void *),void *arg,unsigned long flags)
进程终结:

进程的析构发生在它调用exit()之后。

当进程接受到它既不能处理也不能忽略的信号或者异常时,它还可能被动地终结。

进程的终结大部分任务都靠do_exit()完成。

在调用了do_exit()之后,尽管线程已经将死不能运行了,但是系统还保留了它的进程描述符。因此进程终结时所需要的清理工作和进程描述符的删除被分开执行。

wait()这一族函数都是通过一个系统调用wait4()实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID.

当最终需要释放进程描述符时,release_task()会被调用。

总结:

linux通过task_struct 和 thread_info存放和表示进程。

通过clone()和fork()创建进程。

通过exec()系统调用族把新的执行映像装入到地址空间。

通过wait()系统调用族使父进程可以收集其后代的信息。

通过强制或者自愿地调用exit()来消亡进程。

进程调度:

在一组处于可运行状态的进程中选择一个来执行,是调度程序所需完成的基本工作。

调度策略通常要在两个矛盾的目标中间寻找平和:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量)

linux内核提供了两组独立的优先级范围

第一种是nice值(-20-+19),默认0.

nice值小的进程在nice值大的进程之前执行。

nice值也用来决定分配给进程时间片的长短,nice值为-20的进程可能获得的时间片最长。nice是所有unix系统都用到的标准优先级范围。

第二种是实时优先级,其值是可配置的,默认情况下它的变化范围是从0到99.

任何实时进程 的优先级都高于普通的进程。

linux提供对posix实时优先级的支持。

linux调度程序提高交互式程序的优先级,让他们运行得更频繁。于是调度程序提供较长的默认时间片给交互式程序。

linux调度程序还能根据进程的优先级动态调整分配给它的时间片。保证了优先级高的进程,假定也是重要性高的进程,执行的频率高,执行时间长。

通过实现这样一种动态调整优先级和时间片长度的机制,linux调度性能不但非常稳定也很强健。

进程时间片:最小5ms,最大800ms,默认100ms.

进程抢占:

由调度程序来决定什么时候停止一个进程的运行以便其他进程能够得到执行机会,这个强制的挂起动作叫做抢占。

抢占式内核可以在进程处于内核态时,进行抢占。

当一个进程进入TASK_RUNNING状态,内核会检查它的优先级是否高于当前正在执行的进程。

如果是,调度程序会被唤醒,抢占当前正在运行的进程并运行新的可运行进程。

当一个进程的时间片变为0时,它会被抢占,调度程序被唤醒以选择一个新的进程。

在2.6版本中,内核引入了抢占能力。只要没有持有锁,内核就可以进行抢占。

抢占式内核在以下几种情况下不可抢占:

1.当内核运行中断处理程序和异常处理程序时,在linux内核中进程不能抢占中断,在中断例程中不允许进行调度。

2.当进程在内核态运行临界区的代码时,不可抢占。这些临界区被自旋锁spin_lock保护了起来。【但是当进程使用spin_lock时,自己被锁住并自旋时,这时可以调度。】

3. 内核正在进行bottom half(中断的底半部)处理时,不可抢占。

4.内核正在执行调度程序Scheduler时,不可抢占

5.内核正在对每一个CPU“私有”数据结构操作(per CPU date structures)时,不可抢占

内核调用schedule()的时机:

上下文切换,就是从一个可执行进程切换到另一个可执行进程。由schedule()调用context_switch()完成。

内核提供了一个need_resched标志来表明是否需要重新执行一次调度。

1.进程时间片耗尽,scheduler_tick()就会设置这个标志。

2.当一个优先级高的进程进入可执行态的时候,try_to_wake_up()会设置这个标志。

3.在返回用户空间及从中断返回的时候,内核会检查这个标志。如果已被设置,内核会在继续执行之前调用调度程序。

linux提供了两种实时调度策略:SCHED_FIFO 和 SCHED_RR.普通的,非实时的调度策略是SCHED_NORMAL.

只有较高优先级SCHED_FIFO或者SCHED_RR任务才能抢占SCHED_FIFO任务。

一旦一个SCHED_FIFO级进程处于可执行状态,就会一直执行,直到它自己阻塞或显式地释放处理器为止,它不基于时间片,可以一直执行下去。

SCHED_RR是带有时间片的SCHED_FIFO.

内核不为实时进程计算动态优先级。

默认实时优先级范围是从0到99.

nice值从-20到+19直接对应的是从100到139的实时优先级范围。

系统调用:

linux的系统调用作为C库的一部分提供。

C库实现了unix系统的主要API,包括标准C库函数和系统调用。

asmlinkage long sys_getpid()

{

return current->tgid;

}

asmlinkage用于通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。

注意系统调用get_pid()在内核中被定义成sys_getpid().这是linux所有系统调用都应该遵循的命名规则。

在linux中,每个系统调用都被赋予一个系统调用号。

内核记录了系统调用表中的所有已经注册过的系统调用的列表,存储在sys_call_table中。

它与体系结构有关,一般在entry.s中定义。这个表中为每一个有效的系统调用指定了唯一的系统调用号。

通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。

此时的异常处理程序实际上就是系统调用处理程序,所以system_call().

用户区应用调用read(),C库提供 read() wrapper : 内核区执行system_call(),然后转到sys_read().

内核在执行系统调用的时候处于进程上下文。在进程上下文中,内核可以休眠可以被抢占。

当系统调用返回的时候,控制权仍在system_call()中,它最终会负责切换到用户空间并让用户进程继续执行。

注册一个正式的系统调用:

1 在系统调用表的最后加入一个表项。每种支持该系统调用的硬件体系都必须做这样的工作。

2 对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>中

3 系统调用必须被编译进内核映像(不能被编译成模块)。这只要把它放进kernel/下的一个相关文件中就可以。

每种体系结构不需要对应相同的系统调用号。

在应用中用宏来使用系统调用:

#define __NR_foo 283 __syscall0(long,foo)

建立一个新的系统调用的好处:

系统调用创建容易且使用方便。

系统调用的高性能显而易见。

问题:

1,需要一个系统调用号,这需要在一个内核处于开发版本的时候由官方分配给你。

2,系统调用被加入稳定内核后就被固化了。

3,需要将系统调用分别注册到每个需要支持的体系结构中。

4,在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用。

5,如果仅仅进行简单的信息交换,系统调用就大材小用了。

替代方法:

1 创建一个设备节点,通过read()和write()访问它。 用ioctl()进行特别的设置操作和获取特别信息。

2 把增加的信息作为一个文件放在sysfs的合适位置。

3 像信号量这样的某些接口,可以用文件描述符来表示,因此也可以按上述方式对其操作。

中断:中断是一种电信号,由硬件设备生成,并直接送入中断控制器的输入引脚上。然后由中断控制器向处理器发送相应的信号。处理器一经检测到此信号,便中断自己的当前工作转而处理中断。此后,处理器会通知操作系统一经产生中断,这样,操作系统就可以对这个中断进行适当的处理了。

异常:异常与中断不同,它产生时必须考虑与处理器时钟同步。异常也常常称为同步中断。异常是处理器本身产生的同步中断。

在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序(interrupt handler)或中断服务历程(interrupt service routine,ISR).

中断处理程序与其他内核函数的真正区别在于:中断处理程序是被内核调用来响应中断的,它们运行于中断上下文的特殊上下文中。

中断处理分两个部分:

中断处理程序是上半部分。所有工作都是在所有中断被禁止的情况下完成的。

在合适的时机,下半部会被开中断执行。

注册中断处理程序:

int request_irq(unsigned int irq, irqreturn_t (*handler)(int,void*,struct pt_regs *),unsigned long irqflags,const char * devname, void *dev_id)

irq, 要分配的中断号,

handler,指向处理这个中断的实际中断处理程序。

irqflags,可以为0,也可能是下列一个或多个标志的位掩码:

SA_INTERRUPT: 在本地处理器上,快速中断处理程序在禁止所有中断的情况下运行。默认(灭有这个标志),除了正运行的中断处理程序对应的那条中断线被屏蔽 外,其他所有中断都是激活的。除了时钟中断外,绝大多数中断都不使用该标志。

SA_SAMPLE_RANDOM:如果设备以预知的速率产生中断,或者可能受外部攻击者的影响,就不要设置这个标志。

SA_SHIRQ: 此标志表明可以在多个中断处理程序之间共享中断线。

devname:是与中断相关的设备的ASCII文本表示法。这些名字会被/proc/irq和/proc/interrupt文件使用,以便与用户通信。

dev_id: 主要用于共享中断线。当一个中断处理程序需要释放时,dev_id将提供唯一的标志信息(cookie),以便从共享中断线的诸多中断处理程序中删除指定的那一个。

request_irq()成功执行返回0.最常见的错误是:-EBUSY,它表示给定的中断线已经在使用。

该函数可能会睡眠,不能在中断上下文或者其他不允许阻塞的代码中调用该函数。

初始化硬件和注册中断处理程序的顺序必须正确,以防止中断处理程序在设备初始化完成之前就开始执行。

释放中断处理程序:

卸载驱动时,需要注销相应的中断处理程序,并释放中短线。调用 void free_irq(unsigned int irq,void *dev_id)来释放中断线。

必须从进程上下文中调用free_irq().

编写中断处理程序:

典型的中断处理程序声明: static irqreturn_t intr_handler(int irq,void *dev_id,struct pt_regs *regs)

当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽。

共享的中断处理程序与非共享的中断处理程序差异:

1. request_irq()的参数flags必须设置SA_SHIRQ标志

2. 对每个注册的中断处理程序 来说,dev_id参数必须唯一

3. 中断处理程序必须能够区分它的设备是否真的产生了中断

内核收到一个中断后,它将依次调用在该中断线上注册的每一个处理程序。因此,一个处理程序必须知道它是否应该为这个中断负责。

如果与它相关的设备没有产生中断,那么处理程序应该立即退出。

这需要硬件设备提供状态寄存器,以便中断处理程序进行检查。

中断上下文:

当执行一个中断处理程序或下半部时,内核处于中断上下文(interrupt context)中。

进程上下文:

进程上下文是一种内核所处的操作模式,此时内核代表进程执行,例如执行系统调用或运行内核线程。

中断上下文和进程没什么联系。中断处理程序打断了其他的代码,甚至可能是打断了在其他中断线上的另一个中断处理程序。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: