Chinaunix首页 | 论坛 | 博客
  • 博客访问: 419701
  • 博文数量: 124
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 872
  • 用 户 组: 普通用户
  • 注册时间: 2018-03-29 14:38
个人简介

默默的一块石头

文章分类

全部博文(124)

文章存档

2022年(26)

2021年(10)

2020年(28)

2019年(60)

我的朋友

分类: LINUX

2020-02-28 11:19:03

创建内核使用的线程的函数为kthread_run()函数,而该函数实际是一个宏定义,它由kthread_create()和wake_up_process()两部分组成,调用了kthread_create()后执行了wake_up_process().这样的好处是用kthread_run()创建成功的线程可以直接运行,使用方便。

kthread_create()根据参数创建kthread_create_info变量,并把变量的链表成员list加入到kthread_create_list中链表中,并会在进程创建成功之后通过sched_setscheduler_nocheck(create.result, SCHED_NORMAL, ¶m);函数设定进程的优先级,再通过wake_up_process()函数唤醒kthreadd_task线程去执行线程的创建者kthreadd()函数创建线程,之后会调用wait_for_completion(&create.done)将创建完的线程阻塞(加入到等待队列中)。将线程阻塞之后则kthread_create()函数返回到kthread_run()函数中,其会通过wake_up_process()函数唤醒新建的线程,并投入运行,而其通过kthreadd()函数创建的线程,其入口函数就为kthread()函数,所以当创建的线程开始运行,其会调用kthread()函数而该函数会调用complete(&create->done)唤醒阻塞的模块进程,并使用schedule()调度出去,等待调度。(创建进程上下文、被创建进程上下文、current全局变量)

wait_for_completion()函数功能实现的子函数do_wait_for_common()
static inline long __sched do_wait_for_common(struct completion *x, long timeout, int state){
 if (!x->done) {
  DECLARE_WAITQUEUE(wait, current);
  __add_wait_queue_tail_exclusive(&x->wait, &wait);
  do {
   if (signal_pending_state(state, current)) {
    timeout = -ERESTARTSYS;
    break;
   }
   __set_current_state(state);
   spin_unlock_irq(&x->wait.lock);
   timeout = schedule_timeout(timeout);
   spin_lock_irq(&x->wait.lock);
  } while (!x->done && timeout);
  __remove_wait_queue(&x->wait, &wait);
  if (!x->done) return timeout;
 }
 x->done--;
 return timeout ?: 1;
}

#define __WAITQUEUE_INITIALIZER(name, tsk) {    \
 .private  = tsk,      \
 .func  = default_wake_function,    \
 .task_list  = { NULL, NULL } }
#define DECLARE_WAITQUEUE(name, tsk)     \
 wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

