本篇来介绍下用户级进程的使用方法,创建,执行以及退出的过程。系统中的第一个用户级进程,init进程,它是内核在自举过程中创建的,文件位置放在/sbin/init。其它进程都是由它一层层fork出来的,所以init进程系统内所有用户级进程的始祖进程,同时它还负责接管父进程已经终止的所有子进程。
前面说过每个进程都是由其父进程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;
}
执行结果:
关于这个结果有两点需要说明:
当然不是,父子进程谁会先被调度到理论上是不确定,实际上通常都是子进程,不然写时复制技术还有什么用?
write 函数是无缓冲的,所以写进终端设备的字符会立即输出。但printf是带缓冲的,当标准输出指向终端设备时是行缓冲,遇到\n 就会输出;但如果指向文件,则是全缓冲,调用fork时,字符仍在缓冲区,子进程会将该缓冲区内容复制到自己的地址空间,当进程终止时,会被写进文件。
除了复制数据段的内容,子进程还会从父进程复制什么信息呢?
答案是父进程的数据空间,堆,栈的内容都会复制一遍,而且父子进程共享相同的正文段。这样对系统资源是极大的消耗,所以现在的Linux 系统中也引入了写时复制技术。
写时复制(copy-on-write)的基本思路是父进程fork 子进程之后,虽然为子进程分配了独立的虚拟地址空间,但实际上子进程的虚拟地址空间和 父进程的虚拟地址空间指向相同的物理地址。当父子进程中有更改对应段的行为时,才为子进程相应的段分配新的物理地址。简单的示意图如下所示:
回到上面的示例,再考虑一个问题,我们将执行的结果定位到文件,实际上就是重定向了父子进程的标准输出,这也说明了,子进程从父进程继承了它打开的文件描述符。那除了打开的文件描述符,子进程还从父亲那获取到了哪些信息呢?
很多很多。。。
各种用户ID,组ID,当前工作目录,信号信息,控制终端,and so on~~~!!!
2. vfork
vfork也可以用来创建一个子进程,但是vfork的目的是用来exec 一个新程序,就像是一个实现特定需求的fork函数。于是vfork函数就有了以下的t:额定:
3. 进程四要素
在这里,有必要扩展下,说明下进程的四要素:
如果只是不符合第四条,那么就是后面介绍的线程。vfork 之后 exec 之前子进程就没有自己独立的用户空间,是不是就类似于线程呢?
4. fork 的应用
fork 最常用的两种场景是:
如果进程需要执行另一个程序,可以通过调用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函数则可以简单的总结如下:
下面的例子是分别用上述的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进程有8种终止方式,其中3种是异常方式:
另外还有5中正常终止的方式:
下面再介绍一些进程退出相关的函数。
1. 退出函数
#include
void exit(int status);
void _Exit(int status);
#include
Void _exit(int status);
其中exit和_EXIT是系统库函数,exit 特别的地方在于它会执行一个标准I/O库清理关闭操作,对于所有打开的流调用fclose,这会导致输出缓冲中所有数据被flush。
终止状态:参数status是这三个退出函数的终止状态,如果是下面三种情况,则终止状态是未定义的:
2. 登记函数atexit
登记函数在退出时会被自动调用,通常用于执行一些退出时的清理动作:
#include
int atexit(void (*func)(void));
wait函数书上的介绍很简单,但是深究起来却很复杂。知识点都是环环相扣的,那我们就先从僵尸进程和孤儿进程开始吧。
僵尸进程是指一个进程退出之后,几乎会释放掉它所有的数据单元,比如其数据段,代码段等;但是PCB还在,PCB中会记录一些退出状态信息,这类信息只能由父进程通过wait函数族去清除
孤儿进程是指子进程的父进程已经被终止,该子进程会被init 进程收养,孤儿进程exit之后的状态信息会由init进程定期清除。
那么问题来了,进程退出后这些状态信息有哪几种方式可以清除呢?
暴力一点,直接kill掉它的父进程,前面介绍过孤儿进程init进程会负责料理后事。
文明一点,就是使用本节的主角——wait 函数啦。通过wait函数族,父进程会负责“送终的”。
#include
pid_t wait(int *statloc);
父进程调用wait函数后会发生哪些情况呢?
从上面的介绍中我们知道,一旦调用了wait函数,调用进程立即会被阻塞住。其实我们有更好的处理办法,在子进程终止时,内核会向其父进程发送SIGCHLD信号,那问题就简单了。我们可以在父进程中将SIGCHLD信号的处理动作设置成调用wait函数。还有这种高级用法,想想是不是很兴奋。
更让我们兴奋的是,除了wait函数,还有一些wait函数的兄弟姐妹提供了更多元化的wait 函数功能。这些更高级的函数可以让调用进程不再阻塞,甚至可以让调用进程等待某个特定的子进程终止。比如我们的waitpid函数:
#inlcude
pid_t waitpid(pid_t pid, int *statloc, int options);
第一个pid 参数可取值如下:
第三个参数是waitpid 的另一个优势, 这些值可以按位或作为参数:
关于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 be: the child terminated; the 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 immediately. Otherwise 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这个了。
Linux中还提供了另一组wait函数,用来实现更多元化的功能,在这里我们也简单介绍下吧,首先是waittid函数:
#inlcude
int waittid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
其中idtype 可取值如下:
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);
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该如何解释,它们的取值情况如下:
Linux为度量进程时间定义了三种时间值:
我们可以通过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()函数中给我们展示了怎么将时钟滴答转换成具体的时间。
我们先看下这个程序的执行结果:
“sleep 5”的 命令会让cpu 等待5s的时间,从结果来看 wallclock 也是 5s 的时间;但由于没有执行用户命令或者内核,所以对应的时间都为0。
再来看下一个示例:
可以看到,子进程的系统时间和用户时间 都打印出来了,这里我们并没有fork子进程,为什么命令会在子进程运行呢2.7 ? 这就是我们接下来要介绍的system()函数。
system()函数可以用来执行特定的命令字符串,其执行一般分为三个步骤:
先来看下system函数的定义:
#include
int system(const char *cmdstring)
既然system函数的执行分为三个步骤,这就造成了system函数有各种返回值,每一步执行出错可能都会有对应的返回值:
最后的最后我们来看下解释器文件。
解释器文件就是一种文本文件,用来指定用什么程序去执行什么操作。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包含着更过信息。