kk Blog —— 通用基础


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

Linux的进程内核栈

http://blog.chinaunix.net/uid-20543672-id-2996319.html

  • 内核具有非常小的栈,它可能只和一个4096或8192字节大小的页那样小

什么是进程的“内核栈”?

在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先用户空间中的栈,而是一个内核空间的栈,这个称作进程的“内核栈”。

比如,有一个简单的字符驱动实现了open方法。在这个驱动挂载后,应用程序对那个驱动所对应的设备节点执行open操作,这个应用程序的open其实 就通过glib库调用了Linux的open系统调用,执行系统调用陷入内核后,处理器转换为了特权模式(具体的转换机制因构架而异,对于ARM来说普通 模式和用户模式的的栈针(SP)是不同的寄存器),此时使用的栈指针就是内核栈指针,他指向内核为每个进程分配的内核栈空间。

内核栈的作用

我个人的理解是:在陷入内核后,系统调用中也是存在函数调用和自动变量,这些都需要栈支持。用户空间的栈显然不安全,需要内核栈的支持。此外,内核栈同时用于保存一些系统调用前的应用层信息(如用户空间栈指针、系统调用参数)。

内核栈与进程结构体的关联

每个进程在创建的时候都会得到一个内核栈空间,内核栈和进程的对应关系是通过2个结构体中的指针成员来完成的:

(1)struct task_struct

在学习Linux进程管理肯定要学的结构体,在内核中代表了一个进程,其中记录的进程的所有状态信息,定义在Sched.h (include\linux)。
其中有一个成员:void *stack;就是指向下面的内核栈结构体的“栈底”。
在系统运行的时候,宏current获得的就是当前进程的struct task_struct结构体。

(2)内核栈结构体union thread_union
1
2
3
4
union thread_union {
	struct thread_info thread_info;
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};

其中struct thread_info是记录部分进程信息的结构体,其中包括了进程上下文信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
 * low level task data that entry.S needs immediate access to.
 * __switch_to() assumes cpu_context follows immediately after cpu_domain.
 */
struct thread_info {
	unsigned long    flags;    /* low level flags */
	int      preempt_count;  /* 0 => preemptable, <0 => bug */
	mm_segment_t    addr_limit;  /* address limit */
	struct task_struct  *task;    /* main task structure */
	struct exec_domain  *exec_domain;  /* execution domain */
	__u32      cpu;    /* cpu */
	__u32      cpu_domain;  /* cpu domain */
	struct cpu_context_save  cpu_context;  /* cpu context */
	__u32      syscall;  /* syscall number */
	__u8      used_cp[16];  /* thread used copro */
	unsigned long    tp_value;
	struct crunch_state  crunchstate;
	union fp_state    fpstate __attribute__((aligned(8)));
	union vfp_state    vfpstate;
#ifdef CONFIG_ARM_THUMBEE
	unsigned long    thumbee_state;  /* ThumbEE Handler Base register */
	#endif
	struct restart_block  restart_block;
};

关键是其中的task成员,指向的是所创建的进程的struct task_struct结构体

而其中的stack成员就是内核栈。从这里可以看出内核栈空间和 thread_info是共用一块空间的。如果内核栈溢出, thread_info就会被摧毁,系统崩溃了~~~

内核栈—struct thread_info—-struct task_struct三者的关系入下图:

内核栈的产生

在进程被创建的时候,fork族的系统调用中会分别为内核栈和struct task_struct分配空间,调用过程是: fork族的系统调用—>do_fork—>copy_process—>dup_task_struct 在dup_task_struct函数中:

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
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
	struct task_struct *tsk;
	struct thread_info *ti;
	unsigned long *stackend;

	int err;

	prepare_to_copy(orig);

	tsk = alloc_task_struct();
	if (!tsk)
		return NULL;

	ti = alloc_thread_info(tsk);
	if (!ti) {
		free_task_struct(tsk);
		return NULL;
	}

	err = arch_dup_task_struct(tsk, orig);
	if (err)
		goto out;

	tsk->stack = ti;

	err = prop_local_init_single(&tsk->dirties);
	if (err)
		goto out;

	setup_thread_stack(tsk, orig);
	......

