天天看点

进程上下文切换 – 残酷的性能杀手(上)

对于服务器的优化,很多人都有自己的经验和见解,但就我观察,有两点常常会被人忽视 – 上下文切换 和 Cache Line同步 问题,人们往往都会习惯性地把视线集中在尽力减少内存拷贝,减少IO次数这样的问题上,不可否认它们一样重要,但一个高性能服务器需要更细致地去考察这些问题,这个问题我将分成两篇文章来写:

1)从一些我们常用的用户空间函数,到linux内核代码的跟踪,来看一个上下文切换是如何产生的

2)从实际数据来看它对我们程序的影响

Context Switch简介 -

*) context(这里我觉得叫process context更合适)是指CPU寄存器和程序计数器在任何时间点的内容

*)CS可以描述为kernel执行下面的操作

1. 挂起一个进程,并储存该进程当时在内存中所反映出的状态

2. 从内存中恢复下一个要执行的进程,恢复该进程原来的状态到寄存器,返回到其上次暂停的执行代码然后继续执行

*)CS只能发生在内核态(kernel mode)

*)system call会陷入内核态,是user mode => kernel mode的过程,我们称之为mode switch,但不表明会发生CS(其实mode switch同样也会做很多和CS一样的流程,例如通过寄存器传递user mode 和 kernel mode之间的一些参数)

*)一个硬件中断的产生,也可能导致kernel收到signal后进行CS

什么样的操作可能会引起CS -

首先我们一定是希望减少CS,那什么样的操作会发生CS呢?也许看了上面的介绍你还云里雾里?

首先,linux中一个进程的时间片到期,或是有更高优先级的进程抢占时,是会发生CS的,但这些都是我们应用开发者不可控的。那么我们不妨更多地从应用开发者(user space)的角度来看这个问题,我们的进程可以主动地向内核申请进行CS,而用户空间通常有两种手段能达到这一“目的”:

1)休眠当前进程/线程

2)唤醒其他进程/线程

pthread库中的pthread_cond_wait 和 pthread_cond_signal就是很好的例子(虽然是针对线程,但linux内核并不区分进程和线程,线程只是共享了address space和其他资源罢了),pthread_cond_wait负责将当前线程挂起并进入休眠,直到条件成立的那一刻,而pthread_cond_signal则是唤醒守候条件的线程。我们直接来看它们的代码吧

pthread_cond_wait.c

<code>int</code>

<code>__pthread_cond_wait (cond, mutex)</code>

<code>     </code><code>pthread_cond_t *cond;</code>

<code>     </code><code>pthread_mutex_t *mutex;</code>

<code>{</code>

<code>  </code><code>struct</code> <code>_pthread_cleanup_buffer buffer;</code>

<code>  </code><code>struct</code> <code>_condvar_cleanup_buffer cbuffer;</code>

<code>  </code><code>int</code> <code>err;</code>

<code>  </code><code>int</code> <code>pshared = (cond-&gt;__data.__mutex == (</code><code>void</code> <code>*) ~0l)</code>

<code>        </code><code>? LLL_SHARED : LLL_PRIVATE;</code>

<code>  </code><code>/* yunjie: 这里省略了部分代码 */</code>

<code>  </code><code>do</code>

<code>    </code><code>{</code>

<code>        </code><code>/* yunjie: 这里省略了部分代码 */</code>

<code>      </code><code>/* Wait until woken by signal or broadcast.  */</code>

<code>      </code><code>lll_futex_wait (&amp;cond-&gt;__data.__futex, futex_val, pshared);</code>

<code>      </code><code>/* If a broadcast happened, we are done.  */</code>

<code>      </code><code>if</code> <code>(cbuffer.bc_seq != cond-&gt;__data.__broadcast_seq)</code>

<code>    </code><code>goto</code> <code>bc_out;</code>

<code>      </code><code>/* Check whether we are eligible for wakeup.  */</code>

<code>      </code><code>val = cond-&gt;__data.__wakeup_seq;</code>

<code>    </code><code>}  </code>

<code>  </code><code>while</code> <code>(val == seq || cond-&gt;__data.__woken_seq == val);</code>

<code>  </code><code>/* Another thread woken up.  */</code> 

<code>  </code><code>++cond-&gt;__data.__woken_seq;</code>

<code> </code><code>bc_out:</code>

<code>    </code><code>/* yunjie: 这里省略了部分代码 */</code>

<code>  </code><code>return</code> <code>__pthread_mutex_cond_lock (mutex);</code>

<code>}</code>

