Linux 进程控制

本篇来介绍下用户级进程的使用方法,创建,执行以及退出的过程。系统中的第一个用户级进程,init进程,它是内核在自举过程中创建的,文件位置放在/sbin/init。其它进程都是由它一层层fork出来的,所以init进程系统内所有用户级进程的始祖进程,同时它还负责接管父进程已经终止的所有子进程。

2.1 进程启动

前面说过每个进程都是由其父进程fork创建的,下面就切入正题前面,正式介绍我们的两个fork 进程的函数。

#include

pid_t fork(void);

pid_t vfork(void);

这两个函数函数的的返回情况相同, -1代表出错,子进程返回0,父进程返回子进程ID。出错一般是由于系统进程数已经达到最大值。子进程返回0这种思路,主要是因为子进程只有一个父进程,可以通过getpid 获得父进程pid;而父进程可能有多个子进程,最好的区分方式就是通过返回的子进程pid。

1. fork

     我们从APUE书上关于fork函数的例程开始:

#include  
#include  
#include  
  
int glbvar = 6;  
char buf[] = "a write to aout\n";  
	
int main(int argc, char* argv[])  
{  
    int var  = 88;  
    pid_t pid;  
    if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf) -1)  
    {  
         printf("write error\n");  
    }  
    printf("\n");  
    printf("Before fork\n");  
    printf("\n");  
    if ((pid = fork()) < 0)  
    {  
         printf("fork error\n");  
    }  
    else if (pid == 0)  
    {  
        glbvar++;  
        var++;  
    }  
    else  
    {  
        sleep(2);  
    }  
    printf("pid = %ld, glob = %d, var = %d\n",(long int)getpid(), glbvar, var);  
    return 0;  
}

执行结果:

Linux 进程控制_第1张图片

关于这个结果有两点需要说明:

  • 从结果来看,每次父进程fork子进程后,都是子进程先运行,这是事先设计好的吗?

       当然不是,父子进程谁会先被调度到理论上是不确定,实际上通常都是子进程,不然写时复制技术还有什么用?

  • 上述实验,当结果重定向到 文件,为什么会出来 两句“”before fork“”?

        write 函数是无缓冲的,所以写进终端设备的字符会立即输出。但printf是带缓冲的,当标准输出指向终端设备时是行缓冲,遇到\n 就会输出;但如果指向文件,则是全缓冲,调用fork时,字符仍在缓冲区,子进程会将该缓冲区内容复制到自己的地址空间,当进程终止时,会被写进文件。

除了复制数据段的内容,子进程还会从父进程复制什么信息呢?

答案是父进程的数据空间,堆,栈的内容都会复制一遍,而且父子进程共享相同的正文段。这样对系统资源是极大的消耗,所以现在的Linux 系统中也引入了写时复制技术。

 写时复制(copy-on-write)的基本思路是父进程fork 子进程之后,虽然为子进程分配了独立的虚拟地址空间,但实际上子进程的虚拟地址空间和 父进程的虚拟地址空间指向相同的物理地址。当父子进程中有更改对应段的行为时,才为子进程相应的段分配新的物理地址。简单的示意图如下所示:

Linux 进程控制_第2张图片

回到上面的示例,再考虑一个问题,我们将执行的结果定位到文件,实际上就是重定向了父子进程的标准输出,这也说明了,子进程从父进程继承了它打开的文件描述符。那除了打开的文件描述符,子进程还从父亲那获取到了哪些信息呢?

很多很多。。。

各种用户ID,组ID,当前工作目录,信号信息,控制终端,and so on~~~!!!

2. vfork

vfork也可以用来创建一个子进程,但是vfork的目的是用来exec 一个新程序,就像是一个实现特定需求的fork函数。于是vfork函数就有了以下的t:额定:

  • vfork创建的子进程后,会阻塞父进程,保证子进程先运行,直到子进程执行exec或者exit,父进程才恢复运行
  • vfork创建的子进程,不会去复制父进程的地址空间,而是就运行在父进程的地址空间。这种技术带来了提高效率的有点,也容易使得子进程的运行会破坏父进程地址空间的数据

