# Linux 内核线程原理
何为线程?何为进程?这里不在赘述,一言以蔽之:共享资源的进程,PCB均为 task_struct,我们在混沌学堂说过:进程也好,线程也好对于CPU来说,就是一段指令流,而我们今天分析的内核线程,便是一段只执行内核中代码的指令流,它也拥有task_struct pcb结构,但是它不会执行任何用户空间的代码,当它被调度执行时,运行的代码是Linux内核的代码。
kernel_thread 函数
当内核启动时,将会执行rest_init 函数,在该函数将创建一个内核线程 init ,它将负责进一步初始化操作系统并执行init 1号进程执行初始化程序,这里我们无需过多了解启动后执行的流程,把关注点放在内核线程上。
static void rest_init(void)
{
kernel_thread(init, NULL, CLONE_KERNEL); // init 表示函数指针,也即内核进程执行的IP,NULL 表示没有传递参数, CLONE_KERNEL 标志位用于传递 内核线程共享的数据:FS 文件系统信息、FILES 打开文件信息、SIGHAND 信号及处理函数
unlock_kernel();
cpu_idle();
}
#define CLONE_KERNEL (CLONE_FS | CLONE_FILES | CLONE_SIGHAND) // 内核线程标志位
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
struct pt_regs regs; // 保存进程 CPU 上下文中寄存器信息
memset(®s, 0, sizeof(regs)); // 初始化 pt_regs 内存
regs.ebx = (unsigned long) fn; // 执行函数指针保存到ebx中
regs.edx = (unsigned long) arg; // 参数指针保存在edx中
regs.xds = __USER_DS; // 用户数据段 选择子
regs.xes = __USER_DS; // 用户数据段 选择子
regs.orig_eax = -1;
regs.eip = (unsigned long) kernel_thread_helper; // 内核线程的起始代码为 kernel_thread_helper 函数
regs.xcs = __KERNEL_CS; // 内核代码段 选择子
regs.eflags = 0x286; // 为什么是这样?读者自己打开 intel 手册 一看便知
// 最后调用 do_fork 函数完成 创建(在混沌学堂的道友是不是发现:万物归一)
return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
}
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
do_fork 函数
该函数较为复杂,均是创建 控制块 task_struct ,然后设置其参数,我们这里主要跟踪上述的 flags 和 regs 中设置的对应值处理即可(注意:由于内核中不区分 进程与线程,所以这里统一一进程描述,各位只需要知道这里描述的为内核线程即可)。
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
long pid; // 进程 id
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr); // 完成实际控制块创建
pid = IS_ERR(p) ? PTR_ERR(p) : p->pid; // 获取新进程的pid
...
return pid;
}
struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p = NULL;
...
p = dup_task_struct(current); // 创建 task_struct 并复制当前 task_struct 中的数据,同时在这里创建了内核栈
...
if ((retval = copy_files(clone_flags, p))) // 处理 打开文件信息
goto bad_fork_cleanup_semundo;
if ((retval = copy_fs(clone_flags, p))) // 处理 文件系统信息
goto bad_fork_cleanup_files;
if ((retval = copy_sighand(clone_flags, p))) // 处理 信号处理函数信息
goto bad_fork_cleanup_fs;
if ((retval = copy_mm(clone_flags, p))) // 处理 内存信息
goto bad_fork_cleanup_signal;
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs); // 处理进程CPU上下文 寄存器信息
...
}
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
copy_files 函数
该函数用于处理父进程打开文件fd的信息,我们看到对于内核线程来说,指定了 CLONE_FILES 标志位,所以这里与父进程共享打开文件。源码如下。
static int copy_files(unsigned long clone_flags, struct task_struct * tsk)
{
struct files_struct *oldf, *newf;
struct file **old_fds, **new_fds;
int open_files, nfds, size, i, error = 0;
oldf = current->files;
if (!oldf) // 父进程没有打开文件,那么直接返回(对于一些后台运行的进程来说,可能没有的打开文件)
goto out;
if (clone_flags & CLONE_FILES) { // 若设置 CLONE_FILES 标志位,那么直接增加父进程 files_struct 的引用计数即可,此时表明两者共享
atomic_inc(&oldf->count);
goto out;
}
// 否则,给新进程创建新的 files_struct 然后复制信息
...
}
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
copy_fs 函数
该函数用于处理父进程文件系统的信息,我们看到对于内核线程来说,指定了 CLONE_FS 标志位,所以这里与父进程共享文件系统信息。源码如下。
static inline int copy_fs(unsigned long clone_flags, struct task_struct * tsk)
{
if (clone_flags & CLONE_FS) { // 若设置 CLONE_FS 标志位,那么直接增加父进程 fs_struct 的引用计数即可,此时表明两者共享
atomic_inc(¤t->fs->count);
return 0;
} // 负创建新的fs_struct并将父进程的fs信息复制给子进程
tsk->fs = __copy_fs_struct(current->fs);
if (!tsk->fs) // 创建失败,可能由于内存不足发生错误,那么返回异常信息
return -ENOMEM;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
copy_sighand 函数
该函数用于处理父进程信号处理函数的信息,我们看到对于内核线程来说,指定了 CLONE_SIGHAND 标志位,所以这里与父进程共享信号处理函数。源码如下。
static inline int copy_sighand(unsigned long clone_flags, struct task_struct * tsk)
{
struct sighand_struct *sig;
if (clone_flags & (CLONE_SIGHAND | CLONE_THREAD)) { // 若设置 CLONE_SIGHAND 或者 CLONE_THREAD 标志位,那么直接增加父进程 sighand_struct 的引用计数即可,此时表明两者共享
atomic_inc(¤t->sighand->count);
return 0;
}
// 否则分配新的 sighand_struct 结构,同时将父进程的信号处理函数复制到子进程中
sig = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);
tsk->sighand = sig;
if (!sig)
return -ENOMEM;
// 上锁并复制(避免进程的信号发生变换)
spin_lock_init(&sig->siglock);
atomic_set(&sig->count, 1);
memcpy(sig->action, current->sighand->action, sizeof(sig->action));
return 0;
}
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
copy_mm 函数
该函数用于处理父进程内存信息,我们看到对于内核线程来说,指定了 CLONE_VM 标志位,所以这里与父进程内存信息(毕竟内核线程们,都共享内核代码)。源码如下。
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;
...
oldmm = current->mm;
if (!oldmm) // 当前进程没有内存信息,那么直接返回(这里可能是由于内核线程创建内核线程导致,因为对于内核线程来说,它不访问用户进程的空间,所以没有独立的mm,那么问题来了?内核访问自己的代码和数据肯定需要页表,这是由于CPU MMU 单元指定的,那么怎么做?很简单,直接用上一个用户进程的mm结构,用它的页表,因为所有用户进程的内核页表部分都是一样的)
return 0;
if (clone_flags & CLONE_VM) { // 若设置 CLONE_VM 标志位,那么直接增加父进程 mm_struct 的引用计数即可,此时表明两者共享
atomic_inc(&oldmm->mm_users);
mm = oldmm;
spin_unlock_wait(&oldmm->page_table_lock);
goto good_mm;
}
// 否则,分配新的 mm_struct,并将父进程的 mm_struct 信息复制到子进程中
...
}
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
copy_thread 函数
该函数用于初始化子进程的CPU 上下文信息(寄存器),在linux 2.6的内核中,不在使用TSS状态段描述符,混沌学堂的学员一定注意:进程的切换将由操作系统来完成,使用 thread_struct 结构来替代 tss_struct,此时将不再由CPU来完成切换了。源码描述如下。
#define THREAD_SIZE (2*PAGE_SIZE) // i386 中内核进程栈大小为2页(一页4KB)
// 替代tss,将进程的上下文信息保存在此(其他通用寄存器保存在内核栈)
struct thread_struct {
/* cached TLS descriptors. */
struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES];
unsigned long esp0;
unsigned long eip;
unsigned long esp;
unsigned long fs;
unsigned long gs;
/* Hardware debugging registers */
unsigned long debugreg[8]; /* %%db0-7 debug registers */
/* fault info */
unsigned long cr2, trap_no, error_code;
/* floating point info */
union i387_union i387;
/* virtual 86 mode info */
struct vm86_struct __user * vm86_info;
unsigned long screen_bitmap;
unsigned long v86flags, v86mask, saved_esp0;
unsigned int saved_fs, saved_gs;
/* IO permissions */
unsigned long *io_bitmap_ptr;
};
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
unsigned long unused,
struct task_struct * p, struct pt_regs * regs)
{
struct pt_regs * childregs;
struct task_struct *tsk;
int err;
childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p->thread_info)) - 1; // 首先将子进程的寄存器信息放置在进程栈的栈顶(struct thread_info 结构将放置在内核栈的栈底)
struct_cpy(childregs, regs); // 将参数复制到指针指向的内存中(注意:我们在内核线程创建中放入寄存器值都会在该内存中)
childregs->eax = 0; // 子进程的返回值为0
childregs->esp = esp; // 设置子进程栈指针
...
p->thread.esp = (unsigned long) childregs; // 将子进程的用户态栈指针指向childregs地址,也即内核栈的栈顶
p->thread.esp0 = (unsigned long) (childregs+1); // 将子进程的内核态栈指针指向寄存器参数列表后的地址
p->thread.eip = (unsigned long) ret_from_fork; // 设置返回IP 为 ret_from_fork
...
}
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
ret_from_fork 函数
该函数将会在进程在被调度执行时,执行的代码(当完成fork后,父进程负责将子进程的状态设置为RUNNABLE状态,同时将其放入就绪队列中(run_queue),然后由调度器调度执行,在上面我们看到ip设置的地址为该函数,所以这是一个执行的代码)特别注意:此时运行的代码为子进程的代码。源码如下。
// 保存在进程内核栈底的结构,我们可以根据内核栈和该结构获取到进程的PCB :task_struct
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
unsigned long flags; /* low level flags */
unsigned long status; /* thread-synchronous flags */
__u32 cpu; /* current CPU */
__s32 preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit; /* thread address space:
0-0xBFFFFFFF for user-thead
0-0xFFFFFFFF for kernel-thread
*/
struct restart_block restart_block;
__u8 supervisor_stack[0];
};
ENTRY(ret_from_fork)
pushl %eax // 保存返回值 0
call schedule_tail // 调用schedule_tail函数,该函数主要完成一些清理操作了解即可
GET_THREAD_INFO(%ebp) // 获取当前进程 thread_info 指针,将其保存在 ebp 中
popl %eax // 弹出上面保存的返回值 0
jmp syscall_exit // 跳转到该函数退出系统调用
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
syscall_exit 函数
该函数用于从系统调用返回,可以看到,这里将上述保存的pt_regs的值弹出到寄存器中,此时完成了对内核线程的创建。源码如下。为了方便,这里将上面设置函数放到这里。
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
struct pt_regs regs; // 保存进程 CPU 上下文中寄存器信息
memset(®s, 0, sizeof(regs)); // 初始化 pt_regs 内存
regs.ebx = (unsigned long) fn; // 执行函数指针保存到ebx中
regs.edx = (unsigned long) arg; // 参数指针保存在edx中
regs.xds = __USER_DS; // 用户数据段 选择子
regs.xes = __USER_DS; // 用户数据段 选择子
regs.orig_eax = -1;
regs.eip = (unsigned long) kernel_thread_helper; // 内核线程的起始代码为 kernel_thread_helper 函数
regs.xcs = __KERNEL_CS; // 内核代码段 选择子
regs.eflags = 0x286; // 为什么是这样?读者自己打开 intel 手册 一看便知
// 最后调用 do_fork 函数完成 创建(在混沌学堂的道友是不是发现:万物归一)
return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
}
#define _TIF_ALLWORK_MASK 0x0000FFFF // mask 掩码,混沌学堂学员参考:与运算的作用?
syscall_exit:
cli // 关闭中断响应
movl TI_FLAGS(%ebp), %ecx // 将thread_info 中的 flags 变量保存到 ecx 中
testw $_TIF_ALLWORK_MASK, %cx // 看看是否有其他未完成的工作(ecx的低16位用于保存需要完成的操作位)
jne syscall_exit_work // 若存在未处理的工作,那么跳转到 syscall_exit_work 完成处理(我们这里了解即可,这里面关注到信号、重调度等处理,后面在混沌学堂中分析)
restore_all:
RESTORE_ALL
// 还原保存的寄存器
#define RESTORE_ALL
RESTORE_REGS
addl $4, %esp;
iret; // 从中断返回
// 还原通用寄存器
#define RESTORE_INT_REGS
popl %ebx;
popl %ecx;
popl %edx;
popl %esi;
popl %edi;
popl %ebp;
popl %eax
#define RESTORE_REGS
RESTORE_INT_REGS;
1: popl %ds; // 还原数据段寄存器
2: popl %es; // 还原扩展段寄存器
kernel_thread_helper 函数
该函数将会把ebx中保存的实际调用函数设置到eip中,完成函数的调用。源码如下。
extern void kernel_thread_helper(void);
__asm__(".align 4\n"
"kernel_thread_helper:\n\t"
"movl %edx,%eax\n\t" // 将函数参数指针放入到eax中
"pushl %edx\n\t" // 弹出edx
"call *%ebx\n\t" // 调用函数
"pushl %eax\n\t" // 将函数指针压入栈中,因为在 do_exit 中将会用到该参数
"call do_exit");
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107