8.15 进程时间

8.15 进程时间

1 . 1 0节中说明了墙上时钟时间、用户CPU时间和系统CPU时间。任一进程都可调用times函数以获得它自己及终止子进程的上述值。

#include <sys/times.h>

clock_t times(struct tms *buf ) ;

返回:若成功则为经过的墙上时钟时间(单位:滴答),若出错则为1

此函数填写由buf指向的tms结构,该结构定义如下:

struct tms {

clock_t tms_utime; /* user CPU time */

clock_t tms_stime; /* system CPU time */

clock_t tms_cutime; /* user CPU time, terminated children */

clock_t tms_cstime; /* system CPU time, terminated children */

};

注意,此结构没有包含墙上时钟时间。作为代替,times函数返回墙上时钟时间作为函数值。此值是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使用其相对值。例如,调用times,保存其返回值。在以后某个时间再次调用times,从新返回的值中减去以前返回的值,此差值就是墙上时钟时间。

所有由此函数返回的clock_t值都用_SC_CLK_TCK (sysconf函数返回的每秒时钟滴答数,见2.5.4)变换成秒数。

实例

程序8 – 1 8将每个命令行参数作为shell命令串执行,对每个命令计时,并打印从tms结构取得的值。按下列方式运行此程序,得到:

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/times.h>



static void pr_times(clock_t, struct tms *, struct tms *);

static void do_cmd(char *);



int main(int argc, char *argv[])

{



int i;



setbuf(stdout, NULL);

for (i = 1; i < argc; i++)

do_cmd(argv[i]); /* once for each command-line arg */

exit(0);

}



static void do_cmd(char *cmd)

{ /* execute and time the "cmd" */

struct tms tmsstart, tmsend;

clock_t start, end;

int status;



printf("ncommand: %sn", cmd);



if ((start = times(&tmsstart)) == -1) /* starting values */

printf("times error");



if ((status = system(cmd)) < 0) /* execute command */

printf("system() error");



if ((end = times(&tmsend)) == -1) /* ending values */

printf("times error");



pr_times(end - start, &tmsstart, &tmsend);

//pr_exit(status);

}



static void

pr_times(clock_t real, struct tms *tmsstart, struct tms *tmsend)

{

static long clktck = 0;



if (clktck == 0) /* fetch clock ticks per second first time */

if ((clktck = sysconf(_SC_CLK_TCK)) < 0)

printf("sysconf error");

printf(" real: %7.2fn", real / (double) clktck);

printf(" user: %7.2fn",

(tmsend->tms_utime -

tmsstart->tms_utime) / (double) clktck);

printf(" sys: %7.2fn",

(tmsend->tms_stime -

tmsstart->tms_stime) / (double) clktck);

printf(" child user: %7.2fn",

(tmsend->tms_cutime -

tmsstart->tms_cutime) / (double) clktck);

printf(" child sys: %7.2fn",

(tmsend->tms_cstime -

tmsstart->tms_cstime) / (double) clktck);

}

运行程序,得到:

$ ./a.out “sleep 5” “date”

command: sleep 5

 real:     5.03

 user:     0.00

 sys:      0.00

 child user:      0.00

 child sys:       0.00

 

command: date

Wed Oct 24 03:57:00 PDT 2012

 real:     0.09

 user:     0.00

 sys:      0.00

 child user:      0.00

 child sys:       0.08

在这个实例中,在child userchild sys行中显示的时间是执行shell和命令的子进程所使用的CPU时间。

8.15 用户标识

8.15 用户标识

任一进程都可以得到其实际和有效用户I D及组I D。但是有时希望找到运行该程序的用户的登录名。我们可以调用getpwuid(getuid( ) ),但是如果一个用户有多个登录名,这些登录名又对应着同一个用户I D,那么又将如何呢?一个人在口令文件中可以有多个登录项,它们的用户I D相同,但登录s h e l l则不同。)系统通常保存用户的登录名(见6 . 7节),用getlogin函数可以存取此登录名。

