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.12 system函数

8.12 system函数

在程序中执行一个命令字符串很方便。例如,假定要将时间和日期放到一个文件中,则可使用6 . 9节中的函数实现这一点。调用time得到当前日历时间,接着调用localtime将日历时间变换为年、月、日、时、分、秒、周日形式,然后调用strftime对上面的结果进行格式化处理,最后将结果写到文件中。但是用下面的system函数则更容易做到这一点。

system(“date > file”);

ISO C定义了system函数,但是其操作对系统的依赖性很强。

#include <stdlib.h>

int system(const char *cmdstring) ;

如果cmdstring是一个空指针,则仅当命令处理程序可用时, system返回非0值,这一特征可以决定在一个给定的操作系统上是否支持system函数。在U N I X中,system总是可用的。

因为system在其实现中调用了forkexecwaitpid,因此有三种返回值:

(1) 如果fork失败或者waitpid返回除E I N T R之外的出错,则system返回-1,而且errno中设置了错误类型。

(2) 如果exec失败(表示不能执行shell ),则其返回值如同shell执行了exit ( 1 2 7 )一样。

(3) 否则所有三个函数( fork , execwaitpid )都成功,并且system的返回值是shell的终止状态,其格式已在waitpid中说明。

程序8 – 1 2system函数的一种实现。它对信号没有进行处理。1 0 . 1 8节中将修改此函数使其进行信号处理。

#include    <sys/wait.h>

#include <errno.h>

#include <unistd.h>



int system(const char *cmdstring)

{ /* version without signal handling */

pid_t pid;

int status;



if (cmdstring == NULL)

return (1); /* always a command processor with UNIX */



if ((pid = fork()) < 0) {

status = -1; /* probably out of processes */

} else if (pid == 0) { /* child */

execl("/bin/sh", "sh", "-c", cmdstring, (char *) 0);

_exit(127); /* execl error */

} else { /* parent */

while (waitpid(pid, &status, 0) < 0) {

if (errno != EINTR) {

status = -1; /* error other than EINTR from waitpid() */

break;

}

}

}



return (status);

}

      shell-c选项告诉shell程序取下一个命令行参数,上面是cmdstring作为命令输入而不是从标准输入或从一个给定的文件中读取命令。

      注意,我们调用_exit而不是exit。这是为了防止任何一个标准IO缓冲区(这些缓存区会在fork中由父进程复制到子进程)在子进程中被冲洗。

      下述程序8-13system的这种版本进行了测试。

#include <stdio.h>

#include <stdlib.h>

#include <sys/wait.h>



void pr_exit(int status)

{

if (WIFEXITED(status))

printf("normal termination, exit status = %dn",

WEXITSTATUS(status));

else if (WIFSIGNALED(status))

printf("abnormal termination, signal number = %d%sn",

WTERMSIG(status),

#ifdef WCOREDUMP

WCOREDUMP(status) ? " (core file generated)" : "");

#else

"");

#endif

else if (WIFSTOPPED(status))

printf("child stopped, signal number = %dn",

WSTOPSIG(status));

}





int main(void)

{

int status;



if ((status = system("date")) < 0)

printf("system() error");

pr_exit(status);



if ((status = system("nosuchcommand")) < 0)

printf("system() error");

pr_exit(status);



if ((status = system("who; exit 44")) < 0)

printf("system() error");

pr_exit(status);



exit(0);

}

程序运行输出为:

Tue Oct 23 00:32:42 PDT 2012

normal termination, exit status = 0

sh: nosuchcommand: not found

normal termination, exit status = 127

leo      tty7         2012-10-23 00:29 (:0)

leo      pts/0        2012-10-23 00:31 (:0.0)

normal termination, exit status = 44

      使用system而不是直接使用forkexec的优点是:system进行了所需的各种出错处理,以及各种信号处理。

设置用户ID程序

      如果在一个设置用户ID程序中调用system,会产生一个安全性方面的漏洞。绝不应当这样做。

      程序8-14system执行命令行参数

#include <stdio.h>

#include <stdlib.h>

#include <sys/wait.h>



void pr_exit(int status)

