《UNUX环境高级编程》(8)进程控制

1、引言

2、进程标识

  • 每个进程都用一个唯一的非负整数标识,即为进程id:pid。进程ID是可以复用的,当一个进程终止时,其进程ID就可以用来标识其他进程。
  • 系统中有一些专用进程:
    • 进程ID为0的是调度进程,也称交换进程(swapper),它并不执行任何磁盘上的程序,它是内核中的系统进程;
    • 进程ID为1的是init进程,此进程负责在自举内核后启动一个UNIX系统,init进程绝不会终止,它是一个以超级用户特权运行的普通用户进程;init通常读取与系统有关的初始化文件(/etc/rc*文件或/etc/inittab文件,以及在/etc/init.d中的文件)并将系统引导到一个状态(如多用户)。此外,init是所有孤儿进程的父进程。
    • 进程ID为2的是页守护进程,负责支持虚拟存储器系统的分页操作。
  • 通过以下函数获取一个进程的pid等id信息:
    pid_t getpid(void);  // 调用进程pid
    pid_t getppid(void); // 调用进程的父进程pid
    uid_t getuid(void);  // 调用进程的实际用户id
    uid_t geteuid(void); // 调用进程的有效用户id
    gid_t getgid(void);  // 调用进程的实际组id
    gid_t getegid(void); //调用进程的有效组id
    
    注意:这些函数都没有出错返回。

3、函数fork

  • 一个现有进程可以通过调用fork复刻一个新进程

    pid_t fork(void);
    
    • fork函数调用一次返回两次。对于子进程返回值是0;对于父进程返回值是子进程pid。子进程只能有一个父进程,通过getppid获得父进程pid。
    • 子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本,子进程获得父进程的数据空间、堆和栈的副本。注意这是子进程所拥有的副本,父子进程并不共享这些存储空间部分。父进程和子进程共享正文段.text。
    • 由于fork之后进程跟着exec,所以现在很多fork实现并不执行父进程数据段、堆和栈的完全副本,而是使用写时复制(copy-on-write)这些区域由父子进程共享,并且内核将它们的访问权限修改为只读。如果父子进程中的一个试图修改这些区域,则内核只为修改区域的那块内存制作副本。
  • 实例:演示了一个fork函数,可以看出子进程对变量所做的改变并不影响父进程中该变量的值

    #include "apue.h"
    
    int		globvar = 6;		/* external variable in initialized data */
    char	buf[] = "a write to stdout\n";
    
    int
    main(void)
    {
    	int		var;		/* automatic variable on the stack */
    	pid_t	pid;
    
    	var = 88;
    	/*
    		注意sizeof和strlen的区别:
    		1)strlen需要进行一次系统调用,它计算的是不包含终止null字节的字符串长度。
    		2)sizeof计算包含null字节的字符串长度,sizeof是在编译时计算缓冲区长度,也就是说它不需要进行系统调用。
    	*/
    	if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1) /*write函数是不带缓冲的,其数据写道标准输出一次*/
    		err_sys("write error");
    	/*
    		printf属于标准I/O库,是带缓冲的。如果标准输出连接到终端设备,则它是行缓冲的,否则它是全缓冲的。
    		这一点会影响到冲洗数据的时机。
    	*/
    	printf("before fork\n");	/* we don't flush stdout */
    
    	if ((pid = fork()) < 0) {
    		err_sys("fork error");
    	} else if (pid == 0) {		/* fork函数调用一次返回两次,若返回的是0,则为子进程 */
    		globvar++;				/* 子进程对数据进行更改,通过命令行可以发现子进程的变量值改变了,而父进程没有 */
    		var++;
    	} else { /* fork函数调用一次返回两次,若返回的是子进程的PID,结果>0,为父进程 */
    		sleep(2);/* 父进程是自己睡眠2s,目的是让子进程先执行,但并不能保证2s是足够的 */
    	}
    
    	printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
    	  var);
    	exit(0);
    }
    

    命令行输出结果:

    lh@LH_LINUX:~/桌面/apue.3e/proc$ ./fork1
    a write to stdout
    before fork
    pid = 3638, glob = 7, var = 89
    pid = 3637, glob = 6, var = 88
    lh@LH_LINUX:~/桌面/apue.3e/proc$ ./fork1 > result.txt
    lh@LH_LINUX:~/桌面/apue.3e/proc$ cat result.txt 
    a write to stdout
    before fork
    pid = 3650, glob = 7, var = 89
    before fork
    pid = 3649, glob = 6, var = 88
    
    • fork之后是父进程先执行还是子进程先执行是不确定的,取决于内核的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。
    • 根据命令行输出结果我们可以发现:
      • 当以交互方式运行程序时,标准输出连接至终端设备,此时为行缓冲,标准输出缓冲区由换行符\n冲洗,最后只得到printf输出的行一次。
      • 当将标准输出重定向到一个文件时,此时为全缓冲,当调用fork后,该行数据仍在缓冲区,然后在将父进程数据空间复制到子进程中时,该缓冲区数据也被复制到子进程中,此时父进程和子进程各自有了带该行内容的缓冲区。后面的语句printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,var);将数据追加到已有的缓冲区中。当每个进程终止时(exit()会执行标准I/O清理程序),其缓冲区的内容都被写到相应文件中。
  • 文件共享

    • 父进程的所有打开文件描述符都被复制到子进程中,就好像执行了dup函数。父进程和子进程每个相同的打开描述符共享一个文件表项。如下图所示。
      在这里插入图片描述
    • 可以看出,fork之后的父子进程每个相同的打开描述符共享一个文件表项,因此也共享相同的文件偏移量(读写指针)
    • 除了打开文件之外,父进程的很多其他属性也由子进程继承
      • 实际用户ID、实际组ID、有效用户ID、有效组ID
      • 附属组ID
      • 进程组ID
      • 会话ID
      • 控制终端
      • 设置用户ID标志和设置组ID标志
      • 当前工作目录
      • 根目录
      • 文件模式创建屏蔽字
      • 信号屏蔽和安排
      • 对任一打开文件描述符的执行时关闭标志(close-on-exec)
      • 环境
      • 连接的共享存储段
      • 存储映像
      • 资源限制
    • 父子进程的区别
      • fork返回值不同
      • 进程ID不同
      • 子进程的tms_utimetms_stimetms_cutimetms_ustime值设置为0
      • 子进程不继承父进程设置的文件锁。
      • 子进程的未处理闹钟被清除。
      • 子进程的未处理信号集设置为空集。
  • fork失败原因

    • 系统中已经有了太多的进程
    • 该实际用户ID的进程总数超过了系统限制
  • fork常用以下方法

    • 一个父进程希望复刻自己,使父进程和子进程同时执行不同的代码段。在网络服务器中较为常见:父进程等待客户端的服务请求,请求到达时fork使子进程处理此请求,父进程则继续等待下一个服务请求。
    • 一个进程要执行一个不同的程序,shell常使用这种方式。子进程从fork返回后立即调用exec。有些操作系统将fork之后立刻exec组合成一个操作:spawn,但是UNIX将这两个操作分开,因为子进程可以在fork和exec之间更改自己的属性。