3. 进程四要素

在这里,有必要扩展下,说明下进程的四要素:

  1. 有一段程序供其执行(可以是多个进程共享的)
  2. 在内核有自己的专用系统堆栈空间
  3. 在内核有进程控制块(task_struct)
  4. 有独立的用户空间

如果只是不符合第四条,那么就是后面介绍的线程。vfork 之后 exec 之前子进程就没有自己独立的用户空间,是不是就类似于线程呢?

4. fork 的应用

     fork 最常用的两种场景是:

  • 希望使父子进程同时执行不同的代码段。这在网络服务进程中很常见,例如父进程等待客户端的服务请求。当请求到达时,fork一个子进程处理此请求,父进程则继续等待下一个请求。
  • 进程希望去执行一个不同的程序。Shell 中这很常见,子进程fork返回后立即调用exec,这样就可以去执行另一段程序。

2.2 执行新程序

如果进程需要执行另一个程序,可以通过调用exec函数族实现,调用exec后新程序会替换当前进程的正文段、数据段、堆和栈段,而新程序从其main函数开始执行。

#include
int execl(const char *pathname, const char *arg0, ... , /* (char*)0 */ );
int execv(const char *pathname, char const *argv[]);

int execlp(const char *file, const char *arg0, ... , /* (char*)0 */ );
int execvp(const char *file, const char *argv[]);

int execle(const char *pathname, const char *arg0, ... , /* (char*)0 */, char *const envp[]);
int execve(const char *pathname, const char *argv[], char *const envp[]);

int fexecve(int fd, char *const argv[], char *const envp[]);

关于exec函数则可以简单的总结如下:

  • l/v:l 代表第二个参数是list的方式,v代表是vector的方式
  • 有p和无p: 有p 第一个参数就是filename,没有p 第一个参数是pathname;当指定以filename作为参数时,如果filename中包含/,就将其视为路径名,否则就到PATH环境变量中目录表中去查找可执行文件
  • 如果有e的话,代表可以用指定的环境变量表去替换默认的环境变量表;没有则使用从父进程继承过来的环境变量
  • 只有execve 是内核的系统调用,其它6个都是库函数,而它们都是在execve函数的基础上进程封装的Linux 进程控制_第3张图片

  下面的例子是分别用上述的exec 函数族函数去执行”ps –r” command。

