kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

利用kexec快速切换内核

kexec是一个用于在当前系统下快速切换到另一个内核的一种办法,它采用了一定的机制略过了硬件的初始化,所以切换速度会很快。

自2.6.13以后,Linux内核就已经自置了kexec,而Debian采用的内核已经是2.6.26,而且默认就支持kexec,所以在Debian下我们只要安装kexec-tools就行了。

1
2
$ yum install kexec-tools
$ sudo apt-get install kexec-tools

安装好以后,就可以开始加载其他的内核了。
先看看我有哪些内核可以用:

1
2
3
$ ls /boot/vmlinuz-2.6.26-1-*
/boot/vmlinuz-2.6.26-1-amd64         
/boot/vmlinuz-2.6.26-1-vserver-amd64

好多好多,再看看当前的内核

1
2
$ uname -r
2.6.26-1-vserver-amd64

好了,现在我打算切换到2.6.26-1-amd64去:
记得,需要root权限的

1
$ sudo -s

先要用kexec加载它,先看看该追加哪些参数

1
2
3
4
$ cat /boot/grub/menu.lst | grep 2.6.26-1-amd64
title Debian GNU/Linux, kernel 2.6.26-1-amd64
kernel /vmlinuz-2.6.26-1-amd64 root=/dev/sda1 ro
initrd /initrd.img-2.6.26-1-amd64

找到了,对照上面开始用kexec加载了

1
$ kexec -l /boot/vmlinuz-2.6.26-1-amd64 --initrd=/boot/initrd.img-2.6.26-1-amd64 --append="root=/dev/sda1 ro"

加载以后并不直接执行哦,所以我们要执行一下才会切换

1
$ kexec -e

不要紧张,等一下下就好了,起来以后还会提示登录的
看看我的效果:

1
2
$ uname -r
2.6.26-1-amd64

切换到我想要的内核了

内核抢占与中断返回

1、上下文

一般来说,CPU在任何时刻都处于以下三种情况之一:
(1)运行于用户空间,执行用户进程;
(2)运行于内核空间,处于进程上下文;
(3)运行于内核空间,处于中断上下文。
应用程序通过系统调用陷入内核,此时处于进程上下文。现代几乎所有的CPU体系结构都支持中断。当外部设备产生中断,向CPU发送一个异步信号,CPU调用相应的中断处理程序来处理该中断,此时CPU处于中断上下文。

在进程上下文中,可以通过current关联相应的任务。进程以进程上下文的形式运行在内核空间,可以发生睡眠,所以在进程上下文中,可以使作信号量(semaphore)。实际上,内核经常在进程上下文中使用信号量来完成任务之间的同步,当然也可以使用锁。

中断上下文不属于任何进程,它与current没有任何关系(尽管此时current指向被中断的进程)。由于没有进程背景,在中断上下文中不能发生睡眠,否则又如何对它进行调度。所以在中断上下文中只能使用锁进行同步,正是因为这个原因,中断上下文也叫做原子上下文(atomic context)(关于同步以后再详细讨论)。在中断处理程序中,通常会禁止同一中断,甚至会禁止整个本地中断,所以中断处理程序应该尽可能迅速,所以又把中断处理分成上部和下部(关于中断以后再详细讨论)。

2、上下文切换

上下文切换,也就是从一个可执行进程切换到另一个可执行进程。上下文切换由函数context_switch()函数完成,该函数位于kernel/sched.c中,它由进程调度函数schedule()调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static inline
task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next)
{
	struct mm_struct *mm = next->mm;
	struct mm_struct *oldmm = prev->active_mm;

	if (unlikely(!mm)) {
		next->active_mm = oldmm;
		atomic_inc(&oldmm->mm_count);
		enter_lazy_tlb(oldmm, next);
	} else
		switch_mm(oldmm, mm, next);

	if (unlikely(!prev->mm)) {
		prev->active_mm = NULL;
		WARN_ON(rq->prev_mm);
		rq->prev_mm = oldmm;
	}

	/* Here we just switch the register state and the stack. */
	switch_to(prev, next, prev);

	return prev;
}

