linux学习进程控制【创建-终止-等待】

目录

1.进程创建

1.1fork函数 

1.2写时拷贝

2.进程终止

2.1进程退出场景

2.2进程退出方式 

3.进程等待

3.1进程等待的必要性 

3.2等待方式 

3.2.1wait() 

3.2.2waitpid() 

3.3轮训等待

总结:



1.进程创建

1.1fork函数 

 linuxfork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:
  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

 

fork 函数返回类型为 pid_t,相当于 typedef int,不过是专门用于进程的,同时它拥有两个返回值:

  • 如果进程创建失败,返回 -1
  • 进程创建成功后
    • 给子进程返回 0
    • 给父进程返回子进程的 PID 值
当一个进程调用 fork 之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以
开始它们自己的旅程,看如下程序

 

int main( void )
{
 pid_t pid;
 printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
 printf("After:pid is %d, fork return %d\n", getpid(), pid);
 sleep(1);
 return 0;
}

运行结果为:

运行结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
这里看到了三行输出,一行 before ,两行 after 。进程 43676 先打印 before 消息,然后它有打印 after 。另一个 after
消息有 43677 打印的。注意到进程 43677 没有打印 before ,为什么呢?如下图所示

所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器 决定。

1.2写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本,创建进程的时候,系统就会生成页表, 页表有一列特征值就是权限, 当没有发生写时拷贝的时候,权限都是只读属性,一旦父子进程有一方试图写入操作系统就会在内存中重新申请一块内存,并将页表中物理地址和内存重新映射。具体如下图所示:

注意:

  • 写时拷贝不止可以发生在常规栈区、堆区,还能发生在只读的数据段和数据段
  • 写时拷贝后,生成的是副本,不会对原数据造成影响

 

2.进程终止

2.1进程退出场景

进程退出场景包含以下三种情况:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2.2进程退出方式 

 对一个正在运行中的进程,存在两种终止方式:外部终止和内部终止,外部终止时,通过 kill -9 PID 指令(发信号),强行终止正在运行中的程序,或者通过 ctrl + c 终止前台运行中的程序(快捷键=kill-9)

 

如果是正常终止的话,可以通过 echo $?查看进程退出码 这个?就是显示最近一次进程运行的退出码,因为echo也是命令行也是进程,所以第二次输出之后就是 0 如下图所示:

 

 内部终止是通过函数 exit() 或 _exit() 实现的,同时,需要注意的是在我们之前写的main函数中,我们总会在程序执行完之后,写一个 return 0,这个0在进程信息中就代表sucess因为信号都是从1号开始的,在main函数中,return 0和exit(0)代表的含义是一样的 .

        我们重点说一下exit()和_exit().

 

zvoid exit(int status);

void _exit(int status);

这两个退出函数,从本质上来说,没有区别,都是退出进程,但在实际使用时,还是存在一些区别,推荐使用 exit()

比如在下面这段程序中,分别使用 exit() 和 _exit() 观察运行结果

int main()
{
  printf("You can see me");
  //exit(-1); //退出程序
  //_exit(-1);  //第二个函数
  return 0;
}

 使用 exit() 时,输出语句

使用 _exit() 时,输出语句

 

原因:

  • exit() 是对 _exit() 做的封装实现
  • _exit() 就只是单纯的退出程序(都要冲刷内核区)
  • 而 exit() 在退出之前还会做一些事,比如冲刷缓冲区,再调用 _exit()
  • 程序中输出语句位于输出缓冲区,不冲刷的话,是不会输出内容的

3.进程等待

3.1进程等待的必要性 

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,杀人不眨眼kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源获取子进程退出信息

3.2等待方式 

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* status);

pid_t waitpid(pid_t pid, int* status, int options);

3.2.1wait() 

wait()函数,之前有介绍过,没有说过形参status,因为接下来介绍的waitpid()包含退出状态status所以会在下面介绍 

3.2.2waitpid() 

返回值:

等待成功时,返回 >0 的值
等待失败时,返回 -1
等待中,返回 0


参数列表:

pid 表示所等子进程的 PID
status 表示状态,为整型,其中高 16 位不管,低 16 位中,次低 8 位表示退出码,第 7 位表示 core dump,低 7 位表示终止信号
options 为选项,比如可以选择父进程是否需要阻塞等待子进程退出,默认为0就是阻塞等待还有轮训等待。

