关于进程的上下文切换

参考这篇博客http://www.linfo.org/.html

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

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

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

另外,关于Cache Line 的测试大家可移步 http://www.cppthinker.com/cpp/9/cpu_cache/

Context Switch简介 -

上下文切换(以下简称CS)的定义,http://www.linfo.org/context_switch.html 此文中已做了详细的说明,这里我又偷懒不详细解释了:)  只提炼以下几个关键要点:

*) 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

 1 int
 2 __pthread_cond_wait (cond, mutex)
 3      pthread_cond_t *cond;
 4      pthread_mutex_t *mutex;
 5 {
 6   struct _pthread_cleanup_buffer buffer;
 7   struct _condvar_cleanup_buffer cbuffer;
 8   int err;
 9   int pshared = (cond->__data.__mutex == (void *) ~0l)
10         ? LLL_SHARED : LLL_PRIVATE;
11
12   /* yunjie: 这里省略了部分代码 */
13
14   do
15     {
16         /* yunjie: 这里省略了部分代码 */
17
18       /* Wait until woken by signal or broadcast.  */
19       lll_futex_wait (&cond->__data.__futex, futex_val, pshared);
20
21         /* yunjie: 这里省略了部分代码 */
22
23       /* If a broadcast happened, we are done.  */
24       if (cbuffer.bc_seq != cond->__data.__broadcast_seq)
25     goto bc_out;
26
27       /* Check whether we are eligible for wakeup.  */
28       val = cond->__data.__wakeup_seq;
29     }
30   while (val == seq || cond->__data.__woken_seq == val);
31
32   /* Another thread woken up.  */
33   ++cond->__data.__woken_seq;
34
35  bc_out:
36     /* yunjie: 这里省略了部分代码 */
37   return __pthread_mutex_cond_lock (mutex);
38 }

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

lll_futex_wait宏展开的全貌

 1 #define lll_futex_wake(futex, nr, private)  2   do {                                         3     int __ignore;                                  4     register __typeof (nr) _nr __asm ("edx") = (nr);                   5     __asm __volatile ("syscall"                            6               : "=a" (__ignore)                        7               : "0" (SYS_futex), "D" (futex),                  8             "S" (__lll_private_flag (FUTEX_WAKE, private)),        9             "d" (_nr)                         10               : "memory", "cc", "r10", "r11", "cx");              11   } while (0)

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

linux/kernel/futex.c

 1 SYSCALL_DEFINE6(futex, u32 __user *, uaddr, int, op, u32, val,
 2         struct timespec __user *, utime, u32 __user *, uaddr2,
 3         u32, val3)
 4 {
 5     struct timespec ts;
 6     ktime_t t, *tp = NULL;
 7     u32 val2 = 0;
 8     int cmd = op & FUTEX_CMD_MASK;
 9
10     if (utime && (cmd == FUTEX_WAIT || cmd == FUTEX_LOCK_PI ||
11               cmd == FUTEX_WAIT_BITSET)) {
12         if (copy_from_user(&ts, utime, sizeof(ts)) != 0)
13             return -EFAULT;
14         if (!timespec_valid(&ts))
15             return -EINVAL;
16
17         t = timespec_to_ktime(ts);
18         if (cmd == FUTEX_WAIT)
19             t = ktime_add_safe(ktime_get(), t);
20         tp = &t;
21     }
22     /*
23      * requeue parameter in 'utime' if cmd == FUTEX_REQUEUE.
24      * number of waiters to wake in 'utime' if cmd == FUTEX_WAKE_OP.
25      */
26     if (cmd == FUTEX_REQUEUE || cmd == FUTEX_CMP_REQUEUE ||
27         cmd == FUTEX_WAKE_OP)
28         val2 = (u32) (unsigned long) utime;
29
30     return do_futex(uaddr, op, val, tp, uaddr2, val2, val3);
31 }

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

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

futex_wait中的部分代码

 1 /* add_wait_queue is the barrier after __set_current_state. */
 2     __set_current_state(TASK_INTERRUPTIBLE);
 3     add_wait_queue(&q.waiters, &wait);
 4     /*
 5      * !plist_node_empty() is safe here without any lock.
 6      * q.lock_ptr != 0 is not safe, because of ordering against wakeup.
 7      */
 8     if (likely(!plist_node_empty(&q.list))) {
 9         if (!abs_time)
10             schedule();
11         else {
12             hrtimer_init_on_stack(&t.timer, CLOCK_MONOTONIC,
13                         HRTIMER_MODE_ABS);
14             hrtimer_init_sleeper(&t, current);
15             t.timer.expires = *abs_time;
16
17             hrtimer_start(&t.timer, t.timer.expires,
18                         HRTIMER_MODE_ABS);
19             if (!hrtimer_active(&t.timer))
20                 t.task = NULL;
21
22             /*
23              * the timer could have already expired, in which
24              * case current would be flagged for rescheduling.
25              * Don't bother calling schedule.
26              */
27             if (likely(t.task))
28                 schedule();
29
30             hrtimer_cancel(&t.timer);
31
32             /* Flag if a timeout occured */
33             rem = (t.task == NULL);
34
35             destroy_hrtimer_on_stack(&t.timer);
36         }
37     }

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

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

