#引言
有時,我們爲了分析程序的某些問題(例如性能問題),需要對程序的IO操作的頻率耗時等數據進行監控。就以經常調用的read和write函數爲例,如何監控程序中所有上述兩種操作,同時進行計時呢?使用gdb打斷點自然是個方法,這樣可以很容易發現程序中所有的調用,但是這樣就沒辦法計時了。如果在程序中所有read/write函數前後打上時間戳,就可以實現計時了,但是這會有另一個問題,我們可能調用了某些三方庫的接口,可能沒辦法爲三方庫的read/write函數打時間戳計時。那麼有沒有辦法既能監控所有read/write函數調用,同時進行計時呢?自然是有的,例如本文下面要講的——利用LD_PRELOAD機制實現上述目標。
關於LD_PRELOAD的概述,在之前一篇博文我已經進行過描述:CSAPP第三版運行時打樁Segmentation fault。這裏貼上其部分內容:
CSAPP第三版7.13.3節提到了運行時打樁機制,它可以在運行時將程序中對共享庫函數的調用進行截獲,替換爲執行自己的代碼。這個機制基於動態鏈接器的LD_PRELOAD環境變量。如果LD_PRELOAD環境變量被設置爲一個共享路徑名的列表(以空格或分號分隔),那麼當加載和執行一個程序,需要解析未定義的引用時,動態鏈接器(ld-linux.so)會先搜索LD_PRELOAD庫,然後才搜索任何其他的庫。有了這個機制,當加載和執行任意可執行文件時,可以對共享庫中的任何函數打樁,包括libc.so。
我們要做的就是對C標準庫的read/write函數進行包裝,實現計時功能。同時在運行時利用LD_PRELOAD將C標準庫的read/write函數替換成我們的實現。
#包裝read/write函數
直接上代碼:
#ifndef _GNU_SOURCE
# define _GNU_SOURCE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/time.h>
// 計時函數,返回值是一個毫秒值
static double getMs()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000 + tv.tv_usec * 0.001;
}
ssize_t read(int fd, void *buf, size_t count)
{
// 爲什麼要有print_times這個變量,請查看上面我貼出來的那篇博文
static __thread int print_times = 0;
print_times++;
// 用於保存真正的read函數
ssize_t (*readp)(int, void *, size_t);
char *error;
// 獲取真正的read函數
readp = dlsym(RTLD_NEXT, "read");
if ((error = dlerror()) != NULL)
{
fputs(error, stderr);
exit(1);
}
double start = getMs();
// 調用真正的read函數
ssize_t result = readp(fd, buf, count);
double stop = getMs();
if (print_times == 1)
{
// 輸出調用read時的實參和耗時
printf("read(%d, %p, %lu) %lf\n", fd, buf, count, stop - start);
}
print_times--;
return result;
}
ssize_t write(int fd, const void *buf, size_t count)
{
static __thread int print_times = 0;
print_times++;
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 start = getMs();
ssize_t result = writep(fd, buf, count);
double stop = getMs();
if (print_times == 1)
{
printf("write(%d, %p, %lu) %lf\n", fd, buf, count, stop - start);
}
print_times--;
return result;
}
上面的代碼即能夠對read/write函數進行計時,並將計時結果輸出到標準輸出。
將上面的代碼編譯成庫:
$ gcc -shared -fpic -Wall -Wextra -o libwrapper.so wrapper.c -ldl
這樣,我們就得到了一個libwrapper.so文件。
#替換read/write函數
用於測試的demo代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd == -1)
{
exit(1);
}
write(fd, "test", 4);
lseek(fd, 0, SEEK_SET);
char buf[5] = {0};
read(fd, buf, 4);
close(fd);
printf("%s\n", buf);
return 0;
}
編譯成可執行文件:
$ g++ main.c
那麼如何使用前面生成的libwrapper.so文件呢?就像下面這樣:
$ LD_PRELOAD=./libwrapper.so ./a.out
其輸出爲:
write(3, 0x55a5dd6c29ad, 4) 0.023926
read(3, 0x7ffcf0a92fa3, 4) 0.010010
test
這樣,我們就達到了監控read/write函數的目標,對於其他IO庫函數,你也可以根據需要進行包裝替換。