1.	#include   
2.	#include   
3.	#include   
4.	#include   
5.	#include   
6.	  
7.	int main(int argc, char *argv[])  
8.	{  
9.	    pid_t pid;  
10.	    //以NULL结尾的字符串数组的指针,执行ps -r  
11.	    char *arg[] = { "ps", "-r", NULL };  
12.	      
13.	    /* execl 中以逗号分隔的list为参数列表,并以NULL指针为结束标志*/  
14.	    if ((pid = fork()) < 0)  
15.	    {  
16.	        perror("fork 0 error ");  
17.	    }  
18.	    else if (pid == 0)  
19.	    {  
20.	        // in clild   
21.	        printf("1------------execl------------\n");  
22.	        if (execl("/bin/ps", "ps", "-r", NULL) == -1)  
23.	        {  
24.	            perror("execl error ");  
25.	            exit(1);  
26.	        }  
27.	    }  
28.	    else  
29.	    {  
30.	        sleep(1);  
31.	        /* execv 接收一个以NULL结尾的字符串数组的指针为参数列表*/  
32.	        if ((pid = fork()) < 0)  
33.	        {  
34.	            perror("fork 1 error ");  
35.	        }  
36.	        else if (pid == 0)  
37.	        {  
38.	            // in child   
39.	            printf("2------------execv------------\n");  
40.	            if (execv("/bin/ps", arg) < 0)  
41.	            {  
42.	                perror("execv error ");  
43.	                exit(1);  
44.	            }  
45.	        }  
46.	        else  
47.	        {  
48.	            sleep(1);  
49.	            /*execlp:以逗号分隔的参数列表,列表以NULL指针作为结束标志 
50.	             *p是一个 filename,可以在environ 中path目录项中寻找*/  
51.	            if ((pid = fork()) < 0)  
52.	            {  
53.	                perror("fork 2 error ");  
54.	            }  
55.	            else if (pid == 0)  
56.	            {  
57.	                // in clhild   
58.	                printf("3------------execlp------------\n");  
59.	                if (execlp("ps", "ps", "-r", NULL) < 0)  
60.	                {  
61.	                    perror("execlp error ");  
62.	                    exit(1);  
63.	                }  
64.	            }  
65.	            else  
66.	            {  
67.	                sleep(1);  
68.	                /*execvp:以NULL结尾的字符串数组的指针为参数列表 
69.	                 *p是一个 filename,可以在environ 中path目录项中寻找 */  
70.	                if ((pid = fork()) < 0)  
71.	                {  
72.	                    perror("fork 3 error ");  
73.	                }  
74.	                else if (pid == 0)  
75.	                {  
76.	                    printf("4------------execvp------------\n");  
77.	                    if (execvp("ps", arg) < 0)  
78.	                    {  
79.	                        perror("execvp error ");  
80.	                        exit(1);  
81.	                    }  
82.	                }  
83.	                else  
84.	                {  
85.	                    sleep(1);  
86.	                    /* execle 
87.	                    *l 逗号分隔的参数列表,列表以NULL指针作为结束标志 
88.	                    *e 函数传递指定参数envp,允许改变子进程的环境*/  
89.	                    if ((pid = fork()) < 0)  
90.	                    {  
91.	                        perror("fork 0 error ");  
92.	                    }  
93.	                    else if (pid == 0)  
94.	                    {  
95.	                        printf("5------------execle------------\n");  
96.	                        if (execle("/bin/ps", "ps", "-r", NULL, NULL) == -1)  
97.	                        {  
98.	                            perror("execle error ");  
99.	                            exit(1);  
100.	                        }  
101.	                    }  
102.	                    else  
103.	                    {  
104.	                        sleep(1);  
105.	                        /*execve 
106.	                        * v 希望接收到一个以NULL结尾的字符串数组的指针 
107.	                        * e 函数传递指定参数envp,允许改变子进程的环境*/  
108.	                        if ((pid = fork()) < 0)  
109.	                        {  
110.	                            perror("fork 0 error ");  
111.	                        }  
112.	                        else if (pid == 0)  
113.	                        {  
114.	                            printf("6------------execve-----------\n");  
115.	                            if (execve("/bin/ps", arg, NULL) == 0)  
116.	                            {  
117.	                                perror("execve error ");  
118.	                                exit(1);  
119.	                            }  
120.	                        }  
121.	                    }  
122.	                }  
123.	            }  
124.	        }  
125.	    }  
126.	  
127.	    return 0;  
128.	}  

执行结果为:

Linux 进程控制_第4张图片

2.3 终止进程

Linux进程有8种终止方式,其中3种是异常方式:

  • 调用abort
  • 收到到一个信号
  • 最后一个线程对取消请求做出响应

另外还有5中正常终止的方式:

  • 从main 函数返回
  • 调用exit
  • 调用_exit或_Exit
  • 最后一个线程从其启动例程返回
  • 最后一个线程调用pthread_exit

下面再介绍一些进程退出相关的函数。

1. 退出函数

#include
void exit(int status);
void _Exit(int status);

#include
Void _exit(int status);

其中exit和_EXIT是系统库函数,exit 特别的地方在于它会执行一个标准I/O库清理关闭操作,对于所有打开的流调用fclose,这会导致输出缓冲中所有数据被flush。

终止状态:参数status是这三个退出函数的终止状态,如果是下面三种情况,则终止状态是未定义的:

  • 调用这些函数时不带终止状态
  • main函数执行了一个无返回值的return
  • main函数没有声明返回类型为整型

2. 登记函数atexit

登记函数在退出时会被自动调用,通常用于执行一些退出时的清理动作:

  • 且先登记后调用,若多次登记,等也会多次被调用
  • 登记函数无参数,登记成功返回0,否则返回非0
