这是对 22-23 年秋季学期南京大学操作系统课程的 Lab 的实验记录,这边为蒋炎岩老师班的课程。

本博客是对 OS Lab M3 的编程实验的个人的思考过程与部分解答。

课程主页: http://jyywiki.cn/OS/2023/

pstree实验主页: https://jyywiki.cn/OS/2023/labs/M3.html

注意:这不是非官方答案,不完全正确,仅适当参考。

注意:请勿直接复制粘贴,否则后果自负。


​ 每一个人都有属于自己的一片森林, ​ 也许我们从来不曾去过, ​ 但它一直在那里,总会在那里。 ​ 迷失的人迷失了,相逢的人会再相逢。 ​ 即使是你最心爱的人, ​ 心中都会有一片你没有办法到达的森林。

​ ——《挪威的森林》


整体思路

sperf的任务和pstree类似,都是通过查阅手册熟悉操作系统的接口,实现出一个小工具。实现思路jyy也已经告诉我们了,我们只需要照着复刻即可:

  1. 解析出 COMMAND 和 ARG,这可以当做是普通的编程练习;
  2. 使用fork创建一个新的进程:
    1. 子进程使用 execve 调用strace COMMAND ARG...启动一份 strace;
      • execve 成功返回以后,子进程已经不再受控制了,strace 会不断输出系统调用的 trace,直到程序结束。程序不结束 strace 也不会结束
    2. 父进程想办法不断读取 strace 的输出,直到 strace 程序结束。
      • 能读取到输出,就可以解析出每个系统调用的时间,从而把统计信息打印到屏幕上

为此,我的代码逻辑大概如下所示:

1
2
3
4
5
6
main: 主函数
Init: 初始化
forkRead: 主干函数
subProcessing: 子进程,用于启动我们输入的程序,并向父进程输入系统调用
parentProcessing: 父进程,用于读取子进程的系统调用并进行分析
analysis: 分析系统调用

接下来我便按照这个流程来进行介绍

初始化

首先是进行一些初始化的工作,包括创建管道,获取环境变量等等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Init()
{
readPort = &Pipe[0], writePort = &Pipe[1];
// if the pipeline creation fails the function will directly end the function
if (pipe(Pipe) == -1)
{

perror("pipe error!");
exit(EXIT_FAILURE);
}
// reduce the output that is not needed to /dev/null
devNull = open("/dev/null", O_WRONLY);
// get environment variables and save it into PATH
char *pathVar = getenv("PATH");
strcpy(PATH, pathVar);
// fill in pad to '\0'
memset(pad, '\0', sizeof(pad));
}

主干函数

主干函数主要用于启动子进程,通过cpid区分父子进程,然后父子进程分别执行不同的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void forkRead(int argc, char *argv[], char *envp[])
{
int cpid = fork(); // replication process
// if the creation fails exit directly
if (cpid == -1)
{
perror("fork error!");
exit(EXIT_FAILURE);
}
Assert((cpid != -1), "fork exit failed");
if (cpid == 0)
{
// the process of running strace created
subProcessing(argc, argv, envp);
exit(EXIT_SUCCESS);
}
else
{
// the thread used for output results
parentProcessing();
exit(EXIT_SUCCESS);
}
}

子进程

子进程接入管道的写口,然后启动我们指定的任务程序开始执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void subProcessing(int argc, char *argv[], char *envp[])
{
// reset the output of the sub thread
dup2(devNull, fileno(stdout)), dup2(devNull, fileno(stderr));
// sub process redirection to close the reading at the same time
close(*readPort);
// construct the parameters required for strace
char *exec_argv[32] = {"strace", "-T", "-o"};
char writeFile[32];
sprintf(writeFile, "/proc/self/fd/%d", *writePort);
exec_argv[3] = writeFile;
for (int i = 1; i <= argc - 1; i++)
{
exec_argv[i + 3] = argv[i];
}
exec_argv[argc + 3] = NULL;
// according to ":" divide path and look for strace for each path
char *token = strtok(PATH, delim);
while (token != NULL)
{
char straceFile[128];
sprintf(straceFile, "%s/strace", token);
execve(straceFile, exec_argv, envp);
straceFile[0] = '\0';
token = strtok(NULL, delim);
}
Assert((token == NULL), "incompletely divided");
perror(argv[0]);
exit(EXIT_FAILURE);
}

父进程

父进程接入管道的读口,然后接受子进程的系统调用并进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void parentProcessing()
{
// parent process closes the write port
close(*writePort);
// copy the output results of the sub processes
// and adjust the analysis function as a solid parameter
char buf[2], arr[1024];
int index = 0;
struct timeval start, end;
gettimeofday(&start, NULL);
while (1)
{
if (isEnd)
{
/*
if the process ends print immediately and return
*/
print();
break;
}
// read a line of system calls and parse them
while (read(*readPort, &buf, 1) > 0)
{
arr[index++] = buf[0];
if (buf[0] == '\n')
{
arr[index] = '\0';
analysis(arr, index);
index = 0;
break;
}
}
gettimeofday(&end, NULL);
// output every second
if (end.tv_sec - start.tv_sec >= 1)
{
Assert((end.tv_sec - start.tv_sec >= 1), "pass less than one second");
print();
start = end;
}
}
Assert((isEnd), "child process not terminated");
close(*readPort);
}

解析函数

解析函数就比较简单了,根据固定的格式进行解析读取即可(可以采用正则),需要注意的是每次解析都需要判断子进程的程序是否结束(以'+'结尾):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void analysis(char buffer[], ssize_t count)
{
// name is the name that is called by the system
// and time is the time to use this call
char name[128];
float time = 0.0f;
if (buffer[0] == '+')
{
// "+++ exited with 0 +++" represents the end of the process
isEnd = 1;
return;
}

// get name and time obtained through format string
sscanf(buffer, regular, name, &time);
for (int i = 0; i <= countOfItem; i++)
{
if (i == countOfItem)
{
// unprecedented system calls
strcpy(table[countOfItem].name, name);
table[countOfItem].time = time;
countOfItem++;
break;
}
else if (strcmp(name, table[i].name) == 0)
{
// system calls that have appeared
table[i].time += time;
break;
}
}
}

至此我们的sperf便基本完成了。

总结

总体而言,有过pstree的经验之后,sperf便显得简单了很多。因为他们的coding流程基本是一模一样的:

熟悉系统调用,解析命令行,将命令行的任务迁移到对应的系统调用上面。

掌握了这个流程,sperf的编写就显得十分简单了。