按照传统UNIX规定的一些操作系统标准,一些重要的任务需要由进程来周期性地执行。这些任务包括刷新磁盘高速缓存,交换出不用的页框,维护网络连接等等。那么,由于这些系统进程只运行在内核态,所以Linux将他改造了,跟一般的进程不一样了,给它取个名称叫内核线程(kernel thread)。这个内核线程跟普通进程最大的区别就是,只运行在内核态,不受不必要的用户态上下文的拖累。在Linux中,内核线程在以下几个方面不同于普通进程:
1、内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。
2、因为内核线程只运行在内核态,它们只使用大于PAGE_OFFSET的线性地址空间。另一方面,不管在用户态还是在内核态,普通进程可以用4GB的线性地址空间。
创建一个内核线程:
do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL)
CLONE_VM标志避免复制调用进程的页表:由于新内核线程无论如何都不会访问用户态地址空间,所以这种复制无疑会造成时间和空间的浪费。
CLONE_UNTRACED标志保证不会有任何进程跟踪内核线程,因为任何跟踪都无意义。
传递给do_fork的参数regs表示该内核线程(本质上是个内核态进程)的各寄存器的值。kernel_thread函数把ebx和edx分别设置为参数fn和arg的值,以使内核栈栈顶成为该函数的入口。
把regs.xds和regs.xes设置成__USER_DS,为什么这么放,《 执行进程间切换 》博文中写得很清楚,这里不再话下。
把eip寄存器的值设置为kernel_thread_helper(同样是位于/arch/i386/kernel/entry.S文件中,记住,跟进程相关的汇编语言都在该文件中)汇编语言代码段的地址:
movl %ebx, %eax
pushl %edx
call *%ebx
pushl %eax
call do_exit
因此,新的内核线程开始执行fn(arg)函数,如果该函数结束,内核线程执行系统调用_exit(),并把fn(arg)函数的返回值传递给它。下面,我们来看看几个重要的内核线程:
(1)0号进程
系统引导时,从无到有创建一个内核线程。这个祖先进程使用下列静态分配的数据结构:
- 存放在init_task变量中的进程描述符,由INIT_TASK(tsk)宏完成对它的初始化。
- 存放在init_thread_union变量中的thread_info描述符和内核堆栈。由INIT_THREAD_INFO宏完成对它们的初始化。
- 由进程描述符指向下列表:
init_mm
init_fs
init_files
init_signals
init_sighand
- 这些表分别由下列宏进行初始化:
INIT_MM
INIT_FS
INIT_FILES
INIT_SIGNALS
INIT_SIGHAND
- 主内核页全局目录存放在swapper_pg_dir中。
start_kernel()函数初始化内核需要的所有数据结构,激活中断,创建1号进程(内核线程),称为init进程:
kernel_thread(init, NULL, CLONE_FS|CLONE_SIGHAND);
新创建的内核线程有PID了,其值为1,并与0号进程共享每进程所有的内核数据结构。此外,当调度程序选择到它时,init进程开始执行init()函数。
创建init进程后,进程0执行cpu_idle()函数,该函数本质上是在开中断情况下重复执行hlt汇编指令。只有当没有其他进程处于TASK_RUNNING状态时,调度程序才选择进程0。
(2)1号进程
由进程0创建的内核线程执行init()函数,init()依次完成内核初始化。init()调用execve()系统调用装入可执行程序init。结果,init内核线程成为一个普通进程,且拥有自己的每进程内核数据结构。在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。
(3)keventd进程
执行keventd_wq工作队列中的函数。
(4)kapmd
处理与高级电源管理相关的事件。
(5)kswapd
执行内存周期回收。
(6)pdflush
刷新“脏”缓冲区的内容到磁盘以回收内存。
(7)kblockd
执行kblockd_workqueue工作队列中的函数。实质上,它周期性地激活块设备驱动程序。
(8)ksoftirqd
运行tasklet;系统中每个CPU都有这样一个内核线程。
1、内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。
2、因为内核线程只运行在内核态,它们只使用大于PAGE_OFFSET的线性地址空间。另一方面,不管在用户态还是在内核态,普通进程可以用4GB的线性地址空间。
创建一个内核线程:
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
struct pt_regs regs;
int err;
memset(®s, 0, sizeof(regs));
regs.ebx = (unsigned long) fn;
regs.edx = (unsigned long) arg;
regs.xds = __USER_DS;
regs.xes = __USER_DS;
regs.orig_eax = -1;
regs.eip = (unsigned long) kernel_thread_helper;
regs.xcs = __KERNEL_CS;
regs.eflags = X86_EFLAGS_IF | X86_EFLAGS_SF | X86_EFLAGS_PF | 0x2;
/* Ok, create the new process.. */
err = do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
if (err == 0) /* terminate kernel stack */
task_pt_regs(current)->eip = 0;
return err;
}
kernel_thread函数创建一个新的内核线程,它接受的参数有:所要执行内核函数的地址(fn)、要传递给函数的参数(arg)、一直clone标志(flags)。该函数本质上以下面的方式调用do_fork:
do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL)
CLONE_VM标志避免复制调用进程的页表:由于新内核线程无论如何都不会访问用户态地址空间,所以这种复制无疑会造成时间和空间的浪费。
CLONE_UNTRACED标志保证不会有任何进程跟踪内核线程,因为任何跟踪都无意义。
传递给do_fork的参数regs表示该内核线程(本质上是个内核态进程)的各寄存器的值。kernel_thread函数把ebx和edx分别设置为参数fn和arg的值,以使内核栈栈顶成为该函数的入口。
把regs.xds和regs.xes设置成__USER_DS,为什么这么放,《 执行进程间切换 》博文中写得很清楚,这里不再话下。
把eip寄存器的值设置为kernel_thread_helper(同样是位于/arch/i386/kernel/entry.S文件中,记住,跟进程相关的汇编语言都在该文件中)汇编语言代码段的地址:
movl %ebx, %eax
pushl %edx
call *%ebx
pushl %eax
call do_exit
因此,新的内核线程开始执行fn(arg)函数,如果该函数结束,内核线程执行系统调用_exit(),并把fn(arg)函数的返回值传递给它。下面,我们来看看几个重要的内核线程:
(1)0号进程
系统引导时,从无到有创建一个内核线程。这个祖先进程使用下列静态分配的数据结构:
- 存放在init_task变量中的进程描述符,由INIT_TASK(tsk)宏完成对它的初始化。
- 存放在init_thread_union变量中的thread_info描述符和内核堆栈。由INIT_THREAD_INFO宏完成对它们的初始化。
- 由进程描述符指向下列表:
init_mm
init_fs
init_files
init_signals
init_sighand
- 这些表分别由下列宏进行初始化:
INIT_MM
INIT_FS
INIT_FILES
INIT_SIGNALS
INIT_SIGHAND
- 主内核页全局目录存放在swapper_pg_dir中。
start_kernel()函数初始化内核需要的所有数据结构,激活中断,创建1号进程(内核线程),称为init进程:
kernel_thread(init, NULL, CLONE_FS|CLONE_SIGHAND);
新创建的内核线程有PID了,其值为1,并与0号进程共享每进程所有的内核数据结构。此外,当调度程序选择到它时,init进程开始执行init()函数。
创建init进程后,进程0执行cpu_idle()函数,该函数本质上是在开中断情况下重复执行hlt汇编指令。只有当没有其他进程处于TASK_RUNNING状态时,调度程序才选择进程0。
(2)1号进程
由进程0创建的内核线程执行init()函数,init()依次完成内核初始化。init()调用execve()系统调用装入可执行程序init。结果,init内核线程成为一个普通进程,且拥有自己的每进程内核数据结构。在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。
(3)keventd进程
执行keventd_wq工作队列中的函数。
(4)kapmd
处理与高级电源管理相关的事件。
(5)kswapd
执行内存周期回收。
(6)pdflush
刷新“脏”缓冲区的内容到磁盘以回收内存。
(7)kblockd
执行kblockd_workqueue工作队列中的函数。实质上,它周期性地激活块设备驱动程序。
(8)ksoftirqd
运行tasklet;系统中每个CPU都有这样一个内核线程。