代码已经经过精简,但我们仍然直接把目光放到19行,lll_futex_wait,这是一个pthread内部宏,用处是调用系统调用sys_futex(futex是一种user mode和kernel mode混合mutex,这里不展开讲了),这个操作会将当前线程挂起休眠(马上我们将会到内核中一探究竟)

lll_futex_wait宏展开的全貌

<code>#define lll_futex_wake(futex, nr, private) \                                                                                                                                                                                                </code>

<code>  </code><code>do</code> <code>{                                        \</code>

<code>    </code><code>int</code> <code>__ignore;                                 \</code>

<code>    </code><code>register</code> <code>__typeof (nr) _nr __asm (</code><code>"edx"</code><code>) = (nr);                  \</code>

<code>    </code><code>__asm __volatile (</code><code>"syscall"</code>                           <code>\</code>

<code>              </code><code>:</code><code>"=a"</code> <code>(__ignore)                       \</code>

<code>              </code><code>:</code><code>"0"</code> <code>(SYS_futex),</code><code>"D"</code> <code>(futex),                 \</code>

<code>            </code><code>"S"</code> <code>(__lll_private_flag (FUTEX_WAKE,</code><code>private</code><code>)),       \</code>

<code>            </code><code>"d"</code> <code>(_nr)                         \</code>

<code>              </code><code>:</code><code>"memory"</code><code>,</code><code>"cc"</code><code>,</code><code>"r10"</code><code>,</code><code>"r11"</code><code>,</code><code>"cx"</code><code>);              \</code>

<code>  </code><code>}</code><code>while</code> <code>(0)</code>

可以看到,该宏的行为很简单,就是通过内嵌汇编的方式,快速调用syscall:SYS_futex,所以我们也不用再多费口舌,直接看kernel的实现吧

linux/kernel/futex.c

<code>SYSCALL_DEFINE6(futex, u32 __user *, uaddr,</code><code>int</code><code>, op, u32, val,</code>

<code>        </code><code>struct</code> <code>timespec __user *, utime, u32 __user *, uaddr2,</code>

<code>        </code><code>u32, val3)</code>

<code>    </code><code>struct</code> <code>timespec ts;</code>

<code>    </code><code>ktime_t t, *tp = NULL;</code>

<code>    </code><code>u32 val2 = 0;</code>

<code>    </code><code>int</code> <code>cmd = op &amp; FUTEX_CMD_MASK;</code>

<code>    </code><code>if</code> <code>(utime &amp;&amp; (cmd == FUTEX_WAIT || cmd == FUTEX_LOCK_PI ||</code>

<code>              </code><code>cmd == FUTEX_WAIT_BITSET)) {</code>

<code>        </code><code>if</code> <code>(copy_from_user(&amp;ts, utime,</code><code>sizeof</code><code>(ts)) != 0)</code>

<code>            </code><code>return</code> <code>-EFAULT;</code>

<code>        </code><code>if</code> <code>(!timespec_valid(&amp;ts))</code>

<code>            </code><code>return</code> <code>-EINVAL;</code>

<code>        </code><code>t = timespec_to_ktime(ts);</code>

<code>        </code><code>if</code> <code>(cmd == FUTEX_WAIT)</code>

<code>            </code><code>t = ktime_add_safe(ktime_get(), t);</code>

<code>        </code><code>tp = &amp;t;</code>

<code>    </code><code>}</code>

<code>    </code><code>/*</code>

<code>     </code><code>* requeue parameter in 'utime' if cmd == FUTEX_REQUEUE.</code>

<code>     </code><code>* number of waiters to wake in 'utime' if cmd == FUTEX_WAKE_OP.</code>

<code>     </code><code>*/</code>

<code>    </code><code>if</code> <code>(cmd == FUTEX_REQUEUE || cmd == FUTEX_CMP_REQUEUE ||</code>

<code>        </code><code>cmd == FUTEX_WAKE_OP)</code>

<code>        </code><code>val2 = (u32) (unsigned</code><code>long</code><code>) utime;</code>

<code>    </code><code>return</code> <code>do_futex(uaddr, op, val, tp, uaddr2, val2, val3);</code>