4、函数vfork(不推荐使用)

  • vfork函数调用方式和fork相同,但语义不同。现在不应该使用这个函数
  • vfork用于创建一个新进程,而该新进程的目的就是exec一个新程序。但是它不会将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit)。不过在子进程exec或exit之前,它在父进程的空间中运行。这种方式提高了效率,但是如果子进程修改了数据(子进程修改数据会影响到父进程,因为共享存储空间)、进行函数调用、或者没有调用exec或exit就返回可能会带来未知结果。
  • 并且vfork保证子进程先运行,在子进程调用exec或exit之后父进程才可能被调度运行。即子进程调用这两个函数中的任意一个时,父进程会恢复运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步操作,会导致死锁)。
  • 实例:vfork函数的使用
    int main(int argc, char* argv[]) {
        int num = 1;
        if(vfork() == 0) { // 子进程
            cout << "子进程执行" << endl;
            num ++;
            cout << "子进程终止" << endl;
            _exit(0);
        } else { // 父进程
            cout << "父进程执行" << endl;
            cout << "num : " << num << endl;
            cout << "父进程终止" << endl;
        }
    }
    
    命令行输出
    $ ./a.out 
    > 子进程执行
    > 子进程终止
    > 父进程执行
    > num : 2
    > 父进程终止
    
    可以看出子进程修改变量值影响了父进程中该变量,这是因为父子进程共享存储空间。

5、函数exit

  • 进程有5种正常终止和3种异常终止。其中正常终止:

    • main函数内执行return语句,等效于调用exit
    • 调用exit函数,此操作调用终止处理程序(atexit登记的函数),关闭所有标准I/O流,然后调用_exit
    • 调用_exit或_Exit。其操作提供一种无需运行终止处理程序或信号处理程序而终止的方法。(exit是标准C库中的一个函数,_exit是一个系统调用)
    • 进程的最后一个线程在启动例程中执行return语句。但是,该线程返回值不用做进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回
    • 进程的最后一个线程调用pthread_exit函数。此时进程终止状态总是0,这与传给pthread_exit参数无关
  • 3种异常终止:

    • 调用abort。它产生SIGABRT,这是下一种异常终止的特例
    • 当进程接到某种信号时。信号可由进程自身(如调用abort函数)、其他进程或内核产生。
    • 最后一个线程对“取消”请求做出响应。默认情况下“取消”以延迟方式发生:一个线程要求取消另一个线程,若干时间之后,目标线程终止。
  • 不管进程是以何种方式终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开标识符,释放它所使用的存储器等。

  • 希望子进程能够通知其父进程它是如何终止的。可以通过将退出状态作为参数传递给三个终止函数(exit、_exit、_Exit)。如果是异常终止,则不再由这三个函数的参数决定其终止状态,而是内核(不是进程本身)产生一个表明其异常终止原因的终止状态。 在任何情况下,父进程都能够通过wait或waitpid取得其终止状态。

  • 退出状态和终止状态区别:

    • 退出状态是传递给三个终止函数的参数,或main的返回值。
    • 在调用_exit时,内核将退出状态转换成终止状态。
  • 如果父进程在子进程之前终止,那么这些子进程的父进程会改变为init进程:当一个进程终止时,内核逐个检查所有活动进程,以判断他是否是正要终止进程的子进程,如果是,则该进程的父进程ID改为1,保证了每个进程都有一个父进程。

  • 内核为每个终止子进程保存了一定量的信息,父进程调用wait或waitpid时可以得到这些信息(至少包括进程ID、该进程的终止状态、该进程使用的CPU时间总量)。一个已经终止,但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息并释放它仍占用的资源)的进程被称为僵尸进程。 即没有被父进程wait的终止子进程都是僵尸进程。

  • 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个等信息。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。

  • 被init进程收养的进程终止不会变成僵尸进程:只要有一个进程终止,init进程就会调用一个wait函数取得其终止状态,防止了系统中塞满僵尸进程。

