linux学习中
分类: LINUX
2010-07-23 17:41:03
本帖最后由 MagicBoy2010 于 2010-07-19 21:33 编辑 原帖见: 先简单快速总结一下,更详细的剖析后续用帖子编辑方式逐步完成。 分成两大部分,第一部分是用来执行work queue中每个节点上挂载的函数的内核线程,第二部分是从驱动程序的角度看work queue的使用。 第一部分 worker_thread内核线程 Linux系统启动期间会创建一名为worker_thread线程,该线程创建之后就处于sleep状态。这里所谓的内核线程,从调度器的角度就是一可以调度的进程,从代码的表现形式看,就是一函数。系统创建的这个worker_thread线程基于一workqueue_struct结构变量上(该结构体变量的成员name为"events"). 第二部分 work queue的使用 1.只考虑使用系统的keventd管理的工作队列 驱动程序调用schedule_work向工作队列递交新的工作节点,schedule_work内部会唤醒worker_thread内核线程(使之进程状态为可调度)。在下一次进程调度时刻,worker_thread被调度执行,其主要任务便是调用它所管理工作队列中每个工作节点上挂载的函数,调用完毕该工作节点会从任务队列中被删除。当所有节点上的函数调用完毕,worker_thread继续sleep,直到schedule_work再次被某个驱动程序调用。 与使用驱动程序自己创建的工作对列的区别是:schedule_work内部是调用queue_work(keventd_wq, work),而使用驱动程序自己创建的工作队列在调用queue_work时的第一个参数是驱动程序自己创建的工作队列。 2.驱动程序使用自己创建的工作队列 这种情况驱动程序调用create_workqueue。该函数的原理跟1中基本是一样的,只不过再会创建一个内核进程,该内核进程的内核函数名字依然为worker_thread,只不过这个worker_thread工作在新的属于驱动程序自己的工作队列。 使用方法是: a. 调用create_workqueue生成属于驱动程序自己的work queue. struct workqueue_struct *my_workqueue = create_workqueue("my_workqueue"); b.调用queue_work象a中生成的my_workqueue中注册工作节点, queue_work(my_workqueue, work) 这两种情况下的内核线程其实都是利用了一个名为kthreadd的内核线程来创建工作队列的worker_thread,其本质是向全局列表kthread_create_list中加入相应的内核线程函数节点,由kthreadd来负责创建进程框架,然后运行kthread_create_list中加入的节点上的内核线程函数。 向kthread_create_list中每加入一个内核线程函数节点,都会唤醒kthreadd线程。 当等待队列是空时,worker_thread会睡眠在该等待队列中。当driver调用queue_work时,再向等待队列中加入工作节点的同时,会唤醒睡眠在该等待队列上的worker_thread线程。 这楼的帖子用来扩展1楼帖子中提到的第一部分>> ------------------------------------------------------------------------- 关于worker_thread内核线程的创立过程: 最开始由void __init init_workqueues(void)发起(Workqueue.c),依次的关键调用节点分别是(为了便于叙述,在内核代码基础上略有改动,但不影响核心调用过程) create_workqueue("events"); __create_workqueue((("events"), 0, 0); __create_workqueue_key("events", 0, 0, NULL, NULL); 在__create_workqueue_key("events", 0, 0, NULL, NULL)中: 1).首先创建一个struct workqueue_struct指针型变量*wq, wq->name = "events".这个新建的队列指针将被记录在全局变量keventd_wq中. workqueue_struct定义如下: 2).把wq所在的队列节点加入到一名为workqueues的全局变量中(list_add(&wq->list, &workqueues)). 3).调用create_workqueue_thread(),最终调用kthread_create(worker_thread, cwq, fmt, wq->name, cpu)来生成内核线程worker_thread. Linux内核中最终是通过do_fork来生成内核线程(正如前面所说,这其实是个能被调度的进程,拥有自己的task_struct结构),这个过程在内核中是个比较复杂的过程,比较重要的节点总结如下:在当前进程下调用do_fork来生成一个新进程时,会大量copy当前进程的task_struct结构到新进程的task_struct变量中,新进程如果被调度运行,入口点(pc值)是kernel_thread_helper函数,在该函数中会再次将pc值设置为kernel_thread()中的function指针,也就是在调用kernel_thread函数时第一参数所表示的函数。 所以,在Linux系统初始化期间,会生成一个新进程,该进程的执行线程/函数为worker_thread,该进程被创建出来之后的状态是STOP,这意味着该线程无法进入调度队列直到针对该进程调用wake_up_process(),该进程才会真正进入调度队列,如果被调度,则开始运行worker_thread函数。新进程被赋予的调度优先级为KTHREAD_NICE_LEVEL(-5),这个标志的实际含义将在Linux进程调度的帖子里去写。然后对于worker_thread线程函数本身,会进一步调整调度优先级(set_user_nice(current, -5)),这样worker_thread所在进程的优先值将为-10,在可抢占式的Linux内核中,如此高的调度优先级极易导致一个调度时点,即使当前进程也许正运行在内核态,也可能被切换出CPU,代之以worker_thread. 该过程的理解也可以参考海豚的另一个帖子: 在kernel_thread_helper函数中,在调用内核线程函数之前设定返回地址为do_exit()函数,所以当内核线程函数退出的话将导致该进程的消失。 比如ARM中的kernel_thread_helper函数代码: extern void kernel_thread_helper(void); mov lr, r3设定内核线程函数返回后的返回地址,Linux内核代码设定为do_exit(). 可以想象,象worker_thread这种内核线程函数,一般不会轻易退出,除非对内核线程函数所在的进程上调用kthread_stop函数。 上述的kernel_thread_helper函数中的mov pc, r2将会导致worker_thread()函数被调用,该函数定义如下: 正如前面猜测的那样,该函数不会轻易退出,其核心是一for循环,如果任务队列中有新的节点,则执行该节点上的函数(在run_workqueue()内部),否则worker_thread所在的进程将会继续休眠。 |