Posts Linux系统编程02_多进程
Post
Cancel

Linux系统编程02_多进程

1. fork

1.1 基本解释

函数思想:fork()前 , 代码公用 , fork() 后, 代码公用 。仅凭借fork()的返回值判断为父进程还是子进程

父进程返回子进程pid 子进程返回0

1.2 代码

getpid() :获得当前进程的pid getppid():获得父进程的pid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(){

    pid_t pid = 0;
    pid = fork();

    if  (pid == 0){
        // 子进程
        printf("son process.%d\n",getpid());
    } else if( pid == -1 ){
        perror("prcoess error%d\n");
    }else if (pid > 0)
    {
        printf(" father process\n");
    }
    
    printf("-----------------------------\n");
    return 0;

}

2 进程共享

2.1 读时共享,写时复制

  • 相同部分: 全局变量、.data、.text、栈、堆、环境变量、用户 ID、宿主目录、进程工 作目录、信号处理方式…

  • 不同部分: 进程 ID 2.fork 返回值 3.父进程 ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集

父子进程间遵循读时共享写时复制的原则。 这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。而不是简单的复制0-3G用户空间内容.

2.2 父子进程真正共享的部分

  1. 文件描述符
  2. mmap映射区

2.3 代码案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int var = 100;

int main(){

    int flag = fork();
    if (flag == -1)
    {
        perror(" new fork falut");
        exit(1);
    }

    if ( flag == 0)
    {
        var = 101;
    }else{
        var = 102;
    }

    printf("%d\n",var);
    
}

3. execl

fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。

3.1 例子

执行环境变量 下的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(){

    pid_t pid = fork();

if (pid == -1  )
{
    perror(" fork error");
    exit(1);
} else if (pid == 0)
{
    execlp("ls","ls","-l",NULL);    
    perror("exec error");
    exit(1);
}else if(pid > 0){
    sleep(1);
    printf("I m parent");
    
}
return 0;
}

执行相对路径下的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(){

    pid_t pid = fork();

if (pid == -1  )
{
    perror(" fork error");
    exit(1);
} else if (pid == 0)
{
    // execlp("ls","ls","-l",NULL);    
    execl("./out.out","out.out",NULL);
    perror("exec error");
    exit(1);
}else if(pid > 0){
    sleep(1);
    printf("I m parent");
}
return 0;
}

4. 僵尸进程与孤儿进程

4.1 孤儿进程

父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为 init进程,称为 init 进程领养孤儿进程。 由init自动回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
int main()
{
  pid_t pid ;
  pid = fork();
  if(pid<0)
  {
  	perror("fork error");
  	
  }
  else if(pid==0)
  {
  	while(1)
  	{
  	  printf("I am child pid =%d my father pid=%d\n",getpid(),getppid());
  	  sleep(1);
	}
  }
  else
  {
  	printf("I am father pid =%d \n",getpid());
  	sleep(4);
  	printf("I am died\n");
  	return 0;
  }
  return 0;
}

4.2 僵尸进程

子进程终止,父进程没有及时回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。

比如父进程一直在while() 空循环等,没有及时回收子进程。

  • 如何解决僵尸进程?

如何解决问题呢,此时杀死父进程,父进程转变成init。init发现子进程是僵尸,自动回收。

4.3 孤儿进程 僵尸进程 守护进程 区别

  1. 孤儿进程

子进程还在继续运行,父进程挂掉了。子进程的父进程变为init进程,由init进程自动回收。

  1. 僵尸进程

子进程结束,应当由父进程进行回收。这时父进程一直在执行,例如一个while(1)循环,抽不出时间来,此时子进程既不能被init接管,也不能被父进程回收。

  1. 守护进程 暂略,后面来补充。

4.3 一些细节

博客

什么是僵尸进程?Unix进程模型中,进程是按照父进程产生子进程,子进程产生子子进程这样的方式创建出完成各项相互协作功能的进程的。当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。如果父进程没有这么做的话,会产生什么后果呢?此时,子进程虽然已经退出了,但是在系统进程表中还为它保留了一些退出状态的信息,如果父进程一直不取得这些退出信息的话,这些进程表项就将一直被占用,此时,这些占着茅坑不拉屎的子进程就成为“僵尸进程”(zombie)。系统进程表是一项有限资源,如果系统进程表被僵尸进程耗尽的话,系统就可能无法创建新的进程。

那么,孤儿进程又是怎么回事呢?孤儿进程是指这样一类进程:在进程还未退出之前,它的父进程就已经退出了,一个没有了父进程的子进程就是一个孤儿进程(orphan)。既然所有进程都必须在退出之后被wait()或waitpid()以释放其遗留在系统中的一些资源,那么应该由谁来处理孤儿进程的善后事宜呢?这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程“凄凉地”结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。

