操作系统笔记(2)-受限直接执行

概念

  • 直接执行 : 表示直接在 CPU 上运行程序

  • 受限 : 为什么要 受限 呢 ? 是为了保证操作系统来管理资源, 防止其失去控制权, 操作系统必须以高性能的方式虚拟化 CPU , 同时保持对系统的控制, 因此程序的执行需要 受限 , 具体是受到硬件的限制

如何执行受限制的操作

操作系统引入了两种模式:

  • 用户模式:在此模式下运行的代码会受到限制。例如,在用户模式下运行时,进程不能发出 I/O 请求

  • 内核模式:在此模式下,运行的代码可以做它喜欢的事,包括特权操作,如发出 I/O 请求和执行特权指令

用户如何执行特权操作

  • 用户模式 下要执行特权操作,需要使用 系统调用 ,它允许内核小心地向用户程序暴露某些关键功能,例如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存

  • 要执行 系统调用 ,程序必须执行特殊的陷阱(trap)指令, 该指令同时跳入内核并将特权级别提升到 内核模式,完成相应工作后,操作系统调用一个特殊的从陷阱返回(return-from-trap)指令,回到 用户模式

  • C 库中进行 系统调用 的部分是用汇编手工编码的,因为它们需要仔细遵循约定,以便正确处理参数和返回值,以及执行硬件特定的陷阱指令

受限直接运行过程

操作系统-启动 (内核模式) 硬件 程序(用户模式)
初始化陷阱表
记住系统调用处理程序的地址
在进程列表上创建条目
为程序分配内存
将程序加载到内存中
根据 argv 设置程序栈
用寄存器/程序计数器填充内核栈
从陷阱返回
从内核栈恢复寄存器
转向用户模式
跳到 main
运行 main
调用系统调用
陷入操作系统
将寄存器保存到内核栈
转向内核模式
跳到陷阱处理程序
处理陷阱
做系统调用的工作
从陷阱返回
从内核栈恢复寄存器
转向用户模式
跳到陷阱之后的程序计数器
从 main 返回
陷入 (通过 exit())
释放进程的内存
将进程从进程列表中清除

在进程之间切换

问题

  • 对于 单核 系统来说, 如果一个进程在 CPU 上运行, 这就意味着操作系统没有运行, 那操作系统怎么做事情呢?

  • 因此如何重新获得 CPU 的控制权以便它可以在进程之间切换变得非常重要

协作方式:等待系统调用

  • 以前采取的一种策略是选择相信用户进程, 运行时间过长的进程被假定会定期放弃 CPU, 以便操作系统可以决定运行其他任务

  • 一个问题是, 放弃 CPU 需要执行 系统调用 , 进入 内核模式 , 将 CPU 的控制权交给操作系统, 然而如果程序一直无限循环而不进行系统调用, 系统就会卡死

  • 以前面对这种问题的解决方法就是 重启

非协作方式:操作系统进行控制

  • 这种方式利用了硬件的 时钟中断 , 产生 时钟中断 时, 当前运行的程序停止, 将状态保存后运行 中断处理程序

  • 操作系统预先配置好 中断处理程序 , 然后当程序启动时开启时钟, 每隔一段时间触发 中断处理程序 , 将 CPU 的控制器交给操作系统, 然后操作系统根据 调度策略 可以决定接下来运行哪个程序

  • 如果操作系统决定切换到另一个程序执行, 就会执行一些底层代码, 即所谓的 上下文切换(content switch) , 它要做的就是为当前只在执行的程序保存一些寄存器的值, 然后为即将执行的程序恢复一些寄存器的值, 然后切换到另一个程序执行

测量作业

  • 测试 系统调用 的执行时间, 使用 C 库的 gettimeofday() 计算, 以 0字节读取 为例, 测试机是单核的, 测试代码入下:
#include <stdio.h>
#include <sys/time.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("请输入迭代次数\n");
        return 0;
    }
    int cnt = atoi(argv[1]);
    printf("迭代次数为: %d\n", cnt);
    int fd = open("./p2.txt", O_WRONLY);
    
    struct timeval start, end;
    char buf[0];
    gettimeofday(&start, NULL);

    for (int i = 0; i < cnt; i++) {
        // 0 字节读取
        write(fd, buf, 0);
    }

    gettimeofday(&end, NULL);

    long int diff = 1000000 * (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec);
    
    printf("diff is : %ldus\n", diff);
    close(fd);
    return 0;
}

结果验证表明平均调用时间是 1~2us :

系统调用测试

  • 为进程指定运行在哪个 CPU 上, 测试机是双核的, 运行时在另一个窗口通过使用 ps -eo pid,args,psr 命令查看结果, 测试代码如下:
#define _GNU_SOURCE

#include <stdio.h>
#include <sched.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char* argv[]) {
    
    cpu_set_t mask, get;

    int cpus = sysconf(_SC_NPROCESSORS_ONLN);
    printf("cpus: %d\n", cpus);

    CPU_ZERO(&mask);
    CPU_SET(0, &mask);  // 将本进程绑定到CPU0上

    if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
        printf("Set CPU affinity failure, ERROR: %s\n", strerror(errno));
        return -1;
    }

    while (1) {
        sleep(1);
    }

    return 0;
}

结果显示如下:两个 CPU 的编号分别是 01

固定cpu

  • 使用 lmbench 工具测试上下文切换的成本

参考官方说明