You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何更精确测量上下文切换开销?基于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调用的收尾、循环变量自增等),严格来说不是纯上下文切换开销。

为什么这种方式仍被广泛使用?核心原因有两个:

  1. 误差可忽略:上下文切换开销通常在几十到几百纳秒级别,而唤醒后执行的简单用户态指令耗时极短,占比很小,在粗略测量场景下可以接受。
  2. 用户态难以精确分离:要精准测量“当前进程退出到另一进程进入”的纯内核调度时间,需要内核级工具(比如ftrace、perf)支持。用户态代码只能记录自身指令的时间点,无法直接捕获内核中保存寄存器、切换页表等调度核心步骤的精确时机。

如果想要提升精度,可以尝试:

  • 大幅增加循环次数(比如把TIMES设为100000),用大数平均抵消单次指令的误差;
  • 替换时钟为CLOCK_MONOTONIC(比CLOCK_REALTIME更稳定,不受系统时间调整影响);
  • 用perf工具直接追踪context-switches事件的耗时,这是更精准的内核级测量方式。

内容的提问来源于stack exchange,提问作者shino

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 10:29:07