惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
Stack Overflow Blog
Stack Overflow Blog
人人都是产品经理
人人都是产品经理
The GitHub Blog
The GitHub Blog
Engineering at Meta
Engineering at Meta
Vercel News
Vercel News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Y
Y Combinator Blog
The Cloudflare Blog
Last Week in AI
Last Week in AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
Google DeepMind News
Google DeepMind News
D
DataBreaches.Net
博客园 - 司徒正美
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
L
LangChain Blog

博客园 - AI产品观察

公式截图怎么转LaTeX代码?这个免费工具一键搞定 免费白嫖Google顶尖算力?深度揭秘这个满血版Gemini 3 Pro体验入口 深度解析 VoGen:2026 年最强免费 AI 语音合成与情感克隆技术方案 分享一个网站,Crealens.ai - 可以免费体验 AI图像/视频 生成-编辑-创作 WriteRightly 这个AI 可以像教授一样批改论文 免费、快速、可靠:揭秘IsGPT如何精准检测AI内容 Crealens.ai 免费体验GPT-4o 生图+吉卜力风格化 目标检测-基于Pytorch实现Yolov3(1)- 搭建模型 01布尔模型&倒排索引 openpose模型在AI challenge人体骨骼关键点检测的表现 隐马尔可夫模型(HMM) 概率生成模型 卷积神经网络经验-CS231n笔记 双显卡笔记本安装CUDA+theano、tensorflow环境 Exynos 4412 Uboot源码解析 隐马尔可夫模型 基于WDF的PCI/PCIe接口卡Windows驱动程序(5)-如何为硬件移植驱动程序 贪吃蛇游戏C语言源代码学习 经典功率谱估计及Matlab仿真 CAN总线(一)
进程详解(1)——可能是最深入浅出的进程学习笔记
AI产品观察 · 2016-03-24 · via 博客园 - AI产品观察
原文地址: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() 没有修改函数栈,所以,父进程得以顺利执行

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应用程序开发详解》