其中,switch_mm()将虚拟内存映射到新的进程;switch_to完成最终的进程切换,它保存原进程的所有寄存器信息,恢复新进程的所有寄存器信息,并执行新的进程。无论何时,内核想要进行任务切换,都通过调用schedule()完成任务切换。

2.2、用户抢占

当内核即将返回用户空间时,内核会检查need_resched是否设置,如果设置,则调用schedule(),此时,发生用户抢占。一般来说,用户抢占发生几下情况:
(1)从系统调用返回用户空间;
(2)从中断(异常)处理程序返回用户空间。

2.3、内核抢占

内核从2.6开始就支持内核抢占,对于非内核抢占系统,内核代码可以一直执行,直到完成,也就是说当进程处于内核态时,是不能被抢占的(当然,运行于内核态的进程可以主动放弃CPU,比如,在系统调用服务例程中,由于内核代码由于等待资源而放弃CPU,这种情况叫做计划性进程切换(planned process switch))。但是,对于由异步事件(比如中断)引起的进程切换,抢占式内核与非抢占式是有区别的,对于前者叫做强制性进程切换(forced process switch)。

为了支持内核抢占,内核引入了preempt_count字段,该计数初始值为0,每当使用锁时加1,释放锁时减1。当preempt_count为0时,表示内核可以被安全的抢占,大于0时,则禁止内核抢占。该字段对应三个不同的计数器(见软中断一节),也就是说在以下三种任何一种情况,该字段的值都会大于0。

