专栏首页J博士的博客进程详解(1)——可能是最深入浅出的进程学习笔记

进程详解(1)——可能是最深入浅出的进程学习笔记

原文地址:http://www.cnblogs.com/jacklu/p/5317406.html

进程控制块(PCB)

在Linux中task_struct结构体即是PCB。PCB是进程的唯一标识,PCB由链表实现(为了动态插入和删除)。

进程创建时,为该进程生成一个PCB;进程终止时,回收PCB。

PCB包含信息:1、进程状态(state);2、进程标识信息(uid、gid);3、定时器(time);4、用户可见寄存器、控制状态寄存器、栈指针等(tss)

每个进程都有一个非负的唯一进程ID(PID)。虽然是唯一的,但是PID可以重用,当一个进程终止后,其他进程就可以使用它的PID了。

PID为0的进程为调度进程,该进程是内核的一部分,也称为系统进程;PID为1的进程为init进程,它是一个普通的用户进程,但是以超级用户特权运行;PID为2的进程是页守护进程,负责支持虚拟存储系统的分页操作。

除了PID,每个进程还有一些其他的标识符:

五种进程状态转换如下图所示:

每个进程的task_struct和系统空间堆栈存放位置如下:两个连续的物理页【《Linux内核源代码情景分析》271页】

系统堆栈空间不能动态扩展,在设计内核、驱动程序时要避免函数嵌套太深,同时不宜使用太大太多的局部变量,因为局部变量都是存在堆栈中的。

进程的创建

新进程的创建,首先在内存中为新进程创建一个task_struct结构,然后将父进程的task_struct内容复制其中,再修改部分数据。分配新的内核堆栈、新的PID、再将task_struct 这个node添加到链表中。所谓创建,实际上是“复制”。

子进程刚开始,内核并没有为它分配物理内存,而是以只读的方式共享父进程内存,只有当子进程写时,才复制。即“copy-on-write”。

fork都是由do_fork实现的,do_fork的简化流程如下图:

fork函数

fork函数时调用一次,返回两次。在父进程和子进程中各调用一次。子进程中返回值为0,父进程中返回值为子进程的PID。程序员可以根据返回值的不同让父进程和子进程执行不同的代码。

一个形象的过程:

运行这样一段演示程序:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <stdlib.h>
 4 
 5 int main()
 6 {
 7  pid_t pid;
 8  char *message;
 9  int n = 0;
10  pid = fork();
11  while(1){
12  if(pid < 0){
13  perror("fork failed\n");
14  exit(1);
15  }
16  else if(pid == 0){
17  n--;
18  printf("child's n is:%d\n",n);
19  }
20  else{
21  n++;
22  printf("parent's n is:%d\n",n);
23  }
24  sleep(1);
25  }
26  exit(0);
27 }

运行结果:

可以发现子进程和父进程之间并没有对各自的变量产生影响。

一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。

什么时候使用fork呢?

一个父进程希望子进程同时执行不同的代码段,这在网络服务器中常见——父进程等待客户端的服务请求,当请求到达时,父进程调用fork,使子进程处理此请求。

一个进程要执行一个不同的程序,一般fork之后立即调用exec

vfork函数

vfork与fork对比:

相同:

返回值相同

不同:

fork创建子进程,把父进程数据空间、堆和栈复制一份;vfork创建子进程,与父进程内存数据共享

vfork先保证子进程先执行,当子进程调用exit()或者exec后,父进程才往下执行

为什么需要vfork?

因为用vfork时,一般都是紧接着调用exec,所以不会访问父进程数据空间,也就不需要在把数据复制上花费时间了,因此vfork就是”为了exec而生“的。