linux 2.5内核以后都使用这种SYSCALL_DEFINE的方式来实现内核对应的syscall(我这里阅读的是inux-2.6.27.62内核), 略过一些条件检测和参数拷贝的代码,我们可以看到在函数最后调用了do_futex,由于这里内核会进行多个函数地跳转,我这里就不一一贴代码污染大家了

大致流程: pthread_cond_wait =&gt; sys_futex =&gt; do_futex =&gt; futex_wait (蓝色部分为内核调用流程)

futex_wait中的部分代码

<code>/* add_wait_queue is the barrier after __set_current_state. */</code>

<code>__set_current_state(TASK_INTERRUPTIBLE);</code>

<code>add_wait_queue(&amp;q.waiters, &amp;wait);</code>

<code>/*  </code>

<code> </code><code>* !plist_node_empty() is safe here without any lock.</code>

<code> </code><code>* q.lock_ptr != 0 is not safe, because of ordering against wakeup.</code>

<code> </code><code>*/</code>

<code>if</code> <code>(likely(!plist_node_empty(&amp;q.list))) {</code>

<code>    </code><code>if</code> <code>(!abs_time)</code>

<code>        </code><code>schedule();</code>

<code>    </code><code>else</code> <code>{</code>

<code>        </code><code>hrtimer_init_on_stack(&amp;t.timer, CLOCK_MONOTONIC,</code>

<code>                    </code><code>HRTIMER_MODE_ABS);</code>

<code>        </code><code>hrtimer_init_sleeper(&amp;t, current);</code>

<code>        </code><code>t.timer.expires = *abs_time;</code>

<code>        </code><code>hrtimer_start(&amp;t.timer, t.timer.expires,</code>

<code>        </code><code>if</code> <code>(!hrtimer_active(&amp;t.timer))</code>

<code>            </code><code>t.task = NULL;</code>

<code>        </code><code>/*  </code>

<code>         </code><code>* the timer could have already expired, in which</code>

<code>         </code><code>* case current would be flagged for rescheduling.</code>

<code>         </code><code>* Don't bother calling schedule.</code>

<code>         </code><code>*/</code>

<code>        </code><code>if</code> <code>(likely(t.task))</code>

<code>            </code><code>schedule();</code>

<code>        </code><code>hrtimer_cancel(&amp;t.timer);</code>

<code>        </code><code>/* Flag if a timeout occured */</code>

<code>        </code><code>rem = (t.task == NULL);</code>

<code>        </code><code>destroy_hrtimer_on_stack(&amp;t.timer);</code>

<code>    </code><code>}   </code>

以上是futex_wait的一部分代码,主要逻辑是将当前进程/线程的状态设为TASK_INTERRUPTIBLE(可被信号打断),然后将当前进程/线程加入到内核的wait队列(等待某种条件发生而暂时不会进行抢占的进程序列),之后会调用schedule,这是内核用于调度进程的函数,在其内部还会调用context_switch,在这里就不展开,但有一点可以肯定就是当前进程/线程会休眠,然后内核会调度器他还有时间片的进程/线程来抢占CPU,这样pthread_cond_wait就完成了一次CS

pthread_cond_signal的流程基本和pthread_cond_wait一致,这里都不再贴代码耽误时间

大致流程:pthread_cond_signal =&gt; SYS_futex =&gt; do_futex =&gt; futex_wake =&gt; wake_futex =&gt; __wake_up =&gt; __wake_up_common =&gt; try_to_wake_up (蓝色部分为内核调用流程)

try_to_wake_up()会设置一个need_resched标志,该标志标明内核是否需要重新执行一次调度,当syscall返回到user space或是中断返回时,内核会检查它,如果已被设置,内核会在继续执行之前调用调度程序,之后我们万能的schedule函数就会在wait_queue(还记得吗,我们调用pthread_cond_wait的线程还在里面呢)中去拿出进程并挑选一个让其抢占CPU,所以,根据我们跟踪的内核代码,pthread_cond_signal也会发生一次CS

本篇结束 -

会造成CS的函数远远不止这些,例如我们平时遇到mutex竞争,或是我们调用sleep时,都会发生,我们总是忽略了它的存在,但它却默默地扼杀着我们的程序性能(相信我,它比你想象中要更严重),在下一篇中我将以chaos库(我编写的一个开源网络库)中的一个多线程组件为例,给大家演示CS所带来的性能下降

希望对大家有帮助 :)

我的个人博客地址:www.cppthinker.com

继续阅读