博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
linux线程同步浅析
阅读量:4312 次
发布时间:2019-06-06

本文共 4814 字,大约阅读时间需要 16 分钟。

一个程序问题


之前写过这样一个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()函数。

转载于:https://www.cnblogs.com/wangfengju/archive/2013/05/11/6173092.html

你可能感兴趣的文章
Docker(一)使用阿里云容器镜像服务
查看>>
Docker(三) 构建镜像
查看>>
FFmpeg 新旧版本编码 API 的区别
查看>>
RecyclerView 源码深入解析——绘制流程、缓存机制、动画等
查看>>
Android 面试题整理总结(一)Java 基础
查看>>
Android 面试题整理总结(二)Java 集合
查看>>
学习笔记_vnpy实战培训day02
查看>>
学习笔记_vnpy实战培训day03
查看>>
VNPY- VnTrader基本使用
查看>>
VNPY - CTA策略模块策略开发
查看>>
VNPY - 事件引擎
查看>>
MongoDB基本语法和操作入门
查看>>
学习笔记_vnpy实战培训day04_作业
查看>>
OCO订单(委托)
查看>>
学习笔记_vnpy实战培训day06
查看>>
回测引擎代码分析流程图
查看>>
Excel 如何制作时间轴
查看>>
matplotlib绘图跳过时间段的处理方案
查看>>
vnpy学习_04回测评价指标的缺陷
查看>>
iOS开发中遇到的问题整理 (一)
查看>>