#include <unistd.h>

char *getlogin(void);

//返回:若成功则为指向登录名字符串的指针,若出错则为NULL

如果调用此函数的进程没有连接到用户登录时所用的终端,则本函数会失败。通常称这些进程为精灵进程(daemon),第1 3章将对这种进程专门进行讨论。

得到了登录名,就可用getpwnam在口令文件中查找相应记录以确定其登录shell等。

8.14 进程会计

2012-10-22Unix环境高级编程进程控制

8.14 进程会计

很多UNIX系统提供了一个选择项以进行进程会计事务处理。当取了这种选择项后,每当进程结束时内核就写一个会计记录。典型的会计记录是3 2字节长的二进制数据,包括命令名、所使用的CPU时间总量、用户ID和组ID、起动时间等。本节将比较译细地说明这种会计记录,这样也使我们得到了一个再次观察进程的机会,得到了使用5 . 9节中所介绍的fread函数的机会。

一个至今没有说明过的函数( acct )起动和终止进程会计。唯一使用这一函数的是accton ( 8 )命令。超级用户执行一个带路径名参数的accton命令起动会计处理。该路径名通常是/ v a r / a d m / p acct(早期系统中为/ u s r / a d m / acct)。执行不带任何参数的accton命令则停止会计处理。

会计记录结构定义在头文件<sys/acct.h>中,其样式如下:

 

typedef  u_short comp_t;   /* 3-bit base 8 exponent; 13-bit fraction */



struct acct

{

char ac_flag; /* flag (see Figure 8.26) */

char ac_stat; /* termination status (signal & core flag only) */

/* (Solaris only) */

uid_t ac_uid; /* real user ID */

gid_t ac_gid; /* real group ID */

dev_t ac_tty; /* controlling terminal */

time_t ac_btime; /* starting calendar time */

comp_t ac_utime; /* user CPU time (clock ticks) */

comp_t ac_stime; /* system CPU time (clock ticks) */

comp_t ac_etime; /* elapsed time (clock ticks) */

comp_t ac_mem; /* average memory usage */

comp_t ac_io; /* bytes transferred (by read and write) */

/* "blocks" on BSD systems */

comp_t ac_rw; /* blocks read or written */

/* (not present on BSD systems) */

char ac_comm[8]; /* command name: [8] for Solaris, */

/* [10] for Mac OS X, [16] for FreeBSD, and */

/* [17] for Linux */

};

会计记录所需的各个数据(各CPU时间、传输的字符数等)都由内核保存在进程表中,并在一个新进程被创建时置初值(例如fork之后在子进程中)。进程终止时写一个会计记录。这就意味着在会计文件中记录的顺序对应于进程终止的顺序,而不是它们起动的顺序。为了确定起动顺序,需要读全部会计文件,并按起动日历时间进行排序。这不是一种很完善的方法,因为日历时间的单位是秒(见1 . 1 0节),在一个给定的秒中可能起动了多个进程。而墙上时钟时间的单位是时钟滴答(通常,每秒滴答数在5 01 0 0之间)。但是我们并不知道进程的终止时间,所知道的只是起动时间和终止顺序。这就意味着,即使墙上时间比起动时间要精确得多,但是仍不能按照会计文件中的数据重构各进程的精确起动顺序。

会计记录对应于进程而不是程序。fork之后,内核为子进程初始化一个记录,而不是在一个新程序被执行时。虽然exec并不创建一个新的会计记录,但相应记录中的命令名改变了,AFORK标志则被清除。这意味着,如果一个进程顺序执行了三个程序(A exec B,B exec C,最后C exit),但只写一个会计记录。在该记录中的命令名对应于程序C,但CPU时间是程序ABC之和。

8.9 竞态条件

8.9 竞态条件

