IO Profiler

写一个IO profile和trace的插桩库,实现如下功能:

采用动态库,不需要用户修改一行代码

  • profile功能:readwrite调用次数,平均IO操作的读大小,写大小,平均读写时间
  • Trace功能:按照时间顺序输出每次IO调用信息(读写类型、读写大小等信息)

运行时库打桩

这部分的知识可以参考书本《深入理解计算机系统》第七章第13节的内容。

基本思想:给定一个需要打桩的目标函数(此次作业中的readwrite),自己创建一个包装函数,其原型与目标函数完全一样。使用某种特殊的打桩机制,欺骗程序去调用包装函数而不是原来的目标函数。一般地,包装函数可以执行自己的逻辑(遂能记录调用次数、验证和追踪输入输出值等),调用原目标函数,再将原目标函数的返回值传递给调用者。

打桩可以分为:

  • 编译时,使用的是C预处理器,需要访问程序源代码;
  • 链接时,用的是静态链接器,需要访问程序的可重定位对象文件;
  • 运行时,用的是动态链接器,只要访问可执行目标文件

这次作业用的是运行时打桩,基于动态链接器的LD_PRELOAD环境变量。当加载和执行一个程序,需要解析未定义的引用时,动态链接器会先搜索LD_PRELOAD的库,然后才搜索任何其他的库。

Profile

因为要追踪读写的调用次数、平均大小、平均时间,所以可以每次调用readwrite时都根据传入的参数fd文件描述符来累加对该文件的记录。每个文件与其文件描述符一一对应。这样的缺点是两次打开同一个文件就会被当作两次不同的记录了,这两次分别有一个次数、大小和时间的统计。

由于一般情况下,Linux系统默认一个进程最多只能打开1024个文件,所以据此开辟用于记录的数组。为了在close之后能复用一些文件描述符,也记录一下某个fd的当前状态(是否被占用)。

#define MAX_FD 1024
char fd_to_names[MAX_FD][512];// 记录fd对应的文件名
int fd_active[MAX_FD];// 记录当前这个fd是否被占用中

int read_cnt[MAX_FD] = {0};// 读的次数
int read_size[MAX_FD] = {0};// 读的总大小
double read_time[MAX_FD] = {0.0};// 读的总耗时

int write_cnt[MAX_FD] = {0};// 写的次数
int write_size[MAX_FD] = {0};// 写的总大小
double write_time[MAX_FD] = {0.0};// 写的总耗时

FILE * prof_fp = NULL;// profile输出的文件指针
FILE * trace_fp= NULL;// trace输出的文件指针

在每次调用open打开某个文件时,需要对相应位置的数据记录做初始化:

ssize_t open(const char * pathname, int flags, mode_t mode) {
  ssize_t (*openp)(const char*, int, mode_t);
  char * error;

  openp = dlsym(RTLD_NEXT, "open");
  if ((error = dlerror()) != NULL) {
    fputs(error, stderr);
    exit(1);
  }

  int fd = openp(pathname, flags, mode);
  assert(fd_active[fd] == 0);// 返回的fd一定是未被占用的
  fd_active[fd] = 1;  
  strcpy(fd_to_names[fd], pathname);
  return fd;
}

在每次读和写时累加记录:

ssize_t read(int fd, void * buf, size_t count) {
  ssize_t (*readp)(int, void *, size_t);
  char * error;

  readp = dlsym(RTLD_NEXT, "read");
  if ((error = dlerror()) != NULL) {
    fputs(error, stderr);
    exit(1);
  }
  
  double t = wall_time();
  int rv = readp(fd, buf, count);
  t = wall_time() - t;

  read_cnt[fd]++;
  read_size[fd] += rv;
  read_time[fd] += t;
  assert(trace_fp != NULL);
  fprintf(trace_fp, "%20s %5s %15d %15.6lf\n", fd_to_names[fd], "RD", rv, t * 1e3);
  return rv;
} 

ssize_t write(int fd, const void * buf, size_t count) {
  ssize_t (*writep)(int, const void *, size_t);
  char * error;
  
  writep = dlsym(RTLD_NEXT, "write");
  if ((error = dlerror()) != NULL) {
    fputs(error, stderr);
    exit(1);
  }
  
  double t = wall_time();
  int rv = writep(fd, buf, count);
  t = wall_time() - t;  

  write_cnt[fd]++;
  write_size[fd] += rv;
  write_time[fd] += t;
  assert(trace_fp != NULL);
  fprintf(trace_fp, "%20s %5s %15d %15.6lf\n", fd_to_names[fd], "WR", rv, t * 1e3);
  return rv;
}

