内核经常需要在后台执行一些操作,这种任务就可以通过内核线程(kernle thread)完成--独立运行在内核空间的标准进程。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,mm指针被设置为NULL;它只在内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。
实际上,内核线程只能由其他内核线程创建,在现有的内核线程中创建一个新的内核线程的方法:
int kernel_thread(int(*fn)(void *),void *arg,unsigned long flags)
fn为你想要运行的函数,arg为函数的参数,flags为创建进程时的标志位,因为新的任务也是通过clone()系统调用而创建的,flags即为传递给clone函数的标志参数。
#include
#include
#include
#include
MODULE_AUTHOR("T-bagwell_CU");
MODULE_LICENSE("GPL");
static DECLARE_WAIT_QUEUE_HEAD(myevent_waitqueue);
extern unsigned int myevent_id;
static int example_kernel_thread(void *unused)
{
DECLARE_WAITQUEUE(wait, current);
daemonize("create_by_T-bag");
allow_signal(SIGKILL);
add_wait_queue(&myevent_waitqueue, &wait);
while(1){
set_current_state(TASK_INTERRUPTIBLE);
schedule();
if(signal_pending(current)){
break;
}
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&myevent_waitqueue, &wait);
printk(KERN_WARNING "This is in example_kernel_thread\n");
return 0;
}
static __init int init_hello_kernel_thread(void)
{
int ret;
ret=kernel_thread(example_kernel_thread, NULL,
CLONE_FS | CLONE_FILES | CLONE_SIGHAND | SIGCHLD );
if(unlikely(ret<0)){
printk(KERN_WARNING "kernel_thread create failed \n");
}
else{
printk(KERN_WARNING "kernel_thread create success \n");
}
return 0;
}
static __exit void cleanup_hello_kernel_thread(void)
{
printk(KERN_WARNING "kernel_thread exit \n");
return ;
}
module_init(init_hello_kernel_thread);
module_exit(cleanup_hello_kernel_thread);
以上代码就是创建一个内核线程的例子,example_kernel_thread即为创建的内核线程。一般新的内核线程在创建以后要通过daemonize释放父进程的资源,完成信号量,睡眠的相关工作(暂时不提)。
用户态创建线程调用clone(),如果要在内核态创建线程,首先想到的是在内核态调用clone()。这是可以的。比如在init内核线程中就直接在内核态调用execve,参数为/sbin/init等等。但是还是要小心翼翼。因为系统调用里会有很多参数要求是用户态的(一般在声明前有__user ),在调用一些内核函数时也会检查参数的界限,严格要求参数在用户态。一旦发现参数是在内核态,就立即返回出错。
所以kernel_thread采用了另外一种办法。
由于不是从用户态进入内核的,它需要制造一种现场,好像它是通过clone系统调用进入内核一样。方法是手动生成并设置一个struct pt_regs,然后调用do_fork()。但是怎样把线程的函数指针fn,参数arg传进去呢?和flags不同,flags可以作为do_fork()的参数。但是fn正常情况下应该是在clone()结束后才执行的。此外,线程总不能长生不老吧,所以执行完fn()还要执行exit()。
所以,我们希望内核线程在创建后,回到内核态(普通情况下是用户态)后,去调用fn(arg),最后调用exit()。而要去“遥控”内核线程在创建以后的事,只能通过设置pt_regs来实现了。
kernel_thread的函数源码如下:
kernel_thread原形在/arch/kernel/process.c中
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
struct pt_regs regs;
memset(®s, 0, sizeof(regs));
regs.ARM_r1 = (unsigned long)arg;
regs.ARM_r2 = (unsigned long)fn;
regs.ARM_r3 = (unsigned long)do_exit;
regs.ARM_pc = (unsigned long)kernel_thread_helper;
regs.ARM_cpsr = SVC_MODE;
/* 利用do_fork来产生一个新的线程,共享父进程地址空间,并且不允许调试子进程 */
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
}
[/arch/i386/kernel/process.c/kernel_thread_helper]
extern void kernel_thread_helper(void); /* 定义成全局变量 */
__asm__(".section .text\n"
".align 4\n"
"kernel_thread_helper:\n\t"
"movl %edx,%eax\n\t"
"pushl %edx\n\t" /* edx指向参数,压入堆栈 */
"call *%ebx\n\t" /* ebx指向函数地址,执行函数 */
"pushl %eax\n\t"
"call do_exit\n" /* 结束线程 */
".previous");
在kernel_thread中调用了do_fork,那么do_fork是怎样转入kernel_thread_helper去执行的呢,继续跟踪下do_fork函数.
[kernel/fork.c/do_fork()]
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)
{
....
....
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
....
....
}
它调用copy_process函数来向子进程拷贝父进程的进程环境和全部寄存器副本.
[kernel/fork.c/do_fork()->copy_process]
static task_t *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,
int pid)
{
...
...
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
...
...
}
它又调用copy_thread来拷贝父进程的系统堆栈并做相应的调整.
[/arch/i386/kernel/process.c/copy_thread]:
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
unsigned long unused,
struct task_struct * p, struct pt_regs * regs)
{
...
...
p->thread.eip = (unsigned long) ret_from_fork;
}
在这里把ret_from_fork的地址赋值给p->thread.eip,p->thread.eip表示当进程下一次调度时的指令开始地址,
所以当线程创建后被调度时,是从ret_from_fork地址处开始的.
[/arch/i386/kernel/entry.s]
到这里说明,新的线程已经产生了.
ENTRY(ret_from_fork)
pushl %eax
call schedule_tail
GET_THREAD_INFO(%ebp)
popl %eax
jmp syscall_exit
syscall_exit:
...
work_resched:
call schedule
...
当它从ret_from_fork退出时,会从堆栈中弹出原来保存的ip,而ip指向kernel_thread_helper,
至此kernel_thread_helper被调用,它就可以运行我们的指定的函数了.