运行这样一段演示程序:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <stdlib.h>
 4 
 5 int main()
 6 {
 7  pid_t pid;
 8  char *message;
 9  int n = 0;
10  int i;
11  pid = vfork();
12  for(i = 0; i < 10; i++){
13  if(pid < 0){
14  perror("fork failed\n");
15  exit(1);
16  }
17  else if(pid == 0){
18  n--;
19  printf("child's n is:%d\n",n);
20  if(i == 1)
21  _exit(0);
22  //return 0;
23  //exit(0);
24  }
25  else{
26  n++;
27  printf("parent's n is:%d\n",n);
28  }
29  sleep(1);
30  }
31  exit(0);
32 }

运行结果:

可以发现子进程先被执行,exit后,父进程才被执行,同时子进程改变了父进程中的数据

子进程return 0 会发生什么?

运行结果:

从上面我们知道,结束子进程的调用是exit()而不是return,如果你在vfork中return了,那么,这就意味main()函数return了,注意因为函数栈父子进程共享,所以整个程序的栈就跪了。 如果你在子进程中return,那么基本是下面的过程: 1)子进程的main() 函数 return了,于是程序的函数栈发生了变化。 2)而main()函数return后,通常会调用 exit()或相似的函数(如:_exit(),exitgroup()) 3)这时,父进程收到子进程exit(),开始从vfork返回,但是尼玛,老子的栈都被你子进程给return干废掉了,你让我怎么执行?(注:栈会返回一个诡异一个栈地址,对于某些内核版本的实现,直接报“栈错误”就给跪了,然而,对于某些内核版本的实现,于是有可能会再次调用main(),于是进入了一个无限循环的结果,直到vfork 调用返回 error) 好了,现在再回到 return 和 exit,return会释放局部变量,并弹栈,回到上级函数执行。exit直接退掉。如果你用c++ 你就知道,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用 _exit()或_exitgroup()的封装) 可见,子进程调用exit() 没有修改函数栈,所以,父进程得以顺利执行

【《vfork挂掉的一个问题》http://coolshell.cn/articles/12103.html#more-12103

execve

可执行文件装入内核的linux_binprm结构体。

进程调用exec时,该进程执行的程序完全被替换,新的程序从main函数开始执行。因为调用exec并不创建新进程,只是替换了当前进程的代码区、数据区、堆和栈。

六种不同的exec函数:

当指定filename作为参数时:

如果filename中包含/,则将其视为路径名。

否则,就按系统的PATH环境变量,在它所指定的各个目录中搜索可执行文件。

*出于安全方面的考虑,有些人要求在搜索路径中不要包括当前目录。

在这6个函数中,只有execve是内核的系统调用。另外5个只是库函数,他们最终都要调用该系统调用,如下图所示:

execve的实现由do_execve完成,简化的实现过程如下图:

关于这些函数的区别,需要时可以查看《APUE》关于exec函数部分的内容。

运行这样一段演示程序:

 1 #include <errno.h>
 2 #include <stdio.h>
 3 #include <stdlib.h>
 4 
 5 char command[256];
 6 void main()
 7 {
 8    int rtn; /*child process return value*/
 9    while(1) {
10        printf( ">" );
11        fgets( command, 256, stdin );
12        command[strlen(command)-1] = 0;
13        if ( fork() == 0 ) {
14           execlp( command, NULL );
15           perror( command );
16           exit( errno );
17        }
18            else {
19           wait ( &rtn );
20           printf( " child process return %d\n", rtn );
21        }
22    }
23 }

a.out 是一个打印hello world的可执行文件。

运行结果:

进程终止

正常终止(5种)

从main返回,等效于调用exit

调用exit

exit 首先调用各终止处理程序,然后按需多次调用fclose,关闭所有的打开流。

调用_exit或者_Exit

最后一个线程从其启动例程返回

最后一线程调用pthread_exit

异常终止(3种)

调用abort

接到一个信号并终止

最后一个线程对取消请求作出响应

wait和waitpid函数

wait用于使父进程阻塞,等待子进程退出;waitpid有若干选项,如可以提供一个非阻塞版本的wait,也能实现和wait相同的功能,实际上,linux中wait的实现也是通过调用waitpid实现的。