#include
int atexit(void (*func)(void));

3. wait函数族

wait函数书上的介绍很简单,但是深究起来却很复杂。知识点都是环环相扣的,那我们就先从僵尸进程和孤儿进程开始吧。

僵尸进程是指一个进程退出之后,几乎会释放掉它所有的数据单元,比如其数据段,代码段等;但是PCB还在,PCB中会记录一些退出状态信息,这类信息只能由父进程通过wait函数族去清除

孤儿进程是指子进程的父进程已经被终止,该子进程会被init 进程收养,孤儿进程exit之后的状态信息会由init进程定期清除。

那么问题来了,进程退出后这些状态信息有哪几种方式可以清除呢?

暴力一点,直接kill掉它的父进程,前面介绍过孤儿进程init进程会负责料理后事。

文明一点,就是使用本节的主角——wait 函数啦。通过wait函数族,父进程会负责“送终的”。

#include
pid_t wait(int *statloc);

父进程调用wait函数后会发生哪些情况呢?

  • 如果其所有的子进程都还在运行,调用进程会被阻塞
  • 如果有已经终止的子进程(有多个的话,取其第一个)或者等到第一个子进程终止状态,则取得子进程状态并返回该子进程ID(这里的终止其实用词并不准确,确切来说是状态变化)
  • 如果调用进程没有任何子进程,则直接返回-1

从上面的介绍中我们知道,一旦调用了wait函数,调用进程立即会被阻塞住。其实我们有更好的处理办法,在子进程终止时,内核会向其父进程发送SIGCHLD信号,那问题就简单了。我们可以在父进程中将SIGCHLD信号的处理动作设置成调用wait函数。还有这种高级用法,想想是不是很兴奋。

更让我们兴奋的是,除了wait函数,还有一些wait函数的兄弟姐妹提供了更多元化的wait 函数功能。这些更高级的函数可以让调用进程不再阻塞,甚至可以让调用进程等待某个特定的子进程终止。比如我们的waitpid函数:

#inlcude
pid_t waitpid(pid_t pid, int *statloc, int options);

第一个pid 参数可取值如下:

  • pid == -1  等待任一子进程
  • pid == 0  等待组ID等于调用进程组ID的任一子进程
  • pid > 0   等待进程ID等于pid 的进程
  • pid < -1   等待组ID等于pid 绝对值的任一子进程

第三个参数是waitpid 的另一个优势, 这些值可以按位或作为参数:

  • WNOHANG: 不阻塞,此时其返回值为0
  • WCONTINUED: 如果支持作业控制,则pid指定的子进程在停止后已经继续,其停止时的状态并未报告,这时候可以返回这个状态信息了
  • WUNTRACED: 如果支持作业控制,则pid指定的子进程已经停止,并且停止后还未报告过状态,这时候就可以报告了

关于wait 函数还有两点想要说要:

       1). wait 函数什么时候会返回?

返回这个词用得其实并不准确,更合适的表示其实是“子进程状态发生变化“。来看下spec中的详细说明:

All of these system calls are used to wait for state changes in a child of the calling process, and obtain information about the child whose state has changed. A state change is considered to bethe child terminatedthe child was stopped by a signal; or the child was resumed by a signal. In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if a wait is not performed, then terminated the child remains in a "zombie" state (see NOTES below).

If a child has already changed state, then these calls return immediatelyOtherwise they block until either a child changes state or a signal handler interrupts the call (assuming that system calls are not automatically restarted using the SA_RESTART flag of sigaction(2)).

看见没,wait函数在子进程状态改变,或者收到一个SA_INTERRUPT信号时都会返回,而且状态改变也不止是进程终止,stop或者 resume也行的。

     2).  wait函数的status 有哪些信息?

如果不关心状态信息,则可以将status参数设置为NULL。否则子进程的状态就存放在它指向的内存单元中。这个值是以bitmap的 方式,每个bit都代表中不同的含义,而我们可以通过一系列宏去解析这个值。

说明

WIFEXITED(status)