status

下面通过代码演示:

int main()
{
  //演示 waitpid()
  pid_t id = fork();  //创建子进程
  if(id == 0)
  {
    int time = 5;
    int n = 0;
    while(n < time)
    {
      printf("我是子进程,我已经运行了:%d秒 PID:%d   PPID:%d\n", n + 1, getpid(), getppid());
      sleep(1);
      n++;
    }

    exit(5);  //子进程退出
  }

  int status = 0; //状态
  pid_t ret = waitpid(id, &status, 0); //参数3 为0,为默认选项

  if(ret == -1)
  {
    printf("进程等待失败!进程不存在!\n");
  }
  else if(ret == 0)
  {
    printf("子进程还在运行中!\n");
  }
  else
  {
    printf("进程等待成功,子进程已被回收\n");
  }

  printf("我是父进程, PID:%d   PPID:%d\n", getpid(), getppid());

  //通过 status 判断子进程运行情况
  if((status & 0x7F))
  {
    printf("子进程异常退出,core dump:%d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
  }
  else
  {
    printf("子进程正常退出,退出码:%d\n", (status >> 8) & 0xFF);
  }

  return 0;
}

不发出终止信号,让程序自然跑完

发出终止信号,强行终止进程

 waitpid() 的返回值可以帮助我们判断此时进程属于什么状态(在下一份测试代码中表现更明显),而 status 的不同部分,可以帮助我们判断子进程因何而终止,并获取 退出码(终止信号)

在进程的 PCB 中,包含了 int _exit_code 和 int _exit_signal 这两个信息,可以通过对 status 的位操作间接获取其中的值,同时,在子进程结束,代码和数据会释放,同时会将自己的退出码,退出信息防止在pcb中,等待父进程回收。

注意:

如果觉得 (status >> 8) & 0xFF (status & 0x7F) 这两个位运算难记,系统还提供了两个宏来简化代码

WIFEXITED(status) 判断进程退出情况,当宏为真时,表示进程正常退出
WEXITSTATUS(status) 相当于 (status >> 8) & 0xFF,直接获取退出码

3.3轮训等待

我们介绍一下,waitpid的第三个参数 options  

 

//options 参数
WNOHANG

//比如
waitpid(id, &status, WNOHANG);

 父进程并非需要一直等待子进程运行结束(阻塞等待),可以通过设置 options 参数,进程解除  状态,父进程变成 等待轮询 状态,不断获取子进程状态(是否退出),如果没退出,就可以干点其他事

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> //进程等待相关函数头文件

int main()
{
  //演示 waitpid()
  pid_t id = fork();  //创建子进程
  if(id == 0)
  {
    int time = 9;
    int n = 0;
    while(n < time)
    {
      printf("我是子进程,我已经运行了:%d秒 PID:%d   PPID:%d\n", n + 1, getpid(), getppid());
      sleep(1);
      n++;
    }

    exit(244);  //子进程退出
  }

  int status = 0; //状态
  pid_t ret = 0;
  while(1)
  {

    ret = waitpid(id, &status, WNOHANG); //参数3 设置为非阻塞状态
    
    if(ret == -1)
    {
      printf("进程等待失败!进程不存在!\n");
      break;
    }
    else if(ret == 0)
    { 
      printf("子进程还在运行中!\n");
      printf("我可以干一些其他任务\n");
      sleep(3);
    }
    else
    {
      printf("进程等待成功,子进程已被回收\n");
      //通过 status 判断子进程运行情况
      if(WIFEXITED(status))
      {
        printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
        break;
      }
      else
      {
        printf("子进程异常退出,code dump:%d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
        break;
      }
    }
  }

  return 0;
}

程序正常运行,父进程通过 等待轮询 的方式,在子进程执行的同时,执行其他任务

 

注意: 如果不写进程等待函数,会引发僵尸进程问题

总结:

进程还有进程替换,因为内容比较多,我放在下一章梳理。以上就是关于 Linux进程控制(创建、终止、等待) 的相关知识了,我们学习了 子进程 是如何被创建的,创建后又是如何终止的,以及 子进程 终止 父进程 需要做些什么,有了这些知识后,在对 进程 进行操作时能更加灵活和全面 。

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值