waitpid返回值:正常返回子进程号;使用WNOHANG且没有子进程退出返回0;调用出错返回-1;

运行如下演示程序

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <sys/types.h>
  4 #include <sys/wait.h> 
  5 
  6 int main()
  7 {
  8         pid_t pid0,pid1;
  9         pid0 = fork();
 10         if(pid0 < 0){
 11                 perror("fork");
 12                 exit(1);
 13         }
 14         else if(pid0 == 0){
 15                 sleep(5);
 16                 exit(0);//child
 17         }
 18         else{
 19                 do{
 20                         pid1 = waitpid(pid0,NULL,WNOHANG);
 21                         if(pid1 == 0){
 22                                 printf("the child process has not exited.\n");
 23                                 sleep(1);
 24                         }
 25                 }while(pid1 == 0);
 26                 if(pid1 == pid0){
 27                         printf("get child pid:%d",pid1);
 28                         exit(0);
 29                 }
 30                 else{
 31                         exit(1);
 32                 }
 33         }
 34         return 0;
 35 }
 36 
 37 
 38 
 39 当把第三个参数WNOHANG改为0时,就不会有上面五个显示语句了,说明父进程阻塞了。
 40 
 41 
 42 
 43 a.out 的代码如下:
 44 
 45 
 46 #include <stdio.h>
 47 void main()
 48 
 49 {
 50         printf("hello WYJ\n");
 51 }
 52 
 53  
 54 
 55 process.c的代码如下:
 56 
 57 #include <stdio.h>
 58 #include <sys/types.h>
 59 #include <unistd.h>
 60 #include <stdlib.h>
 61 #include <sys/times.h>
 62 #include <sys/wait.h>
 63 
 64 int main()
 65 {
 66         pid_t pid_1,pid_2,pid_wait;
 67         pid_1 = fork();
 68         pid_2 = fork();
 69         if(pid_1 < 0){
 70                 perror("fork1 failed\n");
 71                 exit(1);
 72         }else if(pid_1 == 0 && pid_2 != 0){//do not allow child 2 to excute this process.
 73                 if(execlp("./a.out", NULL) < 0){
 74                         perror("exec failed\n");
 75                 }//child;       
 76                 exit(0);
 77         }
 78         if(pid_2 < 0){
 79                 perror("fork2 failded\n");
 80                 exit(1);
 81         }else if(pid_2 == 0){
 82                 sleep(10);
 83         }
 84         if(pid_2 > 0){//parent 
 85                 do{
 86                         pid_wait = waitpid(pid_2, NULL, WNOHANG);//no hang
 87                         sleep(2);
 88                         printf("child 2 has not exited\n");
 89                 }while(pid_wait == 0);
 90                 if(pid_wait == pid_2){
 91                         printf("child 2 has exited\n");
 92                         exit(0);
 93                 }else{
 94                 //      printf("pid_2:%d\n",pid_2);
 95                         perror("waitpid error\n");
 96                         exit(1);
 97 
 98                }
 99         }
100         exit(0);
101 }

运行结果:

编写一个多进程程序:该实验有 3 个进程,其中一个为父进程,其余两个是该父进程创建的子进程,其中一个子进程运行“ls -l”指令,另一个子进程在暂停 5s 之后异常退出,父进程并不阻塞自己,并等待子进程的退出信息,待收集到该信息,父进程就返回。

 1 #include<stdio.h>
 2 #include<string.h>
 3 #include<fcntl.h>
 4 #include<unistd.h>
 5 #include<sys/types.h>
 6 #include<sys/wait.h>
 7 int main()
 8 {
 9  pid_t child1,child2,child;
10  if((child1 = fork()) < 0){
11  perror("failed in fork 1");
12  exit(1);
13  }
14  if((child2 = fork()) < 0){
15  perror("failed in fork 2");
16  exit(1);
17  }
18  if(child1 == 0){
19  //run ls -l
20  if(child2 == 0){
21  printf("in grandson\n");
22  }
23  else if(execlp("ls", "ls", "-l", NULL) < 0){
24  perror("child1 execlp");
25  }
26  }
27  else if(child2 == 0){
28  sleep(5);
29  exit(0);
30  }
31  else{
32  do{
33  sleep(1);
34  printf("child2 not exits\n");
35  child = waitpid(child2, NULL, WNOHANG);
36  }while(child == 0);
37  if(child == child2){
38  printf("get child2\n");
39  }
40  else{
41  printf("Error occured\n");
42  }
43  }
44 }

