ujudger-base 0.1 编写小结

1

Comments

昨天说今天要写一篇来说说 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)

One Response to “ujudger-base 0.1 编写小结”

Leave a Reply