complete()函数能实现的子函数__wake_up_common():
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
    int nr_exclusive, int wake_flags, void *key){
 wait_queue_t *curr, *next;
 list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
  unsigned flags = curr->flags;
  if (curr->func(curr, mode, wake_flags, key) &&
    (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
   break;
 }
}
以下摘自《Linux内核设计与实现》中对完成变量的介绍:
如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量(completion variable)是使两个任务得以同步的简单方法。如果一个任务要执行一些工作时,另一个任务就会在完成变量上等待。当这个任务完成工作后,会使用完成变量去唤醒在等待的任务。
完成变量由结构completion表示,定义在中。通过以下宏静态地创建完成变量并初始化它:
DECLARE_COMPLETION(xx_comp);
通过init_completion()动态创建并初始化完成变量。
在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。当特定事件发生后,产生事件的任务调用complete()来发送信号唤醒正在等待的任务。以下列出了完成变量的方法:
1、init_completion(struct completion *) ------初始化指定的动态创建的完成变量
2、wait_for_completion(struct completion *)-----等待指定的完成变量接收信号
3、complete(struct completion *)-----发信号唤醒任何等待任务
完成变量的通用法是,讲完成变量作为数据结构中的一项动态创建,而完成数据结构初始化工作的内核代码将调用wait_for_completion()进行等待。初始化完成后,初始化函数调用complete()唤醒在等待的内核任务。

以下摘自《Linux内核设计与实现》中对等待队列的介绍:
        针对休眠,以前曾经使用过一些简单的接口。但那些接口会带来竞争条件:有可能导致在判定条件变为真厚,进程却开始了休眠,那样就会使进程无限地休眠下去。所以,在内核中进行休眠的推荐操作就相对复杂了一些:
DEFINE_WAIT(wait);

add_wait_queue(q,&wait);
while(!condition){
       prepare_to_wait(&q,&wait,TASK_INTERRUPTIBLE);
       if(signal_pending(current))
          
        schedule();  
}
finish_wait(&q,&wait);
进程通过执行下面几个步骤将自己加入到一个等待队列中:
1)调用宏DEFINE_WAIT()创建一个等待队列的项。
2)调用add_wait_queue()把自己加入到队列中。该队列会在进程等待的条件满足时唤醒它。当然我们必须在其他地方撰写相关代码,在事件发生时,对等待队列执行wake_up()操作。
3)调用prepare_to_wait()方法将进程的状态变更为TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE。而且该函数如果有必要的话会将进程加回到等待队列,这是在接下来循环遍历中所需要的。
4)如果状态被设置为TASK_INTERRUPTIBLE,则信号唤醒进程。这就是所谓的伪唤醒(唤醒不是因为事件的发生),因此检查并处理信号。
5)当进程被唤醒的时候,它会再次检查条件是否为真。如果是,它就退出循环;如果不是,它再次调用
schedule()并一直重复这步的操作。
6)当条件满足后,进程将自己设置为TASK_RUNNING并调用finish_wait()方法把自己移除等待队列。
如果在进程开始休眠之前条件就已经达成了,那么循环会退出,进程不会存在错误地进入休眠的倾向。需注重的是,内核代码在循环体内常常需要完成一些其他的任务,比如,它可能在调用schedule()之前需要释放掉锁,而在这以后再重新获取它们,或者响应其他事件。

三个关键字:等待队列、调度器、完成变量

另:锁、进程/线程、调度值得思考。锁对调度器的影响如何处理呢?
   内核中有类似可能造成并发执行的原因。它们是:
   (1)中断----中断几乎可以再任何时刻异步发生,也就可能随时打断当前正在执行的代码。
   (2)软中断和tasklet----内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码。
   (3)内核抢占----因为内核具有抢占性,所以内核中的任务可能会被另一任务抢占。
   (4)睡眠及与用户空间的同步----在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行。
   (5)对称多处理----两个或多个处理器可以同时执行代码。
   对内核开发者来说,必须理解上述这些并发执行的原因,并且为它们事先做足准备工作。如果在一段内核代码操作某资源的时候系统产生了一个中断,而且该中断的处理程序还要访问这一资源,这就是一个bug;类似地,如果一段内核代码在访问一个共享资源期间可以被抢占,这也是一个bug;还有,如果内核代码在临界区里睡眠,那简直就是鼓掌欢迎竞争条件的到来。最后还要注意,两个处理器绝对不能同时访问同一共享数据。当我们清楚睡眠样的数据需要保护时,提供锁来保护系统稳定也就不难做到了。然而,真正困难的就是发现上述的潜在并发执行的可能,并有意识地采取某些措施来防止并发执行。

以下摘自《Linux内核编程》中第七章对进程调度和内核同步的介绍:
   Linux内核是多任务内核,这意味着多个进程可以同时运行,就好像它们是系统中唯一的进程一样。操作系统在特定时刻选择哪一个进程访问系统的CPU是由调度程序来决定的。
   调度程序负责在不同的进程之间切换CPU,并决定进程访问CPU的次序。与大多数操作系统一样,Linux通过时钟中断来触发调度程序。当时钟中断发生时,内核必须决定是否为其他非当前进程让出CPU,如果要让出的话,接下来应该由哪一个进程获得CPU。相邻两个时钟中断之间的时间间隔称为时间片

原创文章,转发请注明出处。
阅读(7193) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~