运行结果:

init进程成为所有僵尸进程(孤儿进程)的父进程

僵尸进程

在进程调用了exit之后,该进程并非马上就消失掉,而是留下了一个成为僵尸进程的数据结构,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。

子进程结束之后为什么会进入僵尸状态? 因为父进程可能会取得子进程的退出状态信息。

如何查看僵尸进程?

linux中命令ps,标记为Z的进程就是僵尸进程。

执行下面一段程序:

 1 #include <sys/types.h>
 2 #include <unistd.h>
 3 int main()
 4 {
 5  pid_t pid;
 6  pid = fork();
 7  if(pid < 0){
 8  printf("error occurred\n");
 9  }else if(pid == 0){
10  exit(0);
11  }else{
12  sleep(60);
13  wait(NULL);
14  }
15 }

运行结果:

ps -ef|grep defunc可以找出僵尸进程

ps -l 可以得到更详细的进程信息

运行结果显示:

运行两次之后发现有两个Z进程,然后等待一分钟后,Z进程被父进程回收。

其中S表示状态:

O:进程正在处理器运行

S:休眠状态

R:等待运行

I:空闲状态

Z:僵尸状态

T:跟踪状态

B:进程正在等待更多的内存分页

C:cpu利用率的估算值

收集僵尸进程的信息,并终结这些僵尸进程,需要我们在父进程中使用waitpid和wait,这两个函数能够手机僵尸进程留下的信息并使进程彻底消失。

守护进程Daemon

是linux的后台服务进程。它是一个生存周期较长的进程,没有控制终端,输出无处显示。用户层守护进程的父进程是init进程。

守护进程创建步骤:

1、创建子进程,父进程退出,子进程被init自动收养;fork exit

2、调用setsid创建新会话,成为新会话的首进程,成为新进程组的组长进程,摆脱父进程继承过来的会话、进程组等;setsid

3、改变当前目录为根目录,保证工作的文件目录不被删除;chdir(“/”)

4、重设文件权限掩码,给子进程更大的权限;umask(0)

5、关闭不用的文件描述符,因为会消耗资源;close

一个守护进程的实例:每隔10s写入一个“tick”

 1 #include<stdio.h>
 2 #include<string.h>
 3 #include<fcntl.h>
 4 #include<unistd.h>
 5 #include<sys/types.h>
 6 #define MAXFILE 65535
 7 
 8 int main()
 9 {
10  int fd,len,i;
11  pid_t pid;
12  char *buf = "tick\n";
13  len = strlen(buf);
14  if((pid = fork()) < 0){
15  perror("fork failed");
16  exit(1);
17  }
18  else if(pid > 0){
19  exit(0);
20  }
21  setsid();
22  if(chdir("/") < 0){
23  perror("chdir failed");
24  exit(1);
25  }
26  umask(0);
27  for(i = 0; i < MAXFILE; i++){
28  close(i);
29  }
30  while(1){
31  if((fd = open("/tmp/dameon.log", O_CREAT | O_WRONLY | O_APPEND, 0600)) < 0){
32  perror("open log failed");
33  exit(1);
34  }
35  write(fd, buf, len+1);
36  close(fd);
37  sleep(10);
38  }
39 }

运行结果:

 1 #include<stdio.h>
 2 #include<string.h>
 3 #include<fcntl.h>
 4 #include<unistd.h>
 5 #include<sys/types.h>
 6 #include<syslog.h>
 7 #define MAXFILE 65535
 8  
 9 int main()
