zoukankan      html  css  js  c++  java
  • linux线程同步浅析

    一个程序问题

    之前写过这样一个C程序:模块维护一个工作线程、提供一组调用接口(分同步调用和异步调用)。用户调用模块提供的接口后,会向工作队列添加一个任务。然后任务由工作线程来处理。
    在同步调用情况下,接口调用后调用者被阻塞,等待工作线程处理完成后,将调用者唤醒。
    伪代码如下:

    [调用接口]
    add_command(cmd, pid);          /* 1 */
    raise(SIGSTOP);                 /* 2 */
    get_response(cmd);              /* 6 */

    [工作线程]
    wait_for_command(&cmd, &pid);   /* 3 */
    do_command(cmd);                /* 4 */
    kill(pid, SIGCONT);             /* 5 */

    调用接口向工作队列添加命令以后,向自己发送一个SIGSTOP信号,把自己挂起;工作线程处理命令完成,通过向调用者进程发送SIGCONT信号,将调用者唤醒。

    流程上还是比较清晰的,但是有点想当然了。测试发现,程序的执行流程可能变成下面的情况:

    [调用接口]
    add_command(cmd, pid);          /* 1 */
    raise(SIGSTOP);                 /* 5 ... */
    get_response(cmd);              

    [工作线程]
    wait_for_command(&cmd, &pid);   /* 2 */
    do_command(cmd);                /* 3 */
    kill(pid, SIGCONT);             /* 4 */

    调用者在添加命令后,发生调度,工作线程在调用者进入睡眠之前,先处理了命令并发出唤醒信号。之后,调用者再睡眠,就没办法被唤醒了。

    解决方法

    直接使用信号来实现睡眠和唤醒看来是不可取的,于是想到了使用pthread的互斥机制。改写后的程序如下:

    [调用接口]
    add_command(cmd);               /* 1 */
    pthread_cond_wait(cond);        /* 2 */
    get_response(cmd);              /* 6 */

    [工作线程]
    wait_for_command(&cmd, &pid);   /* 3 */
    do_command(cmd);                /* 4 */
    pthread_cond_signal(cond);      /* 5 */

    测试发现,这样做就不会出现由于调度而出现“先唤醒、后睡眠”的问题了。

    但是,pthread条件变量是如何避免“先唤醒、后睡眠”的呢?
    实际上,它依然无法避免调用者在添加命令后,由于调度,造成pthread_cond_signal先于pthread_cond_wait发生的问题。但是条件变量内部记录了信号是否已发生,如果pthread_cond_signal先于pthread_cond_wait,则pthread_cond_wait将看到条件变量中记录的“信号已发生”,于是放弃睡眠。
    man一下pthread_cond_signal可以看到如下流程:

    [pthread_cond_wait(mutex, cond)]
    value = cond->value;                     /* 1 */
    pthread_mutex_unlock(mutex);             /* 2 */
    pthread_mutex_lock(cond->mutex);         /* 10 */
    if (value == cond->value) {              /* 11 */
        me->next_cond = cond->waiter;
        cond->waiter = me;
        pthread_mutex_unlock(cond->mutex);   /* X */
        unable_to_run(me);                   /* Y */
    } else
        pthread_mutex_unlock(cond->mutex);   /* 12 */
    pthread_mutex_lock(mutex);               /* 13 */

    [pthread_cond_signal(cond)]
    pthread_mutex_lock(cond->mutex);         /* 3 */
    cond->value++;                           /* 4 */
    if (cond->waiter) {                      /* 5 */
        sleeper = cond->waiter;              /* 6 */
        cond->waiter = sleeper->next_cond;   /* 7 */
        able_to_run(sleeper);                /* 8 */
    }
    pthread_mutex_unlock(cond->mutex);       /* 9 */

    这份伪代码中的cond->value就是用于记录“信号已发生”的变量。

    深入一点

    如果你足够细心,可能已经发现上面的pthread的伪代码是有问题的。在‘X’处,cond->value已经判断过了,cond->mutex也已经释放了,而unable_to_run(将进程挂起)还没运行。那么此时如果发生调度,pthread_cond_signal先运行了呢?是不是able_to_run(唤醒)又将发生在unable_to_run之前,而导致“先唤醒、后睡眠”呢?
    这就变成了下面的流程:

    [pthread_cond_wait(mutex, cond)]
    value = cond->value;                    /* 1 */
    pthread_mutex_unlock(mutex);            /* 2 */
    pthread_mutex_lock(cond->mutex);        /* 3 */
    if (value == cond->value) {             /* 4 */
        me->next_cond = cond->waiter;
        cond->waiter = me;
        pthread_mutex_unlock(cond->mutex);  /* 5 */
        unable_to_run(me);                  /* 13 ... */
    } else
        pthread_mutex_unlock(cond->mutex);
    pthread_mutex_lock(mutex);

    [pthread_cond_signal(cond)]
    pthread_mutex_lock(cond->mutex);        /* 6 (注意:5已经释放锁了) */
    cond->value++;                          /* 7 */
    if (cond->waiter) {                     /* 8 */
        sleeper = cond->waiter;             /* 9 */
        cond->waiter = sleeper->next_cond;  /* 10 */
        able_to_run(sleeper);               /* 11 */
    }
    pthread_mutex_unlock(cond->mutex);      /* 12 */

    这个问题实际上和文章最开始的代码一样,在“睡眠前的准备”和“进入睡眠”之间可能发生调度,从而存在“先唤醒、后睡眠”的可能性。

    真的会有问题吗?其实不会,否则pthread提供这么一个不能做到同步的同步接口,实在没什么意义。
    其实able_to_run和unable_to_run的实现还是有讲究的,简单的睡眠和唤醒显然不能满足需要。

    同步的实现

    当时写程序的时候是在嵌入式linux下,uClibc库使用的pthread线程库是linuxthreads(现在主流的线程库是NPTL)。
    在linuxthreads中,上面提到的unable_to_run是基于sigsuspend系统调用来实现的。

    在linux中,每个进程(线程)都有一个信号掩码,如果某个信号被mask掉,那么收到的这个信号就不会被处理,而是作为一个未决信号,记录在进程的控制信息(task_struct结构)中。默认情况下,linuxthreads把SIGUSER1给mask掉了。
    sigsuspend的功能就是使用新的mask,并等待一个信号。收到不被mask的信号后,sigsuspend返回,并且信号掩码被还原。

    这样一来,如果出现“先唤醒、后睡眠”(able_to_run先于unable_to_run被执行),则:
    1、able_to_run:SIGUSER1信号被发送到目标进程上,而目标进程的SIGUSER1信号被mask掉了,于是该信号被记录在目标进程的task_struct结构中,并不被立刻处理
    2、unable_to_run:调用sigsuspend,新的mask不包含SIGUSER1信号,于是记录在task_struct结构中的SIGUSER1信号被取出,sigsuspend直接返回,并不会进入睡眠

    可见,sigsuspend之所以能够实现同步,就是因为它避免了“睡眠前的准备”和“进入睡眠”之间可能发生的调度(“睡眠前的准备”中的最后一步----取消mask,和“进入睡眠”,都是在这个调用中完成的),把这两个操作统一成了一个“原子操作”(对于用户态程序来说是原子的)。

    再深入一点

    那么,由内核实现的系统调用sigsuspend,它本身也是一个函数呀,它还是得面对“在‘睡眠前的准备’和‘进入睡眠’之间可能发生调度”的问题呀!
    其实不然,因为调度其本身是由内核来实现的,内核大不了就在一小段时间内不调度。

    但是,上面只提到由于调度引起的“先唤醒、后睡眠”问题。然而在多处理器条件下,即将睡眠的进程和唤醒进程可能运行在不同的CPU上,即便不发生调度还是可能出现“先唤醒、后睡眠”的问题。

    为了解决这个问题,内核还必须用到锁。内核通过锁来保证“睡眠前的准备”和“进入睡眠”是“原子的”。
    然而,锁总是要释放的,释放锁是不是应该放在睡眠以前?是不是该归为“睡眠前的准备”?于是乎,是不是又存在“在‘睡眠前的准备’和‘进入睡眠’之间被插入唤醒操作”的问题呢?

    没错,如果锁一定要在睡眠以前释放,那么肯定还是存在这样的问题。
    但是内核不一定要在进程睡眠以前释放锁,内核可以让这个进程带着锁去睡眠。然后,当上下文切换到另一个进程之后(注意,这时还是在内核态),内核还可以为上一个进程执行一些代码,做一些切换后的清理工作。锁的释放实际上可以放在这里来做。

    具体到linux内核代码,我们来看看用于唤醒的try_to_wake_up函数和用于睡眠的schedule函数(实际上该函数用于触发一次调度,在调度前如果发现当前进程状态不是RUNNING,则将其移出可执行队列,于是当前进程就睡眠了)。

    [try_to_wake_up]
    1、锁住被唤醒进程对应的可执行队列
    2、将被唤醒进程加入该队列
    3、将被唤醒进程状态设为RUNNING
    4、释放锁

    [schedule]
    1、锁住当前进程对应的可执行队列
    2、如果进程状态不为RUNNING,则将其移出队列
    3、进行进程切换
    4、释放锁

    调用schedule函数之前,当前进程已经被设置为非RUNNING状态,很容易通过锁机制保证这个动作发生在try_to_wake_up函数被调用之前。
    那么,可以看到,即使是“先唤醒、后睡眠”,睡眠的进程也能被唤醒。因为“唤醒”动作将进程状态设为RUNNING了,而“睡眠”动作发现进程状态是RUNNING,则并不会真正睡眠(不会将进程移出可执行队列)。
    可执行队列锁保证了“唤醒”和“睡眠”两个动作是原子的,不会交叉执行。而在“睡眠”过程中,是在完成了进程切换后才释放锁。这个动作可参阅sched.c:context_switch()函数最后部分调用的finish_task_switch()函数。
  • 相关阅读:
    uva 10369 Arctic Network
    uvalive 5834 Genghis Khan The Conqueror
    uvalive 4848 Tour Belt
    uvalive 4960 Sensor Network
    codeforces 798c Mike And Gcd Problem
    codeforces 796c Bank Hacking
    codeforces 768c Jon Snow And His Favourite Number
    hdu 1114 Piggy-Bank
    poj 1276 Cash Machine
    bzoj 2423 最长公共子序列
  • 原文地址:https://www.cnblogs.com/hehehaha/p/6332803.html
Copyright © 2011-2022 走看看