(1) 内核执行中断处理程序时,通过irq_enter增加中断计数器的值;
#define irq_enter() (preempt_count() += HARDIRQ_OFFSET)
(2) 可延迟函数被禁止(执行软中断和tasklet时经常如此,由local_bh_disable完成;
(3) 通过把抢占计数器设置为正而显式禁止内核抢占,由preempt_disable完成。

当从中断返回内核空间时,内核会检preempt_count和need_resched的值(返回用户空间时只需要检查need_resched),如查preempt_count为0且need_resched设置,则调用schedule(),完成任务抢占。一般来说,内核抢占发生以下情况:
(1) 从中断(异常)返回时,preempt_count为0且need_resched置位(见从中断返回);
(2) 在异常处理程序中(特别是系统调用)调用preempt_enable()来允许内核抢占发生;

1
2
3
4
5
6
7
8
//incude/linux/preempt.h
#define preempt_enable() \
do { \
	//抢占计数器值减1
	preempt_enable_no_resched(); \
	//检查是否需要进行内核抢占调度,见(3)
	preempt_check_resched(); \
} while (0)

(3) 启用可延迟函数时,即调用local_bh_enable()时发生;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//kernel/softirq.c
void local_bh_enable(void)
{
	WARN_ON(irqs_disabled());
	/*
	 * Keep preemption disabled until we are done with
	 * softirq processing:
	 */
	//软中断计数器值减1
	preempt_count() -= SOFTIRQ_OFFSET - 1;

	if (unlikely(!in_interrupt() && local_softirq_pending()))
		do_softirq(); //软中断处理
	//抢占计数据器值减1
	dec_preempt_count();
	
	//检查是否需要进行内核抢占调度
	preempt_check_resched();
}

//include/linux/preempt.h
#define preempt_check_resched() \
do { \
	//检查need_resched
	if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \
		//抢占调度
		preempt_schedule(); \
} while (0)

//kernel/sched.c
asmlinkage void __sched preempt_schedule(void)
{
	struct thread_info *ti = current_thread_info();

	/*
	 * If there is a non-zero preempt_count or interrupts are disabled,
	 * we do not want to preempt the current task.  Just return..
	 */
	 //检查是否允许抢占,本地中断关闭,或者抢占计数器值不为0时不允许抢占
	if (unlikely(ti->preempt_count || irqs_disabled()))
		return;

need_resched:
	ti->preempt_count = PREEMPT_ACTIVE;
	//发生调度
	schedule();
	ti->preempt_count = 0;

	/* we could miss a preemption opportunity between schedule and now */
	barrier();
	if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
		goto need_resched;
}

(4) 内核任务显示调用schedule(),例如内核任务阻塞时,就会显示调用schedule(),该情况属于内核自动放弃CPU。

5、从中断返回

当内核从中断返回时,应当考虑以下几种情况:
(1) 内核控制路径并发执行的数量:如果为1,则CPU返回用户态。
(2) 挂起进程的切换请求:如果有挂起请求,则进行进程调度;否则,返回被中断的进程。
(3) 待处理信号:如果有信号发送给当前进程,则必须进行信号处理。
(4) 单步调试模式:如果调试器正在跟踪当前进程,在返回用户态时必须恢复单步模式。
(5) Virtual-8086模式:如果中断时CPU处于虚拟8086模式,则进行特殊的处理。

4.1从中断返回

中断返回点为ret_from-intr: // 从中断返回

1
2
3
4
5
6
7
ret_from_intr:
	GET_THREAD_INFO(%ebp)
	movl EFLAGS(%esp), %eax        # mix EFLAGS and CS
	movb CS(%esp), %al
	testl $(VM_MASK | 3), %eax #是否运行在VM86模式或者用户态
	/*中断或异常发生时,处于内核空间,则返回内核空间;否则返回用户空间*/
	jz resume_kernel        # returning to kernel or vm86-space

从中断返回时,有两种情况,一是返回内核态,二是返回用户态。

5.1.1、返回内核态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifdef CONFIG_PREEMPT 
/*返回内核空间,先检查preempt_count,再检查need_resched*/
ENTRY(resume_kernel)
	/*是否可以抢占,即preempt_count是否为0*/
	cmpl $0,TI_preempt_count(%ebp)    # non-zero preempt_count ?
	jnz restore_all #不能抢占,则恢复被中断时处理器状态
	
need_resched:
	movl TI_flags(%ebp), %ecx    # need_resched set ?
	testb $_TIF_NEED_RESCHED, %cl #是否需要重新调度
	jz restore_all #不需要重新调度
	testl $IF_MASK,EFLAGS(%esp)     # 发生异常则不调度
	jz restore_all
	#将最大值赋值给preempt_count,表示不允许再次被抢占
	movl $PREEMPT_ACTIVE,TI_preempt_count(%ebp)
	sti
	call schedule #调度函数
	cli
	movl $0,TI_preempt_count(%ebp) #preempt_count还原为0
	#跳转到need_resched,判断是否又需要发生被调度
	jmp need_resched
#endif
5.1.2、返回用户态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*返回用户空间,只需要检查need_resched*/
ENTRY(resume_userspace)  #返回用户空间,中断或异常发生时,任务处于用户空间
	 cli                # make sure we don't miss an interrupt
		            # setting need_resched or sigpending
		            # between sampling and the iret
	movl TI_flags(%ebp), %ecx
	andl $_TIF_WORK_MASK, %ecx    # is there any work to be done on
		            # int/exception return?
	jne work_pending #还有其它工作要做
	jmp restore_all #所有工作都做完,则恢复处理器状态

#恢复处理器状态
restore_all:
	RESTORE_ALL

	# perform work that needs to be done immediately before resumption
	ALIGN
	
	#完成其它工作
work_pending:
	testb $_TIF_NEED_RESCHED, %cl #检查是否需要重新调度
	jz work_notifysig #不需要重新调度
 #需要重新调度
work_resched:
	call schedule #调度进程
	cli                # make sure we don't miss an interrupt
		            # setting need_resched or sigpending
		            # between sampling and the iret
	movl TI_flags(%ebp), %ecx
	/*检查是否还有其它的事要做*/
	andl $_TIF_WORK_MASK, %ecx    # is there any work to be done other
		            # than syscall tracing?
	jz restore_all #没有其它的事,则恢复处理器状态
	testb $_TIF_NEED_RESCHED, %cl
	jnz work_resched #如果need_resched再次置位,则继续调度
#VM和信号检测
work_notifysig:                # deal with pending signals and
		            # notify-resume requests
	testl $VM_MASK, EFLAGS(%esp) #检查是否是VM模式
	movl %esp, %eax
	jne work_notifysig_v86        # returning to kernel-space or
		            # vm86-space
	xorl %edx, %edx
	#进行信号处理
	call do_notify_resume
	jmp restore_all

	ALIGN
work_notifysig_v86:
	pushl %ecx            # save ti_flags for do_notify_resume
	call save_v86_state        # %eax contains pt_regs pointer
	popl %ecx
	movl %eax, %esp
	xorl %edx, %edx
	call do_notify_resume #信号处理
	jmp restore_all
5.2、从异常返回

异常返回点为ret_from_exception: #从异常返回
ALIGN
ret_from_exception:
preempt_stop /相当于cli,从中断返回时,在handle_IRQ_event已经关中断,不需要这步/

6、从系统调用返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	#系统调用入口
ENTRY(system_call)
	pushl %eax            # save orig_eax
	SAVE_ALL
	GET_THREAD_INFO(%ebp)
		            # system call tracing in operation
	testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
	jnz syscall_trace_entry
	cmpl $(nr_syscalls), %eax
	jae syscall_badsys
syscall_call:
	#调用相应的函数
	call *sys_call_table(,%eax,4)
	movl %eax,EAX(%esp)        # store the return value,返回值保存到eax
#系统调用返回
syscall_exit:
	cli                # make sure we don't miss an interrupt
		            # setting need_resched or sigpending
		            # between sampling and the iret
	movl TI_flags(%ebp), %ecx
	testw $_TIF_ALLWORK_MASK, %cx    # current->work,检查是否还有其它工作要完成
	jne syscall_exit_work
#恢复处理器状态
restore_all:
	RESTORE_ALL

#做其它工作
syscall_exit_work:
	 #检查是否系统调用跟踪,审计,单步执行,不需要则跳到work_pending(进行调度,信号处理)
	testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SINGLESTEP), %cl
	jz work_pending
	sti                # could let do_syscall_trace() call
		            # schedule() instead
	movl %esp, %eax
	movl $1, %edx
	#系统调用跟踪
	call do_syscall_trace
	#返回用户空间
	jmp resume_userspace