6、函数wait和waitpid

  • 当一个进程终止(无论正常异常),内核会向其父进程发送SIGCHLD信号(对这种信号的系统默认动作是忽略它)

  • 调用waitwaitpid会发生

    • 如果其所有子进程都还在运行,则函数阻塞
    • 如果一个子进程已经终止,正等待父进程获取其终止状态,则取得该子进程终止状态立即返回。
    • 如果它没有任何子进程,立即出错返回
    • 如果进程由于接到SIGCHLD信号而调用wait,那么期望wait会立即返回。但是如果在随机事件调用wait,则进程可能会阻塞。
    pid_t wait(int *status);
    pid_t waitpid(pid_t pid, int *status, int options);
    //成功返回子进程PID,出错返回0、-1
    
  • waitwaitpid区别

    • 在一个子进程终止前,wait使其调用者阻塞,而waitpid可以通过选项使调用者不阻塞。
    • waitpid可以通过选项控制它所等待的进程是哪个或哪些
  • 这两个函数的参数status如果不为空,则会将终止进程的终止状态保存在其中。

  • 注意,这个status保存的终止状态是多个信息的集合。其中某些位表示退出状态(正常返回),有些位表示信号编号(异常返回),有一位指示是否产生core文件等。可以通过以下宏查看该返回的整形状态字。

    说明
    WIFEXITED(status)若为正常终止,则为真。这种情况下可以通过WEXITSTATUS(status)宏获取子进程传给exit或_exit参数的低8位
    WIFSIGNALED(status)若为异常终止(收到一个不捕捉的信号),则为真。可以通过WTERMSIG(status)获取使子进程终止的信号编号。另外有些实现定义了WCOREDUMP(status)宏,返回是否产生终止进程的core文件
    WIFSTOPPED(status)若为当前暂停子进程的返回状态则为真,这种情况下可以通过WSTOPSIG(status)获取使子进程暂停的信号编号
    WIFCONTINUED(status)若在作业控制暂停后已经继续的子进程返回了状态,则为真
  • 实例:函数pr_exit使用上表的宏打印进程状态的说明。

    void pr_exit(int status)
    {
    	if(WIFEXITED(status))
    		printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
    	else if(WIFSIGNALED(status))
    		printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status),
    #ifdef WCOREDUMP
    		WCOREDUMP(status) ? " (core file generated)" : "");
    #else
    		"");
    #endif	
    	else if(WIFSTOPPED(status))
    		printf("child stopped, signal number = %d\n", WSTOPSIG(status));
    }
    
  • 实例:调用pr_exit函数,演示终止状态的各种值

    #include "apue.h"
    #include <sys/wait.h>
    
    int
    main(void)
    {
    	pid_t	pid;
    	int		status;
    
    	if ((pid = fork()) < 0)
    		err_sys("fork error");
    	else if (pid == 0)				/* child */
    		exit(7);
    
    	if (wait(&status) != pid)		/* wait for child */
    		err_sys("wait error");
    	pr_exit(status);				/* and print its status */
    
    	if ((pid = fork()) < 0)
    		err_sys("fork error");
    	else if (pid == 0)				/* child */
    		abort();					/* generates SIGABRT */
    
    	if (wait(&status) != pid)		/* wait for child */
    		err_sys("wait error");
    	pr_exit(status);				/* and print its status */
    
    	if ((pid = fork()) < 0)
    		err_sys("fork error");
    	else if (pid == 0)				/* child */
    		status /= 0;				/* divide by 0 generates SIGFPE */
    
    	if (wait(&status) != pid)		/* wait for child */
    		err_sys("wait error");
    	pr_exit(status);				/* and print its status */
    
    	exit(0);
    }
    

    在命令行中执行得到:

    lh@LH_LINUX:~/桌面/apue.3e/proc$ ./wait1 
    normal termination, exit status = 7
    abnormal termination, signal number = 6 (core file generated)
    abnormal termination, signal number = 8 (core file generated)
    

    可以通过查看<signal.h>头文件验证SIGABRT的值为6SIGFPE的值为8

  • 任何一个子进程终止都会导致wait函数返回,但是有时候我们希望获取指定子进程的终止状态。waitpid通过其pid参数完成此功能:

    • pid==-1:等待任一子进程,waitpid=wait
    • pid>0:等待进程idpid的子进程
    • pid==0:等待组id等于调用进程组id的任一子进程
    • pid<-1:等待组id等于pid绝对值的任一子进程
  • 参数options能进一步控制waitpid的操作,它是0或者以下宏的按位或

    • WCONTINUED:若支持作业控制,那么由pid指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态
    • WNOHANG:若子进程还未终止,则waitpid不阻塞返回0
    • WUNTRACED:若支持作业控制,而由pid指定的任一子进程已处于停止状态,并且其状态自停止以来还未报告过,则返回其状态。
    #include "apue.h"
    #include <sys/wait.h>
    
    int
    main(void)
    {
    	pid_t	pid;
    
    	if ((pid = fork()) < 0) {
    		err_sys("fork error");
    	} else if (pid == 0) {		/* first child */
    		if ((pid = fork()) < 0)
    			err_sys("fork error");
    		else if (pid > 0)
    			exit(0);	/* parent from second fork == first child */
    
    		/*
    		 * We're the second child; our parent becomes init as soon
    		 * as our real parent calls exit() in the statement above.
    		 * Here's where we'd continue executing, knowing that when
    		 * we're done, init will reap our status.
    		 */
    		sleep(2);//调用sleep保证在打印父进程ID时第一个子进程已经终止
    		/*第二个子进程的父进程原本是第一个子进程,但是第一个子进程已经终止,所以该进程已经变成了孤儿进程,被init进程收养,其ID为1*/
    		printf("second child, parent pid = %ld\n", (long)getppid()); 
    		exit(0);
    	}
    
    	if (waitpid(pid, NULL, 0) != pid)	/* wait for first child */
    		err_sys("waitpid error");
    
    	/*
    	 * We're the parent (the original process); we continue executing,
    	 * knowing that we're not the parent of the second child.
    	 */
    	exit(0);
    }
    

    在命令行中执行得到:

    lh@LH_LINUX:~/桌面/apue.3e/proc$ ./fork2
    lh@LH_LINUX:~/桌面/apue.3e/proc$ second child, parent pid = 1584
    

7、waitid函数

  • 此函数类似于waitpid,但是提供了更多的灵活性
    int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
    
    waitid也允许等待指定的子进程,但是它使用两个单独的参数表示要等待的子进程类型(不像waitpid函数只用一个参数表示)。其中idtype指定子进程类型,id则指定id数值
    • 其中idtype参数值:
      • P_PID:等待指定进程,id指定要等待的子进程pid
      • P_PGID:等待特定进程组的任一子进程,id指定进程组id
      • P_ALL:等待任一子进程,忽略id
    • options参数是以下标志按位或运算,其中必须包括WCONTINUED/WEXITED/WSTOPPED之一
      • WCONTINUED:若支持作业控制,那么由pid指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态
      • WEXITED:等待已经退出的进程
      • WNOHANG:若子进程还未终止,则waitpid不阻塞立即返回
      • WNOWAIT:不破坏子进程退出状态,该子进程可由后续的wait、waitid或waitpid调用取得
      • WSTOPPED:若支持作业控制,该子进程已处于停止状态,并且其状态自停止以来还未报告过,则返回其状态。
    • infop参数指向siginfo结构体,该结构包含了造成子进程状态改变有关信号的详细信息。