{

if (WIFEXITED(status))

printf("normal termination, exit status = %dn",

WEXITSTATUS(status));

else if (WIFSIGNALED(status))

printf("abnormal termination, signal number = %d%sn",

WTERMSIG(status),

#ifdef WCOREDUMP

WCOREDUMP(status) ? " (core file generated)" : "");

#else

"");

#endif

else if (WIFSTOPPED(status))

printf("child stopped, signal number = %dn",

WSTOPSIG(status));

}





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

{

int status;



if (argc < 2)

printf("command-line argument required");



if ((status = system(argv[1])) < 0)

printf("system() error");

pr_exit(status);



exit(0);

}

程序8-15打印实际和有效用户ID

#include <stdlib.h>

#include <stdio.h>



int main(void)

{

printf("real uid = %d, effective uid = %dn", getuid(), geteuid());

exit(0);

}

      如果一个进程正以特殊的权限运行(设置用户ID或者设置组ID),它又想生成另一个进程执行另一个程序,则它应当直接使用forkexec,而且在fork之后,exec之前要改回到普通权限。设置用户ID或者设置组ID程序决不应调用system函数。这种警告的一个理由是:system调用shell对命令字符串进行语法分析,而shell使用IFS变量作为其输入字段分隔符。早期的shell版本在被调用时不将此变量恢复为普通字符集。这就允许一个有恶意的用户在调用system之前设置IFS,造成system执行一个不同的程序。其中IFSInternal Field Seperator)Linuxshell中预设的分隔符,用来把command line分解成word(字段)。IFS可以是White Space(空白键)、Tab( 表格键)、Enter( 回车键)中的一个或几个。

8.11 解释器文件

8.11 解释器文件

这种文件是文本文件,其起始行的形式是:

#pathname [optional- a rg u m e nt]

在惊叹号和pathname之间的空格是可任选的。最常见的是以下列行开始:

#/bin/sh

pathname通常是个绝对路径名,对它不进行什么特殊的处理(不使用PAT H进行路径搜索)。对这种文件的识别是由内核作为exec系统调用处理的一部分来完成的。内核使调用exec函数的进程实际执行的文件并不是该解释器文件,而是在该解释器文件的第一行中pathname所指定的文件。一定要将解释器文件(文本文件,它以#!开头)和解释器(由该解释器文件第一行中的pathname指定)区分开来。

很多系统对解释器文件第一行有长度限制。这包括#!、pathname、可选参数以及空格数。

是否一定需要解释器文件呢?那也不完全如此。但是它们确实使用户得到效率方面的好处,其代价是内核的额外开销(因为内核需要识别解释器文件)。由于下述理由,解释器文件是有用的:

(1) 某些程序是用某种语言写的脚本,这一事实可以隐藏起来。例如,为了执行程序8 – 11,只需使用下列命令行:

awkexample optional- arguments

而并不需要知道该程序实际上是一个awk脚本,否则就要以下列方式执行该程序:

awk -f awkexample optional- arguments

(2) 解释器脚本在效率方面也提供了好处。再考虑一下前面的例子。仍旧隐藏该程序是一个awk脚本的事实,但是将其放在一个shell脚本中:

awk ‘BEGIN {

for (i = 0; i < ARGC; i++)

printf “ARGV[%d] = %sn”, i, ARGV[i]

exit

}’ $*

这种解决方法的问题是要求做更多的工作。首先, shell读此命令,然后试图execlp此文件名。因为shell脚本是一个可执行文件,但却不是机器可执行的,于是返回一个错误, execlp就认为该文件是一个shell脚本(它实际上就是这种文件)。然后执行/bin/sh,并以该shell脚本的路径名作为其参数。shell正确地执行我们的shell脚本,但是为了运行awk程序,它调用fork , execwait。用一个shell脚本代替解释器脚本需要更多的开销。

(3) 解释器脚本使我们可以使用除/bin/sh以外的其他shell来编写shell脚本。当execlp找到一个非机器可执行的可执行文件时,它总是调用/bin/sh来解释执行该文件。但是,用解释器脚本,则可编写成:

# ! / bin / csh

(在解释器文件中后随C shell脚本)

再一次,我们也可将此放在一个/bin/sh脚本中(然后由其调用C shell),但是要有更多的开销。如果三个shellawk没有用#作为注释符,则上面所说的都无效。