整个中断、异常和系统调用返回流程如下:

kmalloc 函数详解

1
2
#include <linux/slab.h>
void *kmalloc(size_t size, int flags);

给 kmalloc 的第一个参数是要分配的块的大小. 第 2 个参数, 分配标志, 非常有趣, 因为它以几个方式控制 kmalloc 的行为.

最一般使用的标志, GFP_KERNEL, 意思是这个分配((内部最终通过调用 _get_free_pages 来进行, 它是 GFP 前缀的来源) 代表运行在内核空间的进程而进行的. 换句话说, 这意味着调用函数是代表一个进程在执行一个系统调用. 使用 GFP_KENRL 意味着 kmalloc 能够使当前进程在少内存的情况下睡眠来等待一页.

一个使用 GFP_KERNEL 来分配内存的函数必须, 因此, 是可重入的并且不能在原子上下文中运行. 当当前进程睡眠, 内核采取正确的动作来定位一些空闲内存, 或者通过刷新缓存到磁盘或者交换出去一个用户进程的内存.

GFP_KERNEL 不一直是使用的正确分配标志; 有时 kmalloc 从一个进程的上下文的外部调用. 例如, 这类的调用可能发生在中断处理, tasklet, 和内核定时器中. 在这个情况下, 当前进程不应当被置为睡眠, 并且驱动应当使用一个 GFP_ATOMIC 标志来代替. 内核正常地试图保持一些空闲页以便来满足原子的分配. 当使用 GFP_ATOMIC 时, kmalloc 能够使用甚至最后一个空闲页. 如果这最后一个空闲页不存在, 但是, 分配失败.

其他用来代替或者增添 GFP_KERNEL 和 GFP_ATOMIC 的标志, 尽管它们 2 个涵盖大部分设备驱动的需要. 所有的标志定义在 <linux/gfp.h>, 并且每个标志用一个双下划线做前缀, 例如 __GFP_DMA. 另外, 有符号代表常常使用的标志组合; 这些缺乏前缀并且有时被称为分配优先级. 后者包括:

1
2
3
4
5
6
7
GFP_ATOMIC   用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠.  
GFP_KERNEL    内核内存的正常分配. 可能睡眠.  
GFP_USER  用来为用户空间页来分配内存; 它可能睡眠.  
GFP_HIGHUSER  如同 GFP_USER, 但是从高端内存分配, 如果有. 高端内存在下一个子节描述.  
GFP_NOIO  
GFP_NOFS  
这个标志功能如同 GFP_KERNEL, 但是它们增加限制到内核能做的来满足请求. 一个 GFP_NOFS 分配不允许进行任何文件系统调用, 而 GFP_NOIO 根本不允许任何 I/O 初始化. 它们主要地用在文件系统和虚拟内存代码, 那里允许一个分配睡眠, 但是递归的文件系统调用会是一个坏注意.
上面列出的这些分配标志可以是下列标志的相或来作为参数, 这些标志改变这些分配如何进行:
1
2
3
4
5
6
7
8
9
__GFP_DMA    这个标志要求分配在能够 DMA 的内存区. 确切的含义是平台依赖的并且在下面章节来解释.  
__GFP_HIGHMEM 这个标志指示分配的内存可以位于高端内存.  
__GFP_COLD    正常地, 内存分配器尽力返回"缓冲热"的页 -- 可能在处理器缓冲中找到的页. 相反, 这个标志请求一个"冷"页, 它在一段时间没被使用. 它对分配页作 DMA 读是有用的, 此时在处理器缓冲中出现是无用的.  
__GFP_NOWARN  这个很少用到的标志阻止内核来发出警告(使用 printk ), 当一个分配无法满足.  
__GFP_HIGH    这个标志标识了一个高优先级请求, 它被允许来消耗甚至被内核保留给紧急状况的最后的内存页.  
__GFP_REPEAT  
__GFP_NOFAIL  
__GFP_NORETRY  
这些标志修改分配器如何动作, 当它有困难满足一个分配. __GFP_REPEAT 意思是" 更尽力些尝试" 通过重复尝试 -- 但是分配可能仍然失败. __GFP_NOFAIL 标志告诉分配器不要失败; 它尽最大努力来满足要求. 使用 __GFP_NOFAIL 是强烈不推荐的; 可能从不会有有效的理由在一个设备驱动中使用它. 最后, __GFP_NORETRY 告知分配器立即放弃如果得不到请求的内存.

kmalloc 能够分配的内存块的大小有一个上限. 这个限制随着体系和内核配置选项而变化. 如果你的代码是要完全可移植, 它不能指望可以分配任何大于 128 KB. 如果你需要多于几个 KB

这方面的原因:
kmalloc并不直接从分页机制中获得空闲页面而是从slab页面分配器那儿获得需要的页面,slab的实现代码限制了最大分配的大小为128k,即 131072bytes,理论上你可以通过更改slab.c中的 cache_sizes数组中的最大值使得kmalloc可以获得更大的页面数,不知道有没有甚么副效应或者没有必要这样做,因为获取较大内存的方法有很 多,想必128k是经验总结后的合适值。

alloc_page( )可以分配的最大连续页面是4K

1
2
3
4
5
6
7
8
9
static inline struct page * alloc_pages(unsigned int gfp_mask, unsigned int order) 
{ 
	/*
	 * Gets optimized away by the compiler. 
	 */ 
	if (order >= MAX_ORDER) 
	return NULL; 
	return _alloc_pages(gfp_mask, order); 
} 

alloc_pages最大分配页面数为512个,则可用内存数最大为29*4K=2M