这样来看,孤儿进程并不会有什么危害,真正会对系统构成威胁的是僵尸进程。那么,什么情况下僵尸进程会威胁系统的稳定呢?设想有这样一个父进程:它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵尸进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。严格地来说,僵尸进程并不是问题的根源,罪魁祸首是产生出大量僵尸进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵尸进程时,答案就是把产生大量僵尸进程的那个元凶枪毙掉(通过kill发送SIGTERM或者SIGKILL信号)。枪毙了元凶进程之后,它产生的僵尸进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经“僵尸”的孤儿进程就能瞑目而去了。

5. wait

5.1 wait作用

  1. 阻塞等待子进程退出
  2. 回收子进程残留资源
  3. 获取子进程结束状态(退出原因)。

5.2 wait示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main(){
    pid_t pid,wpid;
    int status;

    pid = fork();
    if(pid == 0){
        printf(" child,my id = %d,going to sleep 10 s\n",getpid());
        sleep(10);
        printf("child is going to died\n");
    }else if(pid > 0){
        wpid = wait(&status);
        if (wpid == -1)
        {
            // 进程回收失败
            perror("wait error");
            exit(1);
        }
        printf("praent wait finish: wpid = %d\n",wpid);
        
    }else{
        perror("fork error");
        return 1;
    }
}

5.3 传出参数 status 详解

  • pid_t wait(int *status);
  • 成功:清理掉的子进程 ID;失败:-1 (没有子进程)

  • WIFEXITED(status)宏判断为真 表示程序正常退出
  • WEXITSTATUS(status)上一个宏判断为真 则返回子进程的函数返回值
  • WIFSIGNALED(status) 宏判断为真 表示程序异常退出
  • WTERMSIG(status) 上一个判断为真,则返回状态值
1
2
3
4
5
6
7
8
9
10
11
12
		wpid=wait(NULL);//不关心怎么结束的
		wpid = wait(&status);//等待子进程结束

		if(WIFEXITED(status))//判断 子进程正常退出判断
		{
			printf("child exit with%d\n",WEXITSTATUS(status));
			printf("------parent  finish\n");
		}
		if(WIFSIGNALED(status))//判断 子进程异常退出判断   信号量退出
		{
			printf("child exit with%d\n",WTERMSIG(status));
		}

6.waitpid()

父进程调用wait() 函数, 如果有多个子进程,那么父进程会回收哪个进程?

waitpid() , 指定子进程pid进行回收。

6.1 函数原型

pid_t waitpid(pid_t pid, int *status, in options); 成功: 返回回收的子进程pid 失败: 返回 -1

参数详解:

  • pid

pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。 pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。 pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。 pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

  • status 略

  • options WNOHANG: 设置非阻塞与阻塞。 若子进程正在运行,直接返回0;

注意,一个wait() 或者 waitpid() 一次只能回收一个子进程,如果要清理多个子进程,应该使用循环

6.2 清理指定的子线程 ( 错误答案)

错误原因 : 子进程的tmpid 不能与父进程共享,读时共享,写时拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int main(){
    int i;
    pid_t pid,wpid,tmpid = 0;

    for(i = 0; i<5;i++){
        pid = fork();

        if  (pid == 0){
            // 新创建的子进程
            if  ( i == 2){
                tmpid = getpid();
                printf(" when i == 2, chilidren pid = : %d\n",tmpid);
            }
            break;
            }
        }

        if( i == 5){
            sleep(1);
            // 父进程
        printf(" parent process , tempid ==: %d\n",tmpid);
        printf(" I'm parent ,pid = %d\n",getpid());
        // wpid = waitpid(tmpid,NULL,0);   阻塞wait
        wpid = waitpid(tmpid,NULL,WNOHANG);     // 非阻塞wait,此时 tmpid 并不是 i = 2 时 的pid。
        printf(" kill children process, wpid = %d\n",wpid);
        }else{
            printf(" I'm children ,pid = %d\n",getpid());
        }
        
    return 0;
}

6.3 清理指定的子线程 ( 正确答案)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int main(){
    int i;
    pid_t pid,wpid,tmpid = 0;

    for(i = 0; i<5;i++){
        pid = fork();

        if  (pid == 0){
            break;
            }

        if  ( i == 2){
            tmpid = pid;
            printf(" when i == 2, chilidren pid = : %d\n",pid);
            }
        }

        if( i == 5){
            sleep(1);
            // 父进程
        printf(" parent process , tempid ==: %d\n",tmpid);
        printf(" I'm parent ,pid = %d\n",getpid());
        // wpid = waitpid(tmpid,NULL,0);   阻塞wait
        wpid = waitpid(tmpid,NULL,WNOHANG);     // 非阻塞wait,此时 tmpid 并不是 i = 2 时 的pid。
        printf(" kill children process, wpid = %d\n",wpid);
        }else{
            printf(" I'm children ,pid = %d\n",getpid());
        }
        
    return 0;
}

6.4 回收多个子线程

循环

This post is licensed under CC BY 4.0 by the author.

Contents

Trending Tags