大致流程:pthread_cond_signal => SYS_futex => do_futex => futex_wake => wake_futex => __wake_up => __wake_up_common => 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所带来的性能下降

希望对大家有帮助 :)

时间: 2024-10-25 04:19:33

关于进程的上下文切换的相关文章

计算机操作系统 --- 进程和进程的上下文切换

问题 一个进程长什么样子的 操作系统缺页中断后是如何将磁盘的数据加载到内存中的,过程是怎么样的 程序和进程      程序是静态的概念,而process进程更像是运行任务,最后面一个例子可以好好理解,一个程序可以被多个进程加载. 进程的概念 进程与上下文切换     可以看到两个进程(shell 和 hello 程序)发生切换的时候会从内核态到用户态.     上面这张图非常重要,好好理解!     上面就是我们所说的上下文分两部分,和系统相关的进程信息为系统上下文,而和用户相关的则是用户级上下

进程学习笔记

1.什么是程序? 程序:程序(Program)是一个静态的命令集合,程序可以作为目标存储在磁盘中.在它不执行的时候,它只占用存储,仅仅是一些代码和数据. 2.什么是进程? 进程:进程(Process)是执行中程序的一个具体实例,是操作系统对一个正在运行的程序的一种抽象. 从程序员角度来讲,可以认为进程总是处于下面三种状态之一:        运行:进程要么在CPU上执行,要么在等待被执行且最终会被执行.        停止:进程的执行被挂起(suspend),且不会被调度.当收到SIGTOP.S

上下文切换详解

上下文切换详解 原文地址,译文地址,译者: 董明鑫,校对:郑旭东 上下文切换(有时也称做进程切换或任务切换)是指CPU 从一个进程或线程切换到另一个进程或线程.进程(有时候也称做任务)是指一个程序运行的实例.在 Linux 系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程.上下文是指某一时间点 CPU 寄存器和程序计数器的内容.寄存器是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主

进程分析之CPU

进程分析之CPU 本文转载自:https://github.com/ColZer/DigAndBuried/blob/master/system/cpu.md 在<进程分析之内存>文中,对系统/进程的内存使用情况进行分析了,本文将从cpu使用情况对进程进行分析:在这之前,先针对cpu比较相关几个概念进行介绍 CPU INFO的阅读以及对基本概念的了解: cpu从硬件到系统层面有三个概念:物理CPU个数.物理核数.逻辑核个数:其中物理CPU的个数即硬件层面实实在在的CPU的个数:现在CPU都为多

通过gdb跟踪进程调度分析进程切换的过程

作者:吴乐 山东师范大学 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 本实验目的:通过gdb在linux下对一个简单的命令行命令实现进程的过程进行跟踪,分析一般用户进程实现进程切换的过程,并进一步剖析进程调度的工作的原理. 一.实验过程 1.打开实验环境,并设置context_switch和pick_next_switch两个断点. 2.来到第二个断点处list(分析在第三部分) 3.到第一个断点处,在这里

Linux进程核心调度器之主调度器--Linux进程的管理与调度(十九)

日期 内核版本 架构 作者 GitHub CSDN 2016-06-30 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度 我们前面提到linux有两种方法激活调度器:核心调度器和 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 因而内核提供了两个调度器主调度器,周期性调度器,分别实现如上工作, 两者合在一起就组成了核心调度器(core sched

上下文切换

上下文切换(有时也称做进程切换或任务切换)是指CPU 从一个进程或线程切换到另一个进程或线程.进程(有时候也称做任务)是指一个程序运行的实例.在 Linux 系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程---(自认为描述非常好).上下文是指某一时间点 CPU 寄存器和程序计数器的内容.寄存器是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存).寄存器通过对常用值(通常是运算的

【转】Context Switches上下文切换性能详解

http://blog.csdn.net/aiai5251/article/details/50015745 Context Switches 上下文切换,有时也被称为进程切换(process switch)或任务切换.是一个重要的性能指标. CPU从一个线程切换到另外一个线程,需要保存当前任务的运行环境,恢复将要运行任务的运行环境,必然带来性能消耗. Context Switches 上下文切换简介 操作系统可以同时运行多个进程, 然而一颗CPU同时只能执行一项任务,操作系统利用时间片轮转的方

Linux进程核心调度器之主调度器schedule--Linux进程的管理与调度(十九)【转】

转自:http://blog.csdn.net/gatieme/article/details/51872594 日期 内核版本 架构 作者 GitHub CSDN 2016-06-30 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度 我们前面提到linux有两种方法激活调度器:核心调度器和 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 因而内