8、函数wait3和wait4

  • 也可以通过这两个函数获取子进程终止状态。它们提供的功能比waitwaitpidwaitid多一个。这两个函数的一个参数允许内核返回终止进程及其所有子进程使用资源概况(shell就是用wait4函数获取终止子进程信息)。
    pid_t wait3(int *wstatus, int options, struct rusage *rusage);
    pid_t wait4(pid_t pid, int *wstatus, int options, struct rusage *rusage);
    
    些使用资源包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。

9、竞争条件

  • 当多个进程都企图对共享数据进行处理,而最后的处理结果又取决于进程运行的顺序时,我们认为发生了竞态条件

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

    while(getppid() != 1) { //若父进程为init进程则说明原本父进程终止了
        sleep(1);
    }
    

    这种形式的循环为轮询,它浪费了CPU时间,因为调用进程每隔1s被唤醒然后进行条件测试。

  • 为了避免竞争条件和轮询,多个进程之间可以通过信号的方式解决。或者使用各种形式的进程间通信(IPC)

  • 实例1:产生了竞争条件的例子

    #include "apue.h"
    
    static void charatatime(char *);
    
    int
    main(void)
    {
    	pid_t	pid;
    
    	if ((pid = fork()) < 0) {
    		err_sys("fork error");
    	} else if (pid == 0) {
    		/*子进程输出*/
    		charatatime("output from child\n");
    	} else {
    		/*父进程输出*/
    		charatatime("output from parent\n");
    	}
    	exit(0);
    }
    
    static void
    charatatime(char *str)
    {
    	char	*ptr;
    	int		c;
    	/*为什么设置标准输出为无缓冲:可以更好的演示竞争条件*/
    	setbuf(stdout, NULL);			/* set unbuffered */
    	for (ptr = str; (c = *ptr++) != 0; )
    		putc(c, stdout);
    }
    

    命令行输出:

    lh@LH_LINUX:~/桌面/apue.3e/proc$ ./tellwait1
    output from parent
    output from child
    lh@LH_LINUX:~/桌面/apue.3e/proc$ ./tellwait1
    output from poaurtepnutt
     from child
    lh@LH_LINUX:~/桌面/apue.3e/proc$ ./tellwait1
    outputo furtopmu tp afrreomn tc
    hild
    

    可以看到,这里尝试了多3次,后面两次可以很明显地看出产生了竞争,当然竞争的结果是不确定,也可能在输出结果上无法看出父进程输出与子进程输出产生了竞争。

  • 实例2:排除了竞争条件的例子

    #include "apue.h"
    
    static void charatatime(char *);
    
    int
    main(void)
    {
    	pid_t	pid;
    
    	TELL_WAIT();
    	
    	if ((pid = fork()) < 0) {
    		err_sys("fork error");
    	/* 这不就是生产者-消费者模型么,下面演示的是父进程由于子进程优先的情况,当然也可模拟倒转过来的情况 */
    	} else if (pid == 0) {
    		WAIT_PARENT();		/* parent goes first */
    		charatatime("output from child\n");
    	} else {
    		charatatime("output from parent\n");
    		TELL_CHILD(pid); /* child gose second */
    	}
    	exit(0);
    }
    
    static void
    charatatime(char *str)
    {
    	char	*ptr;
    	int		c;
    
    	setbuf(stdout, NULL);			/* set unbuffered */
    	for (ptr = str; (c = *ptr++) != 0; )
    		putc(c, stdout);
    }
    

    运行该程序可以保证得到预期的输出,并且两个进程的输出不再交叉混合。