而当close被调用时,则将对应fd的数据整理(计算大小和时间的平均值)输出到IO.prof的结果文件中,并清除该位置上的记录:

int close(int fd) {
  int (*closep)(int);
  char * error;

  closep = dlsym(RTLD_NEXT, "close");
  if ((error = dlerror()) != NULL) {
    fputs(error, stderr);
    exit(1);
  }
  
  int ret = closep(fd);
  if (read_cnt[fd] > 0) {
    read_size[fd] /= read_cnt[fd];
    read_time[fd] /= read_cnt[fd];
  }
  if (write_cnt[fd] > 0) {
    write_size[fd] /= write_cnt[fd];
    write_time[fd] /= write_cnt[fd];
  }
  fprintf(prof_fp, "%20s %8d %15d %15.6lf %8d %15d %15.6lf\n", \
      fd_to_names[fd],   read_cnt[fd],  read_size[fd],  read_time[fd] * 1e3, \
                        write_cnt[fd], write_size[fd], write_time[fd] * 1e3);
  memset(fd_to_names[fd], 0, sizeof(fd_to_names[fd]));
  fd_active[fd] = 0;
  read_cnt [fd] = write_cnt [fd] = 0;
  read_size[fd] = write_size[fd] = 0;
  read_time[fd] = write_time[fd] = 0.0;
  return ret;
}

Trace

要按照时间顺序输出每次IO调用信息(读写类型、读写大小等),只要在readwrite函数中加上对相应文件句柄的记录即可。具体见上面代码中的fprintf(trace_fp,...)部分。

由于这些库函数是运行时被待测试的可执行程序调用的,怎么能够在不修改可执行程序的情况下,将它们记录的东西(内存中在该动态库的数据段)输出出来呢?查了一下,可以设置在main函数执行前和返回前执行两个函数,分别用__attribute__((__constructor__))__attribute__((__destructor__))来修饰:

__attribute__((__constructor__)) void pre_func(void) {
  printf("pre_func: fopen IO.prof and IO.trace\n");
  prof_fp = fopen("IO.prof", "w+");
  if (prof_fp == NULL) {
    printf("unable to open IO.prof\n");
    exit(1);
  }
  fprintf(prof_fp, "  IO profile report\n");
  fprintf(prof_fp, "%20s %8s %15s %15s %8s %15s %15s\n", \
            "pathname", "RD_CNT", "RD_AVG_SIZE/B", "RD_AVG_TIME/ms", \
                        "WR_CNT", "WR_AVG_SIZE/B", "WR_AVG_TIME/ms");

  trace_fp= fopen("IO.trace", "w+");
  if (trace_fp == NULL) {
    printf("unable to open IO.trace\n");
    exit(1);
  }
  fprintf(trace_fp, "  IO trace report\n");
  fprintf(trace_fp, "%20s %5s %15s %15s\n", "pathname", "TYPE", "SIZE/B", "TIME/ms");
}

__attribute__((__destructor__)) void post_func(void) {
  printf("post_func\n");
  fclose(prof_fp);
  fclose(trace_fp);
}

这两个函数,pre_func负责建立profile和trace所需要的输出文件并打印表头,post_func则负责结束输出。或许有更好的实现方法,这里就简单粗暴一点了。

测试验证

作业说从网上找一个IO程序验证该工具,在Github上搜了一下感觉没有直接能用的,所以干脆自己写了个小程序。随机决定每次运行时,所要创建的最大文件数;随机决定每次操作要针对的文件;随机决定每次操作的类型(读、写、关);随机决定每次写时的数据大小;随机决定每次读时的数据大小和起始位置。

/* testIO.cpp */
#include <cstdlib>
#include <cstdio>

#include <string.h>// strerror()
#include <unistd.h>// write(), read(), close()
#include <sys/types.h>// open()
#include <sys/stat.h>// open()
#include <fcntl.h>// open()
#include <errno.h>// errno, strerror()