10 {
11  int fd,len,i;
12  pid_t pid,child;
13  char *buf = "tick\n";
14  len = strlen(buf);
15  if((pid = fork()) < 0){
16  perror("fork failed");
17  exit(1);
18  }
19  else if(pid > 0){
20  exit(0);
21  }
22  openlog("Jack", LOG_PID, LOG_DAEMON);
23  if(setsid() < 0){
24  syslog(LOG_ERR, "%s\n", "setsid");
25  exit(1);
26  }
27 
28  if(chdir("/") < 0){
29  syslog(LOG_ERR, "%s\n", "chdir");
30  exit(1);
31  }
32  umask(0);
33  for(i = 0; i < MAXFILE; i++){
34  close(i);
35  }
36  if((child = fork()) < 0){
37  syslog(LOG_ERR, "%s\n", "fork");
38  exit(1);
39  }
40  if(child == 0){
41  //printf("in child\n");//can not use terminal from now on.
42  syslog(LOG_INFO, "in child");
43  sleep(10);
44  exit(0);
45  }
46  else{
47  waitpid(child, NULL, 0);
48  //printf("child exits\n");//can not use terminal from now on.
49  syslog(LOG_INFO, "child exits");
50  closelog();
51  while(1){
52  sleep(10);
53  }
54  }
55 
56 }

真正编写调试的时候会发现需要杀死守护进程。

如何杀死守护进程?

ps -aux 找到对应PID

kill -9 PID

其他参考资料:

《APUE》

《操作系统》清华大学公开课 向勇、陈渝

《嵌入式Linux应用程序开发详解》

《LInux 的僵尸(zombie)进程》http://coolshell.cn/articles/656.html

《Linux下僵尸进程的处理》http://www.mike.org.cn/articles/treatment-of-zombie-processes-under-linux/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • return 与 exit() 的区别

    用户7043923
  • RtlInitUnicodeString

    用户7043923
  • 操作系统的段机制与页机制

    8086的分段寻址,是指一个物理地址由段地址(segment selector)与偏移量(offset)两部分组成,长度各是16比特。其中段地址左移4位(即乘以...

    用户7043923
  • BLE低功耗蓝牙开发相关概念问题记录

    蓝牙ble的传输速率是指主从机每秒所传输的字节数。既然是传输速率那就涉及到时间和每次所传递包大小的问题。 关于ble通信的demo可以参考蓝牙API介绍及基...

    fanfan
  • 【答疑释惑第十一讲】开发应用主要用什么语言?

    疑惑一 开发应用主要用什么语言? 其实这个问题很多工作的,看到都觉得这个问的有点没有头绪,但是对于初学者来说还是希望更多的了解点这方面的,所谓应用开发就是处于底...

    程序员互动联盟
  • 澄清Fundebug录屏技术的几点误会

    视频中,当鼠标点击“场景重现”,会立即播放一段“视频”。它完整的记录了用户点餐时候遇到障碍之前的一段操作。这段“视频”看起来和真的视频几乎一样,所以会被误以为是...

    Fundebug
  • 用HttpContext.Current.Cache还是HttpRuntime.Cache

    今天不说.NET Core里的新MemoryCache(Microsoft.Extensions.Caching.Memory),说说目前用得最广泛的.NET ...

    崔文远TroyCui
  • Leetcode 242. Valid Anagram

    版权声明:博客文章都是作者辛苦整理的,转载请注明出处,谢谢! https://blog.csdn....

    Tyan
  • 「首席架构师推荐」统计软件包比较

    统计是汉语中的“统计”原有合计或汇总计算的意思。英语中的“统计” (Statistics) 一词来源于拉丁语status,是指各种现象的状态或状况。现今,统计一...

    首席架构师智库
  • Chrome Devtools

    (1)document.designMode = ‘on’ (2)打开任何网站,在网址栏输入:可手机端 javascdy.setAttribute(‘conte...

    jinghong

扫码关注云+社区

领取腾讯云代金券