10、exec函数

  • 当使用fork创建新的子进程后,如果要执行其他程序则需要调用exec函数。exec将该进程执行的程序完全替换为新程序,并且从main函数开始执行。因为exec并不创建新进程,所以前后的pid不变exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆和栈
  • exec是一个函数集合,包括了七个函数
    /*这7个函数只有execve是内核的系统调用,另外6个只是库函数*/
    int execl(const char *path, const char *arg, .../* (char  *) NULL */);
    int execv(const char *path, char *const argv[]);
    int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
    int execve(const char *path, char *const argv[], char *const envp[]);
    int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
    int execvp(const char *file, char *const argv[]);
    int fexecve(int fd, char *const argv[], char *const envp[]);
    //这七个函数出错返回-1,成功不返回
    
  • 这些函数区别:
    • 前四个函数取路径名为参数,后两个函数则取文件名作为参数,最后一个取文件描述符为参数。
      • 当指定路径名path为参数时,替换执行的程序为路径所表示的程序文件
      • 当指定文件名file作为参数时,若file中带有/,则将其视为路径名(不在PATH中寻找);若仅仅是文件名,则按PATH环境变量在它所指定的各目录中搜索可执行文件。
        • 注意,如果要运行/…/a/b/x.out可执行文件,并且PATH=/…/a/,当输入x.out无法在PATH中找到该文件(因为/…/a/中没有x.out文件,也不会递归查找/…/a/的子目录下的文件);输入b/x.out则根本不尝试在PATH中查找,而是以当前工作目录作为相对路径的起始路径,在该相对路径下查找x.out文件。这也是为什么file参数中带有/不会在PATH中寻找而是直接视为路径名的原因。
      • 这一点也可以从函数名看出,带p的函数名都是使用文件名作为参数,并在PATH中寻找该文件的。
      • 当使用PATH路径前缀中找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该文件名作为shell的输入。
    • 第二个区别是与参数表的传递有关,从函数名可以看出来。l表示列表listv表示矢量vector
      • 使用lexec函数要求将每个命令行参数用作一个单独的参数,这些参数以NULL结尾
      • 使用vexec函数则应先构造一个指向各参数的指针数组,然后将这个指针数组地址用作函数参数。
    • 第三个区别于向新程序传递环境表有关。
      • e结尾的exec函数可以传递一个指向环境字符串指针数组的指针(即新程序的环境表设置为该参数指定的环境表)
      • 没有e的则使用调用进程中的environ变量为新程序复制现有环境,因此新程序与调用进程有相同环境表
  • 一些补充
    • PATH变量包含了一张目录表(称为路径前缀),目录之间用冒号(:)分隔。例如,下列name=value环境字符串指定在4个目录进行搜索。(注意回忆全局变量environPATH变量的关系)
      PATH=/bin:/usr/bin:/usr/local/bin:.
      
      最后的后缀.表示当前目录。(零长前缀也表示当前目录。在value的开始处可以用:表示,在和那个中间则要用::表示,在行尾以:表示)
    • 在使用ISO C原型之前,对execlexecleexeclp三个函数表示命令行参数的一般方法是:
      char *pathname,char *arg0,...,char *argn,(char*)0
      
      这种语法显示地说明了最后一个命令行参数为空指针。如果用常量0来表示一个空指针,则必须将它转换为一个指针;否则它将被解释为整形参数。
  • 执行exec后,进程ID没有改变。新程序从调用进程继承了下列属性
    • 进程pid和父进程pid
    • 实际用户ID和实际组ID
    • 附属组ID
    • 进程组ID
    • 会话ID
    • 控制终端
    • 闹钟尚余留的时间
    • 当前工作目录
    • 根目录
    • 文件模式创建屏蔽字
    • 文件锁
    • 进程信号屏蔽
    • 未处理信号
    • 资源限制
    • 友好值
    • tms_utime、tms_stime、tms_cutime、tms_cstime
  • 对打开文件的处理和文件描述符标志close-on-exec有关。若设置了该标志,则在执行exec时关闭该描述符,否则该描述符仍打开。除非调用fcntl设置了该标志,否则系统默认操作是exec后仍然保持这种描述符打开。
  • exec会关闭打开的目录流(即通过opendir函数打开的目录文件),这种操作其实是opendir自己实现的,opendir会调用fcntl设置打开的目录文件的文件描述符标志位close-on-exec
  • 有效ID是否改变取决于所执行的新程序文件的设置用户ID位和设置组ID位。如果新程序的设置用户ID位已设置,则有效用户ID变成新程序文件的所有者ID,否则有效用户ID继承不变。对组ID的处理方式相同。
  • 在很多UNIX实现中,这7个函数只有execve是内核的系统调用,另外6个只是库函数,它们最终都会调用该系统调用。这7个函数之间的关系如图所示。(特别重要!!!会这张图才证明理解透彻了!!!)
    在这里插入图片描述
  • fexecve会把文件描述符参数转换成路径名,然后用execve用该路径名去执行程序。
  • 通过表格对照区分七个函数的区别
    在这里插入图片描述
  • 实例:演示exec函数,这里使用了execleexeclp函数
    #include "apue.h"
    #include <sys/wait.h>
    
    char	*env_init[] = { "USER=unknown", "PATH=/tmp", NULL };
    
    int
    main(void)
    {
    	pid_t	pid;
    
    	if ((pid = fork()) < 0) {
    		err_sys("fork error");
    	} else if (pid == 0) {	/* specify pathname, specify environment */
    		/*要求路径名、参数表和特定的环境*/
    		if (execle("/home/sar/bin/echoall", "echoall", "myarg1",
    				"MY ARG2", (char *)0, env_init) < 0)
    			err_sys("execle error");
    	}
    
    	if (waitpid(pid, NULL, 0) < 0)
    		err_sys("wait error");
    
    	if ((pid = fork()) < 0) {
    		err_sys("fork error");
    	} else if (pid == 0) {	/* specify filename, inherit environment */
    		/* 要求文件名、参数表和特定的环境,该函数能够调用是因为目录/home/sar/bin是当前路径前缀之一 */
    		if (execlp("echoall", "echoall", "only 1 arg", (char *)0) < 0)
    			err_sys("execlp error");
    	}
    
    	exit(0);
    }
    
    • 注意,我们将第一个参数(新程序中的argv[0])设置为路径名的文件名分量。某些shell习惯将此参数设置为完全的路径名,当然这只是惯例而已,不是必须要这样。当login命令执行shell的时候,login在argv[0]之前加一个/作为前缀,这向shell指明它是作为登录shell被调用的。登录shell将执行启动配置文件(start-up profile)命令,而非登录shell不会执行这些命令。

    • 下面是上面程序需要执行的echoall程序

      #include "apue.h"
      
      int
      main(int argc, char *argv[])
      {
      	int			i;
      	char		**ptr;
      	extern char	**environ;
      
      	for (i = 0; i < argc; i++)		/* echo all command-line args */
      		printf("argv[%d]: %s\n", i, argv[i]);
      
      	for (ptr = environ; *ptr != 0; ptr++)	/* and all env strings */
      		printf("%s\n", *ptr);
      
      	exit(0);
      }
      
    • 执行实例程序可以得到

      $ ./a.out
      argv[0]:echoall
      argv[1]:myarg1
      argv[2]:MY ARG2
      USER=unknown
      PATH=/tmp
      argv[0]:echoall
      $ argv[1]:only 1 arg
      SHELL=/bin/bash
      ...还有很多没有列举
      HOME=/home/sar
      

补充A:shell运行程序流程

  • shell运行程序流程入下
    1. 读取用户由键盘输入的命令行。
    2. 分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve()内部处理所要求的形式
    3. 终端进程调用fork()建立一个子进程。
    4. 终端进程本身用系统调用wait4()来等待子进程完成(如果是后台命令,则不等待)。当子进程运行时调用execve(),子进程根据文件名(即命令名)到目录中查找有关文件(这是命令解释程序构成的文件),将它调入内存,执行这个程序(解释这条命令)。
    5. 如果命令末尾有&号(后台命令符号),则终端进程不用系统调用wait4( )等待,立即发提示符,让用户输入下一个命令,转。如果命令末尾没有&号,则终端进程要一直等待,当子进程(即运行命令的进程)完成处理后终止,向父进程(终端进程)报告,此时终端进程醒来,在做必要的判别等工作后,终端进程发提示符,让用户输入新的命令,重复上述处理过程。
      在这里插入图片描述
  • shell如何启动?
    • shell是用户和Linux内核之间的接口程序,为用户提供使用操作系统的接口。它接收用户命令,然后调用相应的应用程序
    • shell在你成功地登录进入系统后启动,并始终作为你与系统内核的交互手段直至你退出系统