#include <cmath>// rand()
#include <ctime>// time()
#include <sys/time.h>// gettimeofday()
#include <vector>
#include <unordered_map>
using namespace std;

#define MAX_FILES 1000

#define MAX_LEN 4096
char content[MAX_LEN];
char buf[MAX_LEN];

void init_content() {
    printf("size of content: %d bytes\n", sizeof(content));
    for (int i = 0; i < sizeof(content); i++) {
        int j = (i + 33 > 126) ? ((i+33)%126 + 33) : (i + 33);
        content[i] = (char)j; 
    }
}

double wall_time() {
    struct timeval t;
    gettimeofday(&t, NULL);
    return 1.*t.tv_sec + 1.e-6*t.tv_usec;
}

typedef enum {OPEN = -1, READ, WRITE, CLOSE, IO_TYPE_CNT} IO_TYPE;

#define ERROR_OPEN(idx,fd)  {printf("file idx %d of fd %d open failed: %s\n", idx, fd, strerror(errno));  return -1;}
#define ERROR_READ(idx,fd)  {printf("read file ids %d of fd %d failed: %s\n", idx, fd, strerror(errno));  return -1;}
#define ERROR_WRITE(idx,fd) {printf("write file ids %d of fd %d failed: %s\n", idx, fd, strerror(errno)); return -1;}

int main()
{
    srand(time(0));
    int num_files = rand() % MAX_FILES + 1;// 随机决定进程最多拥有多少个文件号
    printf("num files: %d\n", num_files);

    init_content();// 初始化一段字符数组,之后反复用这里面的内容来写

    unordered_map<int, int> idx_to_fd;

    double startT = wall_time(), currT;
    while ((currT = wall_time()) < startT + 2.0) {// 让程序运行够2秒
        int idx = rand() % num_files;// 随机选取一个文件号
        int rv;// return value of system calls
        if (idx_to_fd.find(idx) == idx_to_fd.end()) {// 之前还没创建过这个文件
            char filename[256];
            sprintf(filename, "%d.txt", idx);// 该文件号对应的文件名
            int fd = open(filename, O_RDWR|O_CREAT|O_TRUNC, 0666);
            if (fd < 0) ERROR_OPEN(idx, fd);

            idx_to_fd[idx] = fd;// 该文件对应的描述符
            // 创建这个文件的同时就写入一些东西
            if ((rv = write(fd, content, MAX_LEN)) < 0) ERROR_WRITE(idx, fd);
        } else {
            int fd = idx_to_fd[idx];
            if (fd == -1) continue;// 之前已经关掉过这个文件,就不再用它了

            int type = rand() % IO_TYPE_CNT;// 随机决定读写类型
            if (type == READ) {
                int file_len = lseek(fd, 0, SEEK_END);// 返回值就是文件指针距离文件开头的偏移量,也就是文件的长度
                int offset = rand() % file_len;// 随机决定读的位置
                lseek(fd, offset, SEEK_SET);

                int count = rand() % MAX_LEN;// 随机决定读的量
                if ((rv = read(fd, buf, count)) < 0) ERROR_READ(idx, fd);

                lseek(fd, 0, SEEK_END);// 读完之后重新将文件指针恢复到末尾
            } else if (type == WRITE) {
                int count = rand() % MAX_LEN;
                if ((rv = write(fd, content, count)) < 0) ERROR_WRITE(idx, fd);
            } else {// CLOSE
                close(fd);
                idx_to_fd[idx] = -1;// 标记为已关闭
            }
        }
    }

    // 关掉所有仍活跃的fd
    for (unordered_map<int,int>::iterator it = idx_to_fd.begin(); it != idx_to_fd.end(); it++) {
        if (it->second != -1) {
            close(it->second);
        }
    }

    return 0;
}

编译,运行:

gcc -shared -fpic -o myIO.so myIO.c -ldl
g++ -o testIO.exe -std=c++11 testIO.cpp
LD_PRELOAD="./myIO.so" ./testIO.exe

得到输出结果:此次运行最多共有57个文件

在这里插入图片描述

查看IO.prof文件如下:
在这里插入图片描述

查看IO.trace文件如下。如预期,对每个文件的第一次操作都是写,大小为4096字节。第一次非4096字节的写是出现在之前已创建过的5.txt,做了一次随机化的读操作;紧随其后有一个对55.txt的随机化的写操作。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zongy17

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值