如何更精确测量上下文切换开销?基于OSTEP思路的实现代码及相关技术疑问
测量上下文切换开销:代码解析与疑问解答
你基于《OSTEP》思路实现的上下文切换开销测量代码很典型,先把你的代码贴出来方便参考:
#define _GNU_SOURCE #define _POSIX_C_SOURCE 199309L #include <stdio.h> #include <stdlib.h> #include <sched.h> #include <unistd.h> #include <time.h> #define TIMES 1000 #define BILLION 10e9 int main(int argc, char *argv[]) { int pipefd_1[2], pipefd_2[2]; struct timespec start, stop; clockid_t clk_id = CLOCK_REALTIME; // for child and parent process run on the same cpu cpu_set_t set; int parentCPU, childCPU; char testChar = 'a'; /* Use for test */ if (argc != 3) { fprintf(stderr, "Usage: %s parent-cpu child-cpu\n", argv[0]); exit(EXIT_FAILURE); } parentCPU = atoi(argv[1]); childCPU = atoi(argv[2]); CPU_ZERO(&set); if (pipe(pipefd_1) == -1) { perror("pipe"); exit(EXIT_FAILURE); } if (pipe(pipefd_2) == -1) { perror("pipe"); exit(EXIT_FAILURE); } switch (fork()) { case -1: /* error */ perror("fork"); exit(EXIT_FAILURE); case 0: /* child process */ CPU_SET(childCPU, &set); if (sched_setaffinity(getpid(), sizeof(set), &set) == -1) { perror("set cpu for child process"); exit(EXIT_FAILURE); } char readChar_c; close(pipefd_1[0]); /* Close unused read end */ close(pipefd_2[1]); /* Close unused write end */ for (int i = 0; i < TIMES; ++i) { while (read(pipefd_2[0], &readChar_c, 1) <= 0) {} /* read to the first pipe */ write(pipefd_1[1], &readChar_c, 1); /* write to the first pipe */ } close(pipefd_2[0]); close(pipefd_1[1]); exit(EXIT_SUCCESS); default: /* parent process */ CPU_SET(parentCPU, &set); if (sched_setaffinity(getpid(), sizeof(set), &set) == -1) { perror("set cpu for parent process"); exit(EXIT_FAILURE); } char readChar_p; close(pipefd_2[0]); /* Close unused read end */ close(pipefd_1[1]); /* Close unused write end */ clock_gettime(clk_id, &start); for (int i = 0; i < TIMES; ++i) { write(pipefd_2[1], &testChar, 1); /* write to the second pipe */ while (read(pipefd_1[0], &readChar_p, 1) <= 0) {} /* read to the first pipe */ } clock_gettime(clk_id, &stop); printf("the average cost of context switching is: %lf nsec\n", ((stop.tv_sec - start.tv_sec) * BILLION + stop.tv_nsec - start.tv_nsec) / TIMES); } exit(EXIT_SUCCESS); }
接下来针对你的两个疑问逐一解答:
疑问1:空读写操作的行为与上下文切换触发
首先,read(pipefd, NULL, 0) 和 write(pipefd, NULL, 0) 属于零长度I/O调用,它们不会实际传输数据,仅做管道状态检查:
- 如果尝试从空管道读取零字节,
read会直接返回0,不会阻塞,自然也不会触发上下文切换——因为内核不需要等待数据,直接完成调用。 - 但像你代码里那样读取非零字节时,若管道为空且写端未关闭,
read会阻塞当前进程。此时内核会把该进程移入等待队列,调度其他可运行进程,这才会触发上下文切换。
那些用空读写的代码,其实是利用管道的同步特性,但空读写本身不会引发阻塞,所以并不能直接触发上下文切换,更多是简化的同步逻辑。
疑问2:测量精度的问题
你观察得非常准确:当前计算方式确实包含了进程被唤醒后执行用户态指令的时间(比如write调用的收尾、循环变量自增等),严格来说不是纯上下文切换开销。
为什么这种方式仍被广泛使用?核心原因有两个:
- 误差可忽略:上下文切换开销通常在几十到几百纳秒级别,而唤醒后执行的简单用户态指令耗时极短,占比很小,在粗略测量场景下可以接受。
- 用户态难以精确分离:要精准测量“当前进程退出到另一进程进入”的纯内核调度时间,需要内核级工具(比如ftrace、perf)支持。用户态代码只能记录自身指令的时间点,无法直接捕获内核中保存寄存器、切换页表等调度核心步骤的精确时机。
如果想要提升精度,可以尝试:
- 大幅增加循环次数(比如把
TIMES设为100000),用大数平均抵消单次指令的误差; - 替换时钟为
CLOCK_MONOTONIC(比CLOCK_REALTIME更稳定,不受系统时间调整影响); - 用perf工具直接追踪
context-switches事件的耗时,这是更精准的内核级测量方式。
内容的提问来源于stack exchange,提问作者shino
相关产品推荐
相关产品推荐