从本书的目的出发,当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞态条件( race condition。如果在fork之后的某种逻辑显式或隐式地依赖于在fork之后是父进程先运行还是子进程先运行,那么fork函数就会是竞态条件活跃的滋生地。通常,我们不能预料哪一个进程先运行。即使知道哪一个进程先运行,那么在该进程开始运行后,所发生的事情也依赖于系统负载以及内核的调度算法。

在程序8 – 5中,当第二个子进程打印其父进程ID时,我们看到了一个潜在的竞态条件。如果第二个子进程在第一个子进程之前运行,则其父进程将会是第一个子进程。但是,如果第一个子进程先运行,并有足够的时间到达并执行exit,则第二个子进程的父进程就是init。即使在程序中调用sleep,这也不保证什么。如果系统负担很重,那么在第二个子进程从sleep返回时,可能第一个子进程还没有得到机会运行。这种形式的问题很难排除,因为在大部分时间,这种问题并不出现。

如果一个进程希望等待一个子进程终止,则它必须调用wait函数。如果一个进程要等待其父进程终止(如程序8 – 5中一样),则可使用下列形式的循环:

while(getppid() !=1)

sleep ( 1 ) ;

这种形式的循环(称为轮询polling))的问题是它浪费了CPU时间,因为调用者每隔1秒都被唤醒,然后进行条件测试。

为了避免竞态条件和轮询,在多个进程之间需要有某种形式的信号机制。在UNIX中可以使用信号机制,在1 0 . 1 6节将说明它的一种用法。各种形式的进程间通信( IPC )也可使用,在第151 7章将对此进行讨论。

在父、子进程的关系中,常常出现下述情况。在fork之后,父、子进程都有一些事情要做。例如,父进程可能以子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。在本例中,要求每个进程在执行完它的一套初始化操作后要通知对方,并且在继续运行之前,要等待另一方完成其初始化操作。这种情况可以描述如下:

#include  "***"

TELL_WAIT(); /* set things up for TELL_xxx & WAIT_xxx */

if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */

/* child does whatever is necessary ... */

TELL_PARENT(getppid()); /* tell parent we're done */
WAIT_PARENT(); /* and wait for parent */

/* and the child continues on its way ... */

exit(0);
}

/* parent does whatever is necessary ... */

TELL_CHILD(pid); /* tell child we're done */
WAIT_CHILD(); /* and wait for child */

/* and the parent continues on its way ... */

exit(0);

程序8 – 6输出两个字符串:一个由子进程输出,一个由父进程输出。因为输出依赖于内核使进程运行的顺序及每个进程运行的时间长度,所以该程序包含了一个竞态条件。程序具有竞争条件的程序。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static void charactatime(char *str)
{
char *ptr;
int c;
setbuf(stdout, NULL); //set unbuffered
for (ptr = str; (c = *ptr++) != 0;)
putc(c, stdout);
}

int main(void)
{
pid_t pid;

if ((pid = fork()) < 0)
perror("fork error");
else if (pid == 0) //child
charactatime("output from childn");
else
charactatime("output from parentn");

exit(0);
}

由于setbuf(stdout, NULL);     //set unbuffered所以每个字符输出都需要调用一次write函数。这样就会导致输出是不可控的。

如果加入我们刚开始讨论的子进程要等父进程操作完成后或者相反的代码,就能得到预期的输出,两个进程的输出不再交叉混合。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static void charactatime(char *str)
{
char *ptr;
int c;
setbuf(stdout, NULL); //set unbuffered,so every character will call write
for (ptr = str; (c = *ptr++) != 0;)
putc(c, stdout);
}

int main(void)
{
pid_t pid;

TELL_WAIT();

if ((pid = fork()) < 0)
perror("fork error");
else if (pid == 0) //child
{
WAIT_PARENT();
charactatime("output from childn");

} else {
charactatime("output from parentn");
TELL_CHILD();
}
exit(0);
}