若为正常终止,则为真。此时可以执行WEXITSTATUS(status)获取exit参数的低8位

WIFSIGNALED(status)

异常终止则为真,可通过WTERMSIG(status)获取信号编号

WIFSTOPPED(status)

若为暂停子进程的返回状态,则为真。WSTOPSIG(status)可以获取暂停子进程的信号编号

WIFCONTINUED(status)

Waitpid特有,若是作业控制暂停后继续的子进程返回状态,则为真。

还是让我们来看两个例子吧:

1.	int main(int argc, char *argv[])  
2.	{  
3.	    pid_t pid;  
4.	  
5.	    if((pid = fork()) < 0)  
6.	    {  
7.	        printf("fork error\n");  
8.	    }  
9.	    else if(pid == 0)  
10.	    {  
11.	        printf("pid: %d exit status 0\n",(int)getpid());  
12.	        exit(0);  
13.	    }  
14.	    else  
15.	    {  
16.	        sleep(2);  
17.	        if((pid = fork()) < 0)  
18.	        {  
19.	            printf("fork error\n");  
20.	        }  
21.	        else if(pid == 0)  
22.	        {  
23.	            printf("pid: %d exit status 1\n",(int)getpid());  
24.	            exit(1);  
25.	        }  
26.	    }  
27.	    sleep(5);  
28.	    if(pid = wait(NULL))  
29.	    {  
30.	        printf("Wait pid: %d\n",pid);  
31.	    }  
32.	          
33.	}  

So easy,父进程分别fork 两个子进程,立即exit。然后父进程再调用wait 函数,看起来获取的是先 exit的 子进程。执行结果:

稍微修改下,wait 换成waitpid,并且让 第二个子进程 sleep 5s后才退出:

1.	#include  
2.	#include  
3.	#include  
4.	#include  
5.	  
6.	int main(int argc, char *argv[])  
7.	{  
8.	    pid_t pid;  
9.	  
10.	    if((pid = fork()) < 0)  
11.	    {  
12.	        printf("fork error\n");  
13.	    }  
14.	    else if(pid == 0)  
15.	    {  
16.	        printf("pid: %d exit status 0\n",(int)getpid());  
17.	        exit(0);  
18.	    }  
19.	    else  
20.	    {  
21.	        if((pid = fork()) < 0)  
22.	        {  
23.	            printf("fork error\n");  
24.	        }  
25.	        else if(pid == 0)  
26.	        {  
27.	            sleep(5);  
28.	            printf("pid: %d exit status 1\n",(int)getpid());  
29.	            exit(1);  
30.	        }  
31.	    }  
32.	      
33.	    sleep(2);  
34.	
35.	    if(pid = waitpid(pid,NULL,0))  
36.	    {  
37.	        printf("Wait pid: %d\n",pid);  
38.	    }  
39.	  
40.	    printf("Hi parent ,are you blocked?\n");  
41.	  
42.	    return 0;  
43.	}  

执行结果为:

怎么样,这时候父进程wait到的就是第二个子进程了。

这时候,出去好奇的目的,我们再稍微修改下,将waitpid改为WNOHANG:

if(pid = waitpid(pid,NULL,WNOHANG))  

这会不阻塞了,执行结果为:

这下父进程等不到第二个子进程exit,就自己先 return了。这时候pid 3775 这个子进程会被init进程收养。来吧,再query下这时候 子进程的 ppid:

咦,这ppid 怎么是1888,查了下ubuntu下 pid为 1888的进程是:

干掉这个init –user, ppid 就变成了1了。

其实啊,很多unix内核在具体实现过程中: 父进程终止后,不一定就是被init进程收养哦,只是有一个进程 会负责这个事情,ubuntu下这个进程就是上面所谓的1888这个了。

4. waittid wait3 wait4

Linux中还提供了另一组wait函数,用来实现更多元化的功能,在这里我们也简单介绍下吧,首先是waittid函数:

#inlcude

int waittid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

其中idtype 可取值如下:

  • P_PID :  特定pid,id 对应的是进程ID
  • P_PGID: 特定进程组任一进程,id 对应的就是进程组ID
  • P_ALL: 等待任一子进程,忽略id