11、更改用户ID和更改组ID

  • 什么是有效用户id实际用户id设置用户id保存的设置用户id

    • 实际用户id(RUID,进程特有的概念): 在开机时,你输入的账号的对应id就是实际用户uid,说白了就是登录号,站在用户的角度上看。
    • 有效用户id(EUID,进程特有的概念): 一般和RUID相同,站在操作系统的角度上看,用于给操作系统判断某个进程是否拥有操作某个文件的权限。
    • 设置用户id(SUID,set user id,文件特有的概念): 站在文件的角度上看,文件权限有r-w-x-s-t类型,其中的s表示SUID是打开状态(即可用状态)。如果一个文件的权限位里面没有s,则表示SUID是关闭状态(即不可用状态)。注意:SUID只能用于可执行文件,其作用是修改EUID(有效用户id)
    • 保存的设置用户id(SSUID,saved SUID,《Unix高级环境编程 第3版》的8.11节里面定义的,进程特有的概念),也有博客称为保留的设置用户id: EUID(有效用户id)的备份,既然是备份,则应该是恢复的作用。SSUID由linux的exec()函数保存,可以把SSUID理解为exec()函数里面的一个局部变量。exec()的执行逻辑大概如下(自己猜测的,为了好理解SSUID的作用):
      // 这里假设 "可执行文件"为/etc/passwd, 该文件的权限位-rwsr-xr-x,文件的拥有者是root
      void exec("可执行文件") {
      	SSUID = EID;
      	进程的EID = 可执行文件用户(即root用户);
      	执行"可执行文件";
      	//此时调用本函数的进程已经拥有root权限了
      	EID = SSUID;
      }
      
    • 文件特有的概念
      • FUID(file uid): 文件的拥有者
      • FGID(file gid): 文件的拥有者所在组的组id
      • 设置用户id(SUID,set user id,文件特有的概念): 站在文件的角度上看,文件权限有r-w-x-s-t类型,其中的s表示SUID是打开状态(即可用状态)。如果一个文件的权限位里面没有s,则表示SUID是关闭状态(即不可用状态)。注意:SUID只能用于可执行文件,其作用是修改EUID(有效用户id)
    • 总结:RUIDEUIDSSUID都是针对进程的,即站在进程的角度,即进程特有的概念。而SUID则是针对文件的,即站在文件的角度,或者说是SUID是可执行文件特有的概念!为什么要修改进程的EUID?目的是想让进程在某一时刻能够拥有特殊的权限。
      在这里插入图片描述
  • 我们总是使用最小特权模型:我们的程序应当只具有为完成给定任务所需的最小特权

  • 可以通过setuid和setgid函数设置进程相关的用户ID和组ID

    int setuid(uid_t uid);
    int setgid(gid_t gid);
    
    • 若进程具有超级用户特权,则setuid函数将实际用户id、有效用户id、保存的设置用户id设置为uid
    • 若进程没有超级用户权限,但是uid等于实际用户id或保存的设置用户id,则setuid只将有效用户设为uid,不更改实际用户id和保存的设置用户id
    • 如果以上条件不满足则出错返回-1并设置errno
  • 下图是更改3个用户ID的不同方法,同样适用于组ID
    在这里插入图片描述

  • 也可以通过seteuidsetegid,它们类似于setuidsetgid,但是只更改有效用户ID有效组ID

    int seteuid(uid_t euid);
    int setegid(gid_t egid);
    
    • 如果是特权用户,则可以将有效用户id设置为参数uid
    • 如果不是特权用户,但是参数uid等于实际用户id或保存的设置用户id,则可以将有效用户id设置为参数uid。
  • 以上的操作都适用于组ID。但是附属组ID不受这些函数的影响。