其中alloc_task_struct使用内核的slab分配器去为所要创建的进程分配struct task_struct的空间
而alloc_thread_info使用内核的伙伴系统去为所要创建的进程分配内核栈(union thread_union )空间

注意:

后面的tsk->stack = ti;语句,这就是关联了struct task_struct和内核栈 而在setup_thread_stack(tsk, orig);中,关联了内核栈和struct task_struct:

1
2
3
4
5
static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)
{
	*task_thread_info(p) = *task_thread_info(org);
	task_thread_info(p)->task = p;
}

内核栈的大小

由于是每一个进程都分配一个内核栈空间,所以不可能分配很大。这个大小是构架相关的,一般以页为单位。其实也就是上面我们看到的THREAD_SIZE, 这个值一般为4K或者8K。对于ARM构架,这个定义在Thread_info.h (arch\arm\include\asm),

1
2
3
#define THREAD_SIZE_ORDER  1
#define THREAD_SIZE   8192
#define THREAD_START_SP   (THREAD_SIZE - 8)

所以ARM的内核栈是8KB 在(内核)驱动编程时需要注意的问题: 由于栈空间的限制,在编写的驱动(特别是被系统调用使用的底层函数)中要注意避免对栈空间消耗较大的代码,比如递归算法、局部自动变量定义的大小等等

x86—EFLAGS寄存器详解

鉴于EFLAGS寄存器的重要性,所以将这一部分内容从处理器体系结构及寻址模式一文中单独抽出另成一文,这部分内容主要来自Intel Developer Mannual,在后续的内核系列中遇到的许多和EFLAGS寄存器有关的内容将直接从本文引用。众所周知,IA-32体系结构为通用系统(general system)提供了16个基本的程序执行寄存器:包含一些通用目的寄存器(General-purpose registers)、段寄存器(Segment registers)以及EFLAGS和EIP寄存器,而后两者对于程序的执行来说可谓至关重要。

其中EIP寄存器主要用于存放当前代码段即将被执行的下一条指令的偏移,但其本质上并 不能直接被指令直接访问。 【it is controlled implicitly by control-transfer instructions (such as JMP, Jcc, CALL, and RET), interrupts, and exceptions.】 很显然,这个寄存器指令由控制转移指令、中断及异常所控制。 【The only way to read the EIP register is to execute a CALL instruction and then read the value of the return instruction pointer from the procedure stack.The EIP register can be loaded indirectly by modifying the value of a return instruction pointer on the procedure stack and executing a return instruction (RET or IRET)】 这里也已经说的很清楚了,读操作通过执行call指令并取得栈中所存放的地址来实现,而写操作则通过修改程序栈中的返回指令指针并执行RET/IRET指 令来完成,因此尽管这个寄存器相当重要,但其实并不是操作系统在实现过程中所需关注的焦点。

相对来讲,EFLAGS寄存器对于操作系统则重要得多。EFLAGS(program status and control) register主要用于提供程序的状态及进行相应的控制, 【The EFLGAS register report on the status of the program being executed and allows limited(application-program level) control of the process.】 在64-bit模式下,EFLGAS寄存器被扩展为64位的RFLGAS寄存器,高32位被保留,而低32位则与EFLAGS寄存器相同。

32位的EFLAGS寄存器包含一组状态标志、系统标志以及一个控制标志。在x86处理器初始化之后,EFLAGS寄存器的状态值为0000 0002H。 第1、3、5、15以及22到31位均被保留,这个寄存器中的有些标志通过使用特殊的通用指令可以直接被修改,但并没有指令能够检查或者修改整个寄存器。 通过使用LAHF/SAHF/PUSHF/POPF/POPFD等指令,可以将EFLAGS寄存器的标志位成组移到程序栈或EAX寄存器,或者从这些设施 中将操作后的结果保存到EFLAGS寄存器中。在EFLAGS寄存器的内容被传送到栈或是EAX寄存器后,可以通过位操作指令(BT, BTS, BTR, BTC)检查或修改这些标志位。当调用中断或异常处理程序时,处理器将在程序栈上自动保存EFLAGS的状态值。若 在中断或异常处理时发生任务切换,那么EFLAGS寄存器的状态将被保存在TSS中 【the state of the EFLAGS register is saved in the TSS for the task being suspended.】 ,注意是将要被挂起的本次任务的状态。

