题目:
在这个作业中,你将测量系统调用和上下文切换的成本。测量系统调用的成本相对容易。
例如,你可以重复调用一个简单的系统调用(例如,执行 0 字节读取)并记下所花的时间。
将时间除以迭代次数,就可以估计系统调用的成本。你必须考虑的一件事是时钟的精确性和准确性。你可以使用的典型时钟是 gettimeofday()。
详细信息请阅读手册页。你会看到,gettimeofday()返回自 1970 年以来的微秒时间。
然而,这并不意味着时钟精确到微秒。测量 gettimeofday()的连续调用,以了解时钟的精确度。这
会告诉你为了获得一个好的测量结果,需要让空系统调用测试的迭代运行多少次。如果
gettimeofday()对你来说不够精确,可以考虑利用 x86 机器提供的 rdtsc 指令。测量上下文切换的成本有点棘手。lmbench 基准测试的实现方法,是在单个 CPU 上运
行两个进程并在它们之间设置两个 UNIX 管道。管道只是 UNIX 系统中的进程可以相互通
信的许多方式之一。第一个进程向第一个管道写入数据,然后等待第二个数据的读取。由
于看到第一个进程等待从第二个管道读取的内容,OS 将第一个进程置于阻塞状态,并切换
到另一个进程,该进程从第一个管道读取数据,然后写入第二个管理。当第二个进程再次
尝试从第一个管道读取时,它会阻塞,从而继续进行通信的往返循环。通过反复测量这种
通信的成本,lmbench 可以很好地估计上下文切换的成本。你可以尝试使用管道或其他通信
机制(例如 UNIX 套接字),重新创建类似的东西。在具有多个 CPU 的系统中,测量上下文切换成本有一点困难。在这样的系统上,你需
要确保你的上下文切换进程处于同一个处理器上。幸运的是,大多数操作系统都会提供系
统调用,让一个进程绑定到特定的处理器。例如,在 Linux 上,sched_setaffinity()调用就是
你要查找的内容。通过确保两个进程位于同一个处理器上,你就能确保在测量操作系统停
止一个进程并在同一个 CPU 上恢复另一个进程的成本。
两个任务,测量系统调用花费的时间和上下文切换花费的时间,一个小任务,测量gettimeofday()的精度
呃,没有参考代码,那就来吧:
#include<iostream>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include <sys/time.h>
#include <x86intrin.h>
#include <sched.h>
#include <cstring>
#define N 1000000
using std::cout;
using std::cin;
using std::endl;
//测量系统调用的时间和上下文切换的时间
void syscall(){
int fd = open("test.txt",O_RDWR);
char buff[100];
for(int i = 0 ; i < N ; i++ ){
read(fd, buff ,0);
}
close(fd);
}
int main(){
//gettimeofday版
timeval tv[2];
gettimeofday(tv , nullptr);
//syscall();
gettimeofday(&tv[1] , nullptr);
//cout<< N <<"次调用前时间:"<<tv[0].tv_sec<<"秒"<<tv[0].tv_usec<<"微秒"<<endl;
//cout<<"调用后时间:"<<tv[1].tv_sec<<"秒"<<tv[1].tv_usec<<"微秒"<<endl;
//cout<< "平均每百次"<< ( tv[1].tv_usec - tv[0].tv_usec ) * 100/ N <<"微秒"<<endl;
//实验结果是每百次read调用11-13微秒,并未出现很大的误差
//retsc版
unsigned long long start = __rdtsc();
syscall();
unsigned long long end = __rdtsc();
std::cout << "平均每次read系统调用: " << (end - start) / N<< "时钟周期"<<std::endl;
//测量上下文切换的成本
//第一步,建立管道,两个
int pipe1[2];
pipe(pipe1); // pipe1[0] - read, pipe1[1] - write
int pipe2[2];
pipe(pipe2); // pipe2[0] - read, pipe2[1] - write
//第二步,创立进程
pid_t pid1 = fork();
//第三步,绑定cpu
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(0, &mask); // 绑定到 CPU 0
if (pid1 == 0) {
//子进程
pid_t pid2 = getpid();
sched_setaffinity(pid2, sizeof(mask), &mask);
}else if(pid1 > 0){
//父进程
pid_t pid3 = getpid();
sched_setaffinity(pid3, sizeof(mask), &mask);
}
//第四步,进行lmbench 基准测试
//梳理一下思路,进程A往pipe1里写,然后进程B去读,把读到的写到pipe2,然后A从pipe2读,再写进pipe1,每次一个进程的任务完成进入等待都会触发上下文切换
//read这个系统调用函数,在目标为空的时候会自然地触发等待,所以我们不需要显式地做什么操作完成等待
const char* ack = "ACK";
const char* ack2 = "RST";
const int ack_len = strlen(ack);
const int ack2_len = strlen(ack2);
if(pid1 == 0){
//子进程
close(pipe1[1]); // 不写 pipe1
close(pipe2[0]); // 不读 pipe2
char buf[128];
for (int i = 0; i < N; i++) {
ssize_t n = read(pipe1[0], buf, sizeof(buf)); //读
if (n > 0) buf[n] = '\0';
write(pipe2[1], ack, ack_len);//写
}
close(pipe1[0]);
close(pipe2[1]);
}else if(pid1 > 0){
//父进程
unsigned long long begin2 = __rdtsc();
close(pipe1[0]); // 不读 pipe1
close(pipe2[1]); // 不写 pipe2
for (int i = 0; i < N; i++) {
write(pipe1[1], ack2, ack2_len);//写
// 等待 ack
char ack_buf[10] = {0};
read(pipe2[0], ack_buf, sizeof(ack_buf));//读
}
unsigned long long end2 = __rdtsc();
std::cout << "平均每次上下文切换: " << (end2 - begin2) / (N * 2)<< "时钟周期"<<std::endl;
close(pipe1[1]);
close(pipe2[0]);
wait(nullptr); // 等子进程结束
}
}
运行完的结果:
平均每次read系统调用: 381时钟周期
平均每次上下文切换: 4141时钟周期
查阅资料可知,不同的系统调用消耗的时钟周期会差特别大,上下文切换在更大的程序里,开销也会变大
本实验就不继续深入了