12、解释器文件

  • 解释器文件是文本文件,其起始行的形式是
    #! pathname [可选项]
    
    比如
    #! /bin/sh
    
    • pathname通常是绝对路径名,对它不进行特殊处理(不适用PATH搜索)
    • 内核使调用exec函数的进程实际执行的并不是该解释器文件,而是在该解释器文件第一行中pathname所指定的文件(即解释器)。要将解释器文件(以#!开头的文件)和解释器(pathname表示的文件)区分开
    • 当用exec函数执行一个解释器文件时,实际执行的是解释器文件第一行所指定的解释器。并将第一行中的可选项和exec函数中的命令行参数作为该解释器的命令行参数。但是注意,对于exec函数中的第一个命令行参数,会改变成exec函数中的第一个参数pathname,该参数告诉了解释器要解释的文件是哪一个。如以下解释器文件/a/b/c/test
      #! /home/bin/echoarg -f
      ...
      
      在一个进程中调用exec函数
      execl("/a/b/c/test","test","arg1","arg2",(char*)0);
      
      那么传递给解释器echoarg的命令行参数分别是/home/bin/echoarg(解释器的pathname)、-f(解释器的可选参数)、/a/b/c/test(excel中的pathname,注意不是argv[0])、arg1(excel中的argv[1])、arg2(excel中的argv[2])。相当于在shell中输入:
      /home/bin/echoarg -f /a/b/c/test arg1 arg2
      
      然后解释器来解释该文件,由于#是注释字符,因此解释器忽略该文件第一行。之后逐行解释文件内容。
  • 使用解释器文件的优点
    • 有些程序是用某种语言写的脚本,解释器文件可将这一事实隐藏起来
    • 解释器文件效率较好
    • 解释器文件使我们可以使用除了/bin/sh以外的其他shell来写shell脚本。当exec函数找到一个非机器可执行的可执行文件时,总是调用/bin/sh来解释执行该文件。但是解释器文件可以根据第一行内容来指定想要的解释器,不一定是/bin/sh了。

13、函数system

  • 可以在进程中通过system函数执行命令字符串。但是依赖于操作系统,有些操作系统可能不支持system执行命令字符串。
    int system(const char *command);
    
    如果command是一个空指针,那么当该函数可用时返回非0值。可以通过这种方式测试一个给定操作系统是否支持system函数。UNIXsystem总是可用
  • system函数的实现本质上调用了fork、exec、waitpid:
    • fork失败或waitpid返回除EINTR之外的错误,返回-1

      • EINTR:如果进程在一个慢系统调用(slow system call,如waitpid)中阻塞时,当捕获到某个信号且相应信号处理函数返回时,这个系统调用被中断,调用返回错误,设置errno为EINTR
    • exec失败(即不能执行shell命令),则其返回值如同shell执行了_exit(127)

    • 如果三个函数都成功,那么system返回值是shell的终止状态。shell的终止状态是其执行最后一条命令的退出状态。这种情况下和获取子进程的退出状态一样。(可以用wait函数那一节中的几个宏来判断子进程退出状态如WIFEXITEDWIFSIGNALED)。

  • 实例:system函数的一种实现(注意它对信号没有处理)
    #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)作为命令输入(而不是从标准输入或从一个给定的文件中读命令)。shell对以null字节终止的命令字符串进行语法分析,将他们分成命令行参数。传递给shell的实际命令字符串可以包含任一有效的shell命令。例如,可以使用<>对输出和输出进行重定向。
  • 实例:对上述system函数进行测试(注意:pr_exit函数在之前的小节有定义)
    #include "apue.h"
    #include <sys/wait.h>
    
    int
    main(void)
    {
    	int		status;
    
    	if ((status = system("date")) < 0)
    		err_sys("system() error");
    
    	pr_exit(status);
    
    	if ((status = system("nosuchcommand")) < 0)
    		err_sys("system() error");
    
    	pr_exit(status);
    
    	if ((status = system("who; exit 44")) < 0)
    		err_sys("system() error");
    
    	pr_exit(status);
    
    	exit(0);
    }
    
    命令行得到
    lh@LH_LINUX:~/桌面/apue.3e/proc$ ./systest1
    2023年 07月 14日 星期五 22:29:21 CST
    normal termination, exit status = 0
    sh: 1: nosuchcommand: not found
    normal termination, exit status = 127
    lh       tty7         2023-07-14 21:59 (:0)
    normal termination, exit status = 44
    
  • 使用system函数而不是直接使用forkexec的原因是:system进行了所需的各种出错处理以及各种信号处理
  • 一个安全漏洞:
    • 注意,不能在设置用户ID程序中调用system函数。设置组ID程序同理。 即一个文件有设置用户ID位,那么当执行此文件时,该进程中不能调用system函数。因为由于该文件有设置用户ID位,因此该进程的有效用户ID将会被设置为文件所有者ID。但是如果在该进程中又system了其他命令,那么子进程有可能继承父进程的有效用户ID
    • 当一个进程生成另一个进程时,一定要保证其最小权限原则,防止权限被不断地传递复制。
    • 因此如果一个进程正在以特殊的权限运行(由于设置用户ID位或设置组ID位造成),它又想生成另一个进程执行另一个程序,则它不能使用system而应该使用fork和exec,并且在fork之后、exec之前更改回普通权限,防止特殊权限在exec之后传递下去

补充B:sysconf函数

  • 我们可以通过函数参数,来获取系统配置参数值。比如获取cpu核数,页大小,最大页数,cpu个数等。
    long sysconf(int name);
    
    如果成功返回指定参数值,如果出错,那么函数返回 -1 ,并适当地设置 errno当没有错误发生时, -1 也是一个合法的返回值。因此,程序要检查错误,应该在调用 sysconf() 之前将 errno 设置为 0,然后如果返回 -1,则检验errno判断是否出错。
  • 一些典型的参数值
    • _SC_HOST_NAME_MAX: 主机名的最大长度,不包括终止空字节,由 gethostname(2) 返回。
    • _SC_OPEN_MAX:进程可以随时打开的最大文件数
    • _SC_PAGESIZE:页的大小(以字节为单位)
    • _SC_NGROUPS_MAX:附属组 ID 的最大数量。
    • 等等
    int main(int argc, char* argv[]) {
        cout << "主机名最大长度:" << sysconf(_SC_HOST_NAME_MAX) << endl;
        cout << "页的大小:" << sysconf(_SC_PAGE_SIZE) << endl;
    }
    /*输出
    主机名最大长度:64
    页的大小:4096
    */
    
  • 注意:有时候可以不用sysconf函数获取系统配置参数值,而是直接获取其值。通常sysconf参数去掉开头的_SC_剩下的部分就是它要查询的系统参数值。如_SC_HOST_NAME_MAX就是指的HOST_NAME_MAX系统配置参数。但是有些时候这些系统配置参数名并没有定义,因此可以使用以下方法:
    #ifdef HOST_NAME_MAX
        cout << "主机名最大长度:" << HOST_NAME_MAX << endl ;
    #elif defined(_SC_HOST_NAME_MAX)
        cout << "主机名最大长度:" << sysconf(_SC_HOST_NAME_MAX) << endl;
    #endif
    

14、进程会计

  • 启动进程会计后,每当进程结束时内核就会写一个会计记录。该记录包括命令名所使用的CPU时间总量用户ID组ID启动时间等。其本质是accton命令调用acct函数来完成的。
  • 启动和关闭进程会计:
    • 超级用户执行一个带路径名参数的accton命令会启用进程会计,内核会将会计记录写到指定的文件中。如accton outfile
    • 超级用户执行不带任何参数的accton命令则停止进程会计,如accton
  • 会计记录文件中的每个记录项结构基本如下:
    typedef uint16_t comp_t;
    
    struct acct
    {
      char ac_flag;			/* Flags.  */
      uint16_t ac_uid;		/* Real user ID.  */
      uint16_t ac_gid;		/* Real group ID.  */
      uint16_t ac_tty;		/* Controlling terminal.  */
      uint32_t ac_btime;		/* Beginning time.  */
      comp_t ac_utime;		/* User time.  */
      comp_t ac_stime;		/* System time.  */
      comp_t ac_etime;		/* Elapsed time.  */
      comp_t ac_mem;		/* Average memory usage.  */
      comp_t ac_io;			/* Chars transferred.  */
      comp_t ac_rw;			/* Blocks read or written.  */
      comp_t ac_minflt;		/* Minor pagefaults.  */
      comp_t ac_majflt;		/* Major pagefaults.  */
      comp_t ac_swaps;		/* Number of swaps.  */
      uint32_t ac_exitcode;		/* Process exitcode.  */
      char ac_comm[ACCT_COMM+1];	/* Command name.  */
      char ac_pad[10];		/* Padding bytes.  */
    };
    
  • 其中ac_flag成员记录了进程执行期间的某些事件:可以将ac_flag成员与下面常量按位与进行判断时是否发生这些事件
    在这里插入图片描述
  • 会计记录所需要的各个数据都由内核保存在PCB中,并在一个新进程被创建时初始化一个记录(如fork后在子进程中)。进程终止时写一个会计记录
    • 我们无法获取永远不终止的进程的会计记录。如init进程这种一直运行的进程,一些内核守护进程的会计记录也无法获得。
    • 在会计文件中记录的是顺序是进程终止的顺序,而不是它们启动顺序。只能通过记录项中的启动日历时间来确定进程启动顺序。
    • 会计记录对应的是进程而不是程序。因此fork之后内核为子进程初始化一个记录,而不是在exec一个新程序时执行初始化。虽然exec并不创建一个新的会计记录,但相应记录中的命令名变了,AFORK标志清除。比如一个进程中顺序执行了三个程序(A exec B,B exec C,然后C exit),那么只会写一个会计记录,该记录中命令名对应程序C,但是CPU时间是程序A/B/C之和。
    • 会计记录时间中的进程时间都是以时钟滴答数量计算的。因此如果要获取进程时间值(秒数)需要用时钟滴答数除以每秒钟时钟滴答数sysconf(_SC_CLK_TCK)。见第一章内容

15、用户标识

  • 可通过getlogin获取运行该进程的用户登录时用的登录名

    char *getlogin(void);
    

16、进程调度

  • UNIX对进程提供的只是基于调度优先级的粗粒度的控制。调度策略和调度优先级由内核确定。进程可以通过增加nice友好值选择以更低优先级运行(通过增加nice值来降低它对CPU的占有,因此该进程是友好的)。只有特权进程允许提高调度权限(即降低nice值)
    int nice(int inc); // 成功返回新的友好值
    
    • 参数inc是该进程的友好值增加量,不是绝对量。
    • nice值越小优先级越高,nice值越大优先级越小。友好值的最大值NZERO,可以通过sysconf(_SC_NZERO)获取NZERO值。Linux中nice值的有效范围从-2019(即NZERO值为20),而且默认值是0
    • 进程只能通过nice函数影响它自己和子进程的nice值(即子进程会继承父进程友好值)
  • 可以通过getpriority/setpriority函数获取/设置指定进程或一组进程的nice值
    int getpriority(int which, id_t who);
    int setpriority(int which, id_t who, int prio);
    
    • getpriority:可用来取得进程、进程组和指定用户的进程的nice值
    • which参数
      • PRIO_PROCESS表示进程
      • PRIO_PGRP表示进程组
      • PRIO_USER表示用户ID
    • who参数:
      • 0:当which为PRIO_PROCESS时表示调用进程;当which为PRIO_PGRP时表示调用进程所在进程组;当which为PRIO_USER时表示调用进程实际用户ID的进程。
      • 如果是非0值,则为指定的pid进程,进程组id中的所有进程,用户id的所有进程
      • 如果which参数作用于多个进程,则函数返回所有作用进程中优先级最高的(最小nice值)
    • setpriority
      • 可以为进程、进程组、属于特定用户ID的所有进程设置优先级。前两个参数与getpriority相同,prio增加到nice值上。

17、进程时间

  • 可以通过times函数获取调用进程和已终止子进程的时钟时间(墙上时钟时间)、用户CPU时间和系统CPU时间(相关概念见1.10节)。
    clock_t times(struct tms *buf);
    struct tms {
        clock_t tms_utime;  /* user time */
        clock_t tms_stime;  /* system time */
        clock_t tms_cutime; /* user time of children */
        clock_t tms_cstime; /* system time of children */
    };
    
    • 该函数会将墙上时钟时间作为函数返回值返回,并将用户CPU时间、系统CPU时间保存在参数指向的结构体中。
    • 注意times函数获取到的墙上时钟值是相对于过去某一时刻度量的,所以不能使用其绝对值而要使用相对值。例如调用times保存其返回值,过一段时间后再调用times,从新返回的值中减去以前返回的值,此差值就是墙上时钟时间。用户CPU和系统CPU时间值则是相对于进程开始时开始计量的。
    • 需要注意,这个函数获取的时间值都是clock_t类型,可以从第一章知道,clock_t返回的是时钟滴答数。因此如果要知道具体时间(秒数),需要用该类型数值除以每秒时钟滴答数sysconf(_SC_CLK_TCK)
    • tms结构体中对子进程的字段包含了此进程用wait函数族已经等待到的各子进程的相关值。
  • 实例:计算墙上时钟时间、父进程和子进程的用于CPU时间和系统CPU时间
    #include "apue.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: %s\n", cmd);
    
    	if ((start = times(&tmsstart)) == -1)	/* starting values */
    		err_sys("times error");
    
    	if ((status = system(cmd)) < 0)			/* execute command */
    		err_sys("system() error");
    
    	if ((end = times(&tmsend)) == -1)		/* ending values */
    		err_sys("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)
    			err_sys("sysconf error");
    
    	printf("  real:  %7.2f\n", real / (double) clktck);
    	printf("  user:  %7.2f\n",
    	  (tmsend->tms_utime - tmsstart->tms_utime) / (double) clktck);
    	printf("  sys:   %7.2f\n",
    	  (tmsend->tms_stime - tmsstart->tms_stime) / (double) clktck);
    	printf("  child user:  %7.2f\n",
    	  (tmsend->tms_cutime - tmsstart->tms_cutime) / (double) clktck);
    	printf("  child sys:   %7.2f\n",
    	  (tmsend->tms_cstime - tmsstart->tms_cstime) / (double) clktck);
    }
    
    命令行输出:
    lh@LH_LINUX:~/桌面/apue.3e/proc$ ./times1 "sleep 5" "date" "man bash > /dev/null"
    command: sleep 5
      real:     5.00
      user:     0.00
      sys:      0.00
      child user:     0.00
      child sys:      0.00
    normal termination, exit status = 0
    
    command: date
    2023年 07月 15日 星期六 12:03:09 CST
      real:     0.00
      user:     0.00
      sys:      0.00
      child user:     0.00
      child sys:      0.00
    normal termination, exit status = 0
    
    command: man bash > /dev/null
    <standard input>:2295: warning [p 19, 1.2i]: cannot adjust line
      real:     0.34
      user:     0.00
      sys:      0.00
      child user:     0.07
      child sys:      0.20
    normal termination, exit status = 0
    
    第一和第二个程序使用到的cpu时间过少,第三个程序中运行了一个处理时间足够长的命令来表明所有的CPU时间都出现在子进程中。需要注意的是:shell和明林正是在子进程中执行的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elec Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值