1、状态标志(Status Flags)

EFLAGS寄存器的状态标志(0、2、4、6、7以及11位)指示算术指令(如ADD, SUB, MUL以及DIV指令)的结果,这些状态标志的作用如下:

1
2
3
4
5
6
CF(bit 0) [Carry flag]   若算术操作产生的结果在最高有效位(most-significant bit)发生进位或借位则将其置1,反之清零。这个标志指示无符号整型运算的溢出状态,这个标志同样在多倍精度运算(multiple-precision arithmetic)中使用。
PF(bit 2) [Parity flag]   如果结果的最低有效字节(least-significant byte)包含偶数个1位则该位置1,否则清零。
AF(bit 4) [Adjust flag]   如果算术操作在结果的第3位发生进位或借位则将该标志置1,否则清零。这个标志在BCD(binary-code decimal)算术运算中被使用。
ZF(bit 6) [Zero flag]   若结果为0则将其置1,反之清零。
SF(bit 7) [Sign flag]   该标志被设置为有符号整型的最高有效位。(0指示结果为正,反之则为负)
OF(bit 11) [Overflow flag]   如果整型结果是较大的正数或较小的负数,并且无法匹配目的操作数时将该位置1,反之清零。这个标志为带符号整型运算指示溢出状态。

在这些状态标志中,只有CF标志能够通过使用STC, CLC以及CMC指令被直接修改,或者通过位指令(BT, BTS, BTR以及BTC)将指定的位拷贝至CF标志中。

这些状态标志允许单个的算术操作产生三种不同数据类型的结果:无符号整型,有符号整型 以及BCD整型。如果把该结果当做无符号整型,那么CF标志指示越界(out-of-range)状态——即进位或借位,如果被当做有符号整型,则OF标 志指示进位或借位,若作为BCD数,那么AF标志指示进位或借位。SF标志指示有符号整数的符号位,ZF指示结果为零。此外在执行多倍精度算术运算时,CF标志用来将一次运算过程中带进位的加法(ADC)或带借位的减法(SBB)产生的进位或借位传递到下一次运算过程中。

2、DF标志(DF flag)

这个方向标志(位于EFLAGS寄存器的第10位)控制串指令(MOVS, CMPS, SCAS, LODS以及STOS)。设置DF标志使得串指令自动递减(从高地址向低地址方向处理字符串),清除该标志则使得串指令自动递增。STD以及CLD指令分 别用于设置以及清除DF标志。

3、系统标志以及IOPL域(System Flags and IOPL Field)

EFLAGS寄存器中的这部分标志用于控制操作系统或是执行操作,它们不允许被应用程序所修改。这些标志的作用如下:

1
2
3
4
5
6
7
8
9
10
TF(bit 8) [Trap flag]   将该位设置为1以允许单步调试模式,清零则禁用该模式。
IF(bit 9) [Interrupt enable flag]   该标志用于控制处理器对可屏蔽中断请求(maskable interrupt requests)的响应。置1以响应可屏蔽中断,反之则禁止可屏蔽中断。
IOPL(bits 12 and 13) [I/O privilege level field]   指示当前运行任务的I/O特权级(I/O privilege level),正在运行任务的当前特权级(CPL)必须小于或等于I/O特权级才能允许访问I/O地址空间。这个域只能在CPL为0时才能通过POPF以及IRET指令修改。
NT(bit 14) [Nested task flag]   这个标志控制中断链和被调用任务。若当前任务与前一个执行任务相关则置1,反之则清零。
RF(bit 16) [Resume flag]   控制处理器对调试异常的响应。
VM(bit 17) [Virtual-8086 mode flag]   置1以允许虚拟8086模式,清除则返回保护模式。
AC(bit 18) [Alignment check flag]   该标志以及在CR0寄存器中的AM位置1时将允许内存引用的对齐检查,以上两个标志中至少有一个被清零则禁用对齐检查。
VIF(bit 19) [Virtual interrupt flag]   该标志是IF标志的虚拟镜像(Virtual image),与VIP标志结合起来使用。使用这个标志以及VIP标志,并设置CR4控制寄存器中的VME标志就可以允许虚拟模式扩展(virtual mode extensions)
VIP(bit 20) [Virtual interrupt pending flag]   该位置1以指示一个中断正在被挂起,当没有中断挂起时该位清零。【Software sets and clears this flag; the processor only reads it.】与VIF标志结合使用。
ID(bit 21) [Identification flag]   程序能够设置或清除这个标志指示了处理器对CPUID指令的支持。