infop是指向Siginfo结构的指针,改结构包含子进程状态改变有关信号的信息,讲信号的时候再讲吧。

options是一系列标志选项,这些标志指示着调用者关注子进程的哪些状态信息,主要有:

      Option                                                                             Detail
WCONTINUED 等待一个曾经被停止随后又继续运行的进程,且该进程的状态尚未报告
WEXITED 等待已经退出的进程
WNOHANG

如果调用者已经没有可用的子进程退出状态,立即返回

WNOWAIT 不破坏子进程的退出状态,该子进程的退出状态由后续的wait函数族调用获取

WSTOPPED

等待一个已经停止,但状态尚未报告的进程

此外还有wait3和wait4 两个函数也可以达到同样的效果,但是这两个函数扩展的功能主要是用来获取系统资源情况,它们允许内核返回由终止进程及其所有子进程使用的资源概况。

#include
#inlcude
#inlcude
#inlcude

pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);

2.4 进程调度

Linux 中进程的调度策略和调度优先级是由内核决定的,但Linux中也提供了一些函数,可以让我们粗粒度的调整进程的调度优先级,这里我们主要介绍下基于nice值得nice函数和getpriority 和 setpriority函数。

1. nice函数

nice值是在 0 ~ (2^NZENO - 1)的范围内,当我们设置的nice值超过这个范围,系统会自动将其调整成合法值。nice值越大说明越友善,那对不起,好人被人欺,通常你的优先级就更低了。来看下nice函数的定义:

#include

int nice(int incr)

//sucess:nice值;error:-1

其中nice函数会函数新的nice值NZERO,参数incr就是我们要调整的nice值。关于nice函数还需要注意两点:

  • nice得值是由可能是-1的,所以如果nice函数返回-1,我们无法确定nice是否执行成功,通常我们根据errno值来判断

  • Linux中NZERO的值可以通过sysconf参数(_SC_NZERO)来访问

2. getpriority & setpriority

这组函数不但可以获取/设置调用进程的nice值,还可以获取/设置制定进程的nice值,定义如下:

#include

int getpriority(int which , int who)
//sucess: 返回-NZERO ~ NZERO-1之间的值;error:-1

int setpriority(int which , int who)
//sucess: 0; error: -1

这里要说明下的就是两个参数which和who,which决定这who该如何解释,它们的取值情况如下:

  • 当参数which为PRIO_PROCESS时:参数who为0,则返回当前进程的进程优先级;参数who不为0,则返回进程号为who的进程的优先级
  • 当参数which为PRIO_PGRP时,参数who为0,则返回当前进程组的优先级;参数who不为0,则返回进程组号为who的进程组的优先级。进程组的优先级为进程组中优先级最高的进程的优先级
  • 当参数which为PRIO_USER时,参数who为0,则返回当前用户进程的优先级;参数who不为0,则返回用户ID为who的进程的优先级。用户进程的优先级为进程中优先级最高的进程的优先级

2.5 进程时间

Linux为度量进程时间定义了三种时间值:

  • 墙上时钟时间:进程运行的时间总量,其值与系统中同时运行的进程数有关
  • 用户CPU时间:进程执行用户命令所用的时间
  • 系统CPU时间:进程执行内核程序所用的时间

我们可以通过times函数来获取进程时间:

#include

clock_t times(struct tms *buf)

先来看下这里的struct tms结构:

struct tms{
    clock_t tms_utime;      //user time
    clock_t tms_stime;      //system time
    clock_t tms_cutime;     //terminated children user time
    clock_t tms_cstime;     //terminated children system time
}

从这里也可以看到times()函数可以通过形参的方式获取到调用进程以及终止子进程的用户时间,系统时间;times()函数的返回值就是墙上时钟时间。

这里的clock_t 是以时钟滴答作为基本单位;而通过sysconf(_SC_CLK_TCK)可以获取到每秒钟的时钟滴答数,这样我们就可以计算出以秒为单位的时间。我来看个具体的例子:

#include
#include
#include
#include

int main(int argc, char *argv[])
{
	unsigned int i = 10;
	struct tms stime,etime;
	clock_t start,end;
	long clk;

	start = times(&stime);
	system(argv[1]);
	end = times(&etime);

	clk = sysconf(_SC_CLK_TCK);

	printf("Wallclock time: %f\n", (end - start) / (double)clk);
	printf("User time: %f\n",(etime.tms_utime - stime.tms_utime) / (double)clk);
	printf("System time: %f\n",(etime.tms_stime - stime.tms_stime) / (double)clk);
	printf("Child system time: %f\n",(etime.tms_cstime - stime.tms_cstime) / (double)clk);
	printf("Child user time: %f\n",(etime.tms_cutime - stime.tms_cutime) / (double)clk);
	return 0;
}

上例中printf()函数中给我们展示了怎么将时钟滴答转换成具体的时间。

我们先看下这个程序的执行结果:

Linux 进程控制_第5张图片

“sleep 5”的 命令会让cpu 等待5s的时间,从结果来看 wallclock 也是 5s 的时间;但由于没有执行用户命令或者内核,所以对应的时间都为0。

再来看下一个示例:

可以看到,子进程的系统时间和用户时间 都打印出来了,这里我们并没有fork子进程,为什么命令会在子进程运行呢2.7 ? 这就是我们接下来要介绍的system()函数。

2.6 system函数

system()函数可以用来执行特定的命令字符串,其执行一般分为三个步骤:

  • fork一个子进程
  • 子进程执行exec函数,用/bin/sh去执行特定的shell命令, -c选项是告诉shell从字符串command中读取命令
  • 父进程调用wait()函数等待子进程结束

先来看下system函数的定义:

#include

int system(const char *cmdstring)

既然system函数的执行分为三个步骤,这就造成了system函数有各种返回值,每一步执行出错可能都会有对应的返回值:

  • 如果执行fork失败或者waitpid返回除EINTR之外的值,则system函数返回-1,errno设定成特定的错误类型
  • 如果exec执行失败,其返回值如果shell执行了exit(127)
  • 如果都执行成功的话,则system函数的返回值是shell执行的终止状态

最后的最后我们来看下解释器文件。

2.7 解释器文件

解释器文件就是一种文本文件,用来指定用什么程序去执行什么操作。shell脚本就是最常用的解释器文件,它指定了使用shell去执行一些特定的命令。解释器文件都有固定的格式:

#! pathname [options]

shell脚本通常第一行是不是:

#!/bin/bash

这里需要注意的就是pathname通常是一个绝对路径。

最后,我们以一个解释器文件的例子来结束本篇的文章吧。

先来写一个打印所有参数的小程序:

#include
#include

int main(int argc, char *argv[])
{
	int i = 0;
	for(i = 0; i< argc; i++)
	{
		printf("argv[%d]: %s\n",i,argv[i]);
		return 0;
	}
}

再来写一个解释器文件:

#! /mnt/echoarg foo

最后使用exec来执行这个解释器文件看看:

#include
#include
#include
#include

int main(int argc, char *argv[])
{
	pid_t pid;

	if((pid = fork()) < 0)
	{
		printf("fork error\n");
	}
	else if(pid == 0)
	{
		if(execl("/mnt/interp","interp","myarg1",(char*)0) < 0)
		{
			printf("execl error\n");
		}
	}
	else
	{
		if(waitpid(pid,NULL,0) < 0)
		{
			printf("wait error");
		}
	}

	return 0;
		
}

执行结果如下:

对于这个输出结果,我可以这样理解:

将exec()这条命令理解成执行以下程序:

/mnt/interp  "interp","myarg1"

进一步是不是可以理解为:

/mnt/echoarg foo  "/mnt/interp ", "myarg1"

注意这里参数2 是/mnt/interp 而不是 interp,因为内核取execl调用中的pathname 而非第一个参数,因为pathname包含着更过信息。

 

你可能感兴趣的:(APUE学习笔记,APUE)