昨天说今天要写一篇来说说 ujudger-base 用到的技术层的资料,今天就来试着写一写,顺便自己复习一下相关的东西。
这些大多是参考网上资料及 man 手册得到的,有错误的地方请多指教!
最早查早的资料是关于创建子进程的。在查找了很多资料后,我发现,在 Linux 下创建子进程只有一个方法—— fork 函数和 vfork 函数。两个函数有些许的不同,但他们的用法却大体相同,即从调用函数开始,程序分岔为父进程和子进程两个完全相同的程序。最神奇的地方出现了:这个函数一旦调用成功将会返回两次!不知道在哪里看到这个说明,让我瞬间领悟了这个函数的用法。这个函数会在父进程和子进程各返回一次,父进程中返回子进程的 pid ,子进程中返回 0。如果调用错误,则返回一次 -1,并将具体的错误编号放入 errno (需引用 errno.h)。于是我们可以用一个很简短的程序来说明这个函数的用法:
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 | #include <iostream> extern "C" { #include <unistd.h> #include <sys/types.h> #include <errno.h> } using namespace std; int main(int argc, char** argv) { int child = fork(); if (child == 0) { cout << "This is subprocess! " << endl; cout << "My pid is " << getpid() << endl; } else if (child == -1) { cout << "An error occurred! " << endl; cout << "Error Number: " << errno << endl; } else { cout << "This is parent process! " << endl; cout << "My pid is " << getpid() << endl; cout << "My child's pid is " << child << endl; } return 0; } |
创建了子进程就要运行别的程序了,在 Linux 中需要使用 exec 族函数。这些函数包括:
1 2 3 4 5 6 | int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); |
他们的不同用法可以参考 man 手册,不过我个人比较喜欢 execl 所以就小小介绍一下。execl 是我认为这里面最简单的一个函数了!用法就是 execl(欲执行的程序的绝对路径, 参数0, 参数1, …, NULL); 如果不返回就调用成功了(又一个奇怪的函数),因为原来的进程内容被新的程序覆盖了。返回就说明必定出错……同样的错误代码存于 errno。下面用一个小小的程序来说明一下:
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 | #include <iostream> extern "C" { #include <unistd.h> #include <errno.h> } using namespace std; int main(int argc, char** argv) { int child = fork(); if (child == 0) { execl("/bin/ls", "ls", "..", NULL); cout << "An error occurred in execl()! " << endl; cout << "Error Number: " << errno << endl; } else if (child == -1) { cout << "An error occurred in fork()! " << endl; cout << "Error Number: " << errno << endl; } else { cout << "This is parent process!" << endl; } return 0; } |
其中 execl 的参数 0 是必须要有的,可以为空串或者欲执行程序的名称(如该例中为 ls)。使用过 C++ 的 argv 的应该都知道,argv[0] 是程序名称。参数列表要以一个 NULL 结尾,否则应该会出错。另外就是,欲执行的程序也可以用形如“./prog”来表示运行当前目录下的程序。
开始的时候我有些不明白,Linux 为什么要弄得这么麻烦,多一个步骤。不过后来想想,觉得这样也不错,父进程甚至可以“要求”子进程做一些事情,比如 ptrace me……
下面来说说关于获取进程运行时间和占用内存的问题。我原来想单单通过 getrusage 函数来获取,但后来发现这个函数不但获取不了内存耗用(返回 0),而且也不能了解程序是否结束……后来发现一个很不错的函数 wait4 ,拥有全部 getrusage 的功能,并且还更强大!根据 man 手册,wait4 的定义是 pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage); 这告诉我们,wait4 不但可以返回 rusage 结构的资料,还有退出时的返回值 status (突然想起来,ujudger-base 0.1 里忘记监视 status 的值了……)。同时,man 手册告诉我们,如果 options 设为 WNOHANG ,那么函数将不会等待而立即返回,如果没有 pid 指定的进程已经结束,则返回 0。这样我们就有了一个可以等待程序结束的方法了!不过这里获得的 rusage 其实和 getrusage 一样,只有时间可以收集到……收集到的时间是以 struct timeval 的格式存储的,timeval 的定义为:
1 2 3 4 | struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; |
一个是秒,一个是微秒(1μs=1×10-6s),还是蛮精确的吧?
有了这个,我们还需要一点东西,来等待。虽然 wait4 可以自己等待,但我们不希望他在等待的时候我们什么也不能做不是?经过查找,我发现 Linux 下有一个叫 sleep 的函数,但这个函数的等待时间单位是秒……我觉得好郁闷……后来知道还有一个叫 usleep 的函数,等待时间是以微秒记,这才是我们需要的!(虽然根据实验,这个函数的计时是有点误差的)下面把前面的函数全部联合起来写一个小程序(我觉得已经有些许接近 ujudger-base 0.1 了……):
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 | #include <iostream> extern "C" { #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/time.h> #include <sys/resource.h> #include <sys/wait.h> } using namespace std; int main(int argc, char** argv) { int child = fork(); if (child == 0) { execl("./test", "", NULL); cout << "An error occurred in execl()! " << endl; cout << "Error Number: " << errno << endl; } else if (child == -1) { cout << "An error occurred in fork()! " << endl; cout << "Error Number: " << errno << endl; } else { cout << "This is parent process!" << endl; cout << "Start waiting..." << endl; int status; rusage usage; while (wait4(RUSAGE_CHILDREN, &status, WNOHANG, &usage) == 0) { usleep(100 * 1000); // Sleep for 100ms } double usertime = static_cast<double>(usage.ru_utime.tv_sec) + static_cast<double>(usage.ru_utime.tv_usec) / 1e6; cout << "Time used: " << usertime << "s" << endl; } return 0; } |
其中的“./test”的代码:
1 2 3 4 5 6 7 8 9 10 | #include <iostream> using namespace std; int main(int argc, char** argv) { cout << "Test start!" << endl; for (int i = 0; i != 1000000000; ++i); cout << "Test end!" << endl; return 0; } |
测 ujudger-base 的时候也是用类似的程序测试的。
最后就是关于运行时获取程序各项信息的方法。查找了许多资料后,开始使用的读取内存耗用的方法是读取 /proc/<pid>/statm 文件的方法,后来使用因为想要实时获取已经耗用的时间而最终转而读取 /proc/<pid>/stat 文件。这两个文件的相关说明在“man proc”中已经很详细了,这里就不一一说明了。我所用到的是 stat 里面的 utime 和 rss。utime 获取的单位为 jiffies,将其转换为时间需要一个宏 JIFFIES_TO_NS。我在 include 里面没有找到这个宏的定义,这里给出来:
1 2 | #include <sys/param.h> // For HZ below #define JIFFIES_TO_NS(TIME) ((TIME) * (1000000000 / HZ)) |
这个宏将 jiffies 转换为纳秒(1ns=1×10-9s)。而 rss 获取的内存单位为 pages,可以将其乘以 getpagesize 函数来获得进程占用的内存空间(单位:字节)。不过这里的 rss 获得的似乎和系统监视器中获得的数据不大一样,根据对比,系统监视器获得的应该是 statm 文件中的 data 数据。这个部分的代码可以参看 ujudger-base 中getrunstat 函数。
总体上说,这次这个 ujudger-base 0.1 中用到的技术就这些了。写的同时也发现昨晚那个程序不完善的地方,一个是读取 rss 的时候直接乘4(<< 2)而不是乘以 getpagesize() ;一个是没有检验程序返回值(通常运行时错误都已这种形式表达);还有就是没有处理 execl 可能发生的错误……稍微修改一下……
发布修改后的新版本:ujudger-base-0.1.1.cpp (3.9 KB)
Comments