Linux内核获取当前进程指针

一、内存数据表示:

我们在教材或阅读中,经常需要直观的用图示来展示数据在内存中的分布,那么数据是如何在内存中组织的呢?不同的机器有不同的表示法,我们以最常见的Intel X86系列计算机为例来说明这个问题。

如上图示内存示意图:内存低址在上。内存高址在下,内存单位为16bit。对于基于intel i386架构的计算机,系统采用小端字节序来存放数据,所谓小端字节序是指低序字节低地址,高序字节高地址(内存地址增大方向),大端字节序反之,给定系统所用的字节序称为主机字节序;CPU也以小端字节序形式读取数据,如上图所示,如果变量num是16位的short短整类型,则CPU从内存中读出的num=0x1234;如果num是32位的int类型,则CPU从内存中读出的是num=0x56781234,其中num地址是0x12345678,即&num=0x12345678

二、linux内核获取进程任务结构的指针

明白了系统内存数据表示,我们现在来看看linux内核是如何获取当前进程的任务结构指针的,以下代码均参照linux内核2.4.0的源码。
在include\asm-i386\ current.h中

1
2
3
4
5
6
7
8
9
10
11
#ifndef _I386_CURRENT_H
#define _I386_CURRENT_H
struct task_struct;
static inline struct task_struct * get_current(void)
{
	struct task_struct *current;
	__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
	return current;
}
#define current get_current()
#endif /* !(_I386_CURRENT_H) */

每个进程都有一个task_struct任务结构,和一片用于系统空间堆栈的存储空间,他们在物理内存空间中也是联系在一起的,当给进程申请task_struct任务结构空间时,系统将连同系统的堆栈空间一起分配,如下图为某个进程切换时刻的内存图:

下面针对代码实现来分析一下系统如何通过一系列操作获取进程在内核中的任务结构指针的: 由于linux内核分配进程任务结构空间时,是以8KB(2个页面空间,即21*4KB,linux对物理内存空间和虚拟内存空间管理时,均规定其页面单位的尺寸为4KB)为单位来分配的,所以内存应用地址是8KB(213)的整数倍,即指针地址的低13位全为0,所以根据小端字节序,分配内存返回地址应该是指向struct task_struct结构,如图中的0xc2342000地址所指,至于为何采用代码中的做法而不是直接将此指针保存在全局变量中以供应用,内核是从其自身的效率方面来考虑的,我们在此只针对代码解释: 根据上图,此刻内存esp内容必定在0xc2342000和0xc2344000之间的一个数值,我们假设取0xc2343ffe(即堆栈压栈EIP、返回地址、内部数据等相关数据了,地址值要减小;只要符合0xc2342xxx 、0xc2343xxx的地址指针都是正确的),来通过代码运算看是否current的指针是0xc2342000。

1
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));

语句的意思是将ESP的内容与8191UL的反码按位进行与操作,之后再把结果赋值给current,其中8191UL=8192-1=213-1,计算过程如下:

1
2
3
4
5
6
7
8192UL=2^13 0000 0000 0000 0000 0010 0000 0000 0000
8191UL 0000 0000 0000 0000 0001 1111 1111 1111
~8191UL(反码) 1111 1111 1111 1111 1110 0000 0000 0000
0xc2343ffe 1100 0010 0011 0100 0011 1111 1111 1110 
andl结果: 1100 0010 0011 0100 0010 0000 0000 0000
|| (对照着看)
0x c 2 3 4 2 0 0 0

所以按位与操作之后的结果位0xc2342000,正好是struct task_struct结构的地址指针.通过观察可知,只要符合0xc2342xxx 、0xc2343xxx的地址指针经过相同的计算,都可以得到内核进程任务结构的指针。
另外,在进入中断或系统调用时所引用的宏操作(include\asm-i386\ hw_irq.h):

1
2
3
#define GET_CURRENT \
	"movl %esp, %ebx\n\t" \
	"andl $-8192, %ebx\n\t"

其原理与上述描述也是一致的。