# 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(&regs, 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, &regs, 0, NULL, NULL);

}
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

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上下文 寄存器信息

 ...

}
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
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 然后复制信息

 ...

}
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

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(&current->fs->count);

    return 0;

 } // 负创建新的fs_struct并将父进程的fs信息复制给子进程

  tsk->fs = __copy_fs_struct(current->fs);

  if (!tsk->fs) // 创建失败,可能由于内存不足发生错误,那么返回异常信息

    return -ENOMEM;

  return 0;

}
1
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(&current->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;

}
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

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 信息复制到子进程中

 ...

}
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

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

 ...

}
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
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 // 跳转到该函数退出系统调用
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

syscall_exit 函数

该函数用于从系统调用返回,可以看到,这里将上述保存的pt_regs的值弹出到寄存器中,此时完成了对内核线程的创建。源码如下。为了方便,这里将上面设置函数放到这里。

int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)

{

  struct pt_regs regs; // 保存进程 CPU 上下文中寄存器信息

  memset(&regs, 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, &regs, 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");
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
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