【Linux】—— 进程的创建和退出

序言:

在上期,我们已经对 Linux的进程的相关知识进行了相关的学习。接下来,我们要学习的便是关于进程控制 的基本知识!!!

目录

(一)进程创建

1、fork函数初识

2、写时拷贝

3、fork使用场景

4、fork调用失败的原因

(二)进程终止

1、进程退出场景

2、进程常见退出方法

1️⃣echo $?

 2️⃣return退出

3️⃣exit函数

4️⃣_exit函数 

5️⃣exit 和 _exit 的区别

总结


(一)进程创建

1、fork函数初识

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

fork()函数,其原型如下:

#include <unistd.h>
pid_t fork(void);

返回值:自进程中返回 0,父进程返回子进程id,出错返回 -1
fork() 函数不需要参数,返回值是一个进程标识符 PID 。返回值有以下三种情况:
  • 1) 对于父进程,fork()函数返回新创建的子进程的PID
  • 2) 对于子进程,fork()函数调用成功会返回0
  • 3) 如果创建出错,fork()函数返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:
  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝至子进程
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度

当一个进程调用 fork 之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程。
  • 具体看如下程序:

  #include<stdio.h>
  #include<unistd.h>
  #include<stdlib.h>
  
  int main(  )
  {
    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;
 }

输出结果如下:

 【说明】

  • 这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有 20348 打印的。注意到进程 20348 没有打印before,为什么呢?
如下图所示:

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

2、写时拷贝

创建进程的时候,会使用写时拷贝技术来优化内存资源的使用,避免不必要的内存复制。

写时拷贝是一种延迟内存复制的策略,在创建子进程时不会立即复制整个父进程的内存空间,而是让父进程和子进程共享同一份物理内存。只有在需要修改共享内存内容的时候,才会进行实际的内存复制,以保证各个进程之间的隔离性。

  • 下面是一个简单的示例代码,展示了创建进程时的写时拷贝特性:
#include <stdio.h>
#include <unistd.h>

int main() 
{
    int data = 123;
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        return 1;
    } 
    else if (pid == 0) {
        // 子进程
        printf("Child: data = %d\n", data);
        data = 456;  // 修改共享内存中的内容
        printf("Child: modified data = %d\n", data);
    } 
    else {
        // 父进程
        sleep(1);  // 等待子进程执行完毕
        printf("Parent: data = %d\n", data);
    }

    return 0;
}

运行这段代码,输出结果如下:

 【说明】

  1.  可以看到,父进程和子进程在创建时共享了同一份内存空间,即data变量;
  2. 当子进程对data进行修改后,父进程的data并没有发生变化,实现了进程之间的隔离。这就是写时拷贝技术的效果。

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:


3、fork使用场景

  • 1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。
  • 2、一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

4、fork调用失败的原因

fork()调用失败的原因可能有以下几种情况:

  1. 系统资源不足:当系统没有足够的资源(如可用内存、进程表项等)来创建新的进程时,fork()调用可能会失败。
  2. 达到了进程限制:系统对同时存在的进程数量设置了限制,如果已经达到了这个限制,再进行fork()调用就会失败。
  3. 进程权限不足:如果当前进程不具有创建子进程的权限,例如在特殊的用户模式下或者进程受到了某些权限限制,fork()调用也会失败。

(二)进程终止

1、进程退出场景

有多种场景会导致进程退出,以下是一些常见的进程退出场景:

  1. 正常退出:进程执行完了所有的任务,或者遇到了终止条件,主动调用了exit()函数或返回了main()函数的末尾,从而正常退出进程。

  2. 异常退出:进程在执行过程中遇到了错误或异常情况,无法继续执行下去,例如访问非法内存、除以零等。此时,操作系统会将异常信息传递给进程的错误处理机制,进程可能会执行相应的错误处理操作后终止。

  3. 进程被操作系统终止:操作系统可能会因为某些原因(如内存资源紧张、系统崩溃等)需要终止进程,从而导致进程退出。

这些仅是一些常见的进程退出场景,具体情况还可能根据操作系统和应用程序的不同而有所差异。处理进程退出的方式也会因为不同的场景而有所变化,例如通过信号处理函数、异常处理机制等来进行适当的处理。


2、进程常见退出方法

不知道大家是否知道每次写代码都会在末尾加上return 0; 这是为什么?

  1. 在C/C++程序中,最后的return 0;语句用于指示程序的正常退出,并且返回退出码为0
  2. 在UNIX/Linux系统中,退出码为0表示程序成功地执行完成并且没有发生错误

退出码是一个整数值,通常在范围0-255之间。不同的退出码可以用于指示不同的情况,例如指示程序的执行状态、错误码、操作结果等。在编写C/C++程序时,根据程序的需要,可以使用不同的退出码。

一般来说,返回0作为退出码主要用于表示程序成功执行完成,并且没有发生任何错误。这在脚本或其他程序通过检查退出码来判断程序是否成功执行时非常有用。如果程序成功执行完成,它将返回0,否则可能返回其他非零的退出码,以表示不同的错误情况。


1️⃣echo $?

echo $?是一个命令,用于在终端中显示上一个执行的命令的退出状态码。在大多数操作系统中,退出状态码为0表示上一个命令正常执行完成,非零的状态码通常表示出现了错误或异常情况。

当你在终端中执行echo $?时,它会显示上一个命令的退出状态码。例如,如果你之前执行了一个成功的命令,那么echo $?将显示0。如果上一个命令出现了错误,那么echo $?将显示非零的状态码,以反映出错的原因。

代码演示:

#include <stdio.h>

int main() 
{
    int sum = 0;

    for (int i = 1; i <= 100; i++) {
        sum += i;
    }

    if (sum == 5050) {
        printf("和为5050\n");
        return 0;
    } 
    else {
        printf("和不为5050\n");
        return 1;
    }
}

结果展示:

当我们把循环体的范围改为 i<=101; 在查看此时的退出码为多少:

 当我们把错误时的退出码改为 return 11;  在查看此时的退出码为多少:

 【注意】


正常终止(可以通过 echo $? 查看进程退出码)
  • 1. main返回
  • 2. 调用exit
  • 3. _exit
异常退出
  • ctrl + c,信号终止

 2️⃣return退出

return 是一种更常见的退出进程方法。执行 return n 等同于执行 exit(n), 因为调用 main 的运行时函数会将 main 的返回值当做 exit 的参数。

3️⃣exit函数

原型如下:

#include <unistd.h>
void exit(int status);

 exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

1. 执行用户通过 atexit on_exit 定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用 _exit

代码示例:


  #include<stdio.h>
  #include<unistd.h>
  #include<stdlib.h>
   
  int add_to_top(int top)
  {
      printf("enter add_to_top\n");
      int sum = 0;
      for(int i = 0; i <= top; i++)
      {
          sum += i;
      }
      exit(123);
      printf("out add_to_top\n");
      return sum;
  }
  
  int main()
  {
  
      int result = add_to_top(100);
      if(result == 5050)                                                                                                                                               
        return 0;
      else
        return 11; //计算结果不正确
  }

结果展示:

4️⃣_exit函数 

原型如下:

#include <unistd.h>
void _exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值
【说明】
  • 虽然statusint,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255

结果展示:


5️⃣exit 和 _exit 的区别

exit()_exit()是两个不同的函数,用于终止程序的执行,但它们有一些区别:

exit()

  • exit()是C和C++标准库中提供的函数。
  • 调用exit()函数将导致正常的程序终止流程。它首先执行注册的终止处理函数(通过atexit()函数注册的函数),然后关闭所有标准I/O流(例如stdinstdoutstderr),最后返回到操作系统。
  • 在执行exit()之前,还会对静态全局变量、全局对象析构函数、自动变量和动态内存进行清理和释放。
  • exit()还允许程序向操作系统返回一个退出状态码,这个状态码可以在其他程序或脚本中用来判断程序的执行结果。

_exit()

  • _exit()是POSIX标准库中提供的函数。
  • 调用_exit()函数将直接终止程序的执行,不会执行任何清理操作。它会立即关闭进程并返回到操作系统,不会执行终止处理函数、关闭标准I/O流、清理静态全局变量等操作。
  • _exit()函数并不返回任何退出状态码给操作系统。

总结

以上便是进程创建和退出的全部知识了。接下来,我们简单回顾下本文内容!!!

进程的创建:

  1. 进程是执行中的程序实例,它由操作系统创建和管理。
  2. 进程的创建通常是通过调用一个特定的系统调用(例如fork())来实现的。
  3. 在进程创建时,操作系统会为新进程分配资源,包括内存空间、文件描述符等。

进程的退出:

  1. 进程可以正常退出或异常退出。
  2. 正常退出是进程完成其任务后自愿终止执行,通常使用exit()函数来终止进程。
  3. 异常退出是进程在遇到错误、异常或致命错误时非自愿地终止执行,操作系统会接收到相应的信号并处理。
  4. 进程退出时,操作系统会清理该进程占用的资源,释放内存、关闭文件描述符等。
  5. 在进程退出时,还可以返回一个退出状态码给父进程或操作系统,用来表示进程的执行结果。

小结:

  • 进程的创建是通过调用系统调用来实现的,操作系统为新进程分配资源并初始化其属性。
  • 进程的退出分为正常退出和异常退出,正常退出是进程自愿终止执行,异常退出是非自愿地终止执行。
  • 进程退出时,操作系统会清理资源并释放内存。可以返回一个退出状态码,用来表示进程的执行结果。

到此,本文的知识点便全部讲解完毕了。感谢大家的观看与支持!!!

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
目录 1 进程的组织 5 1.1 进程相关数据结构 5 1.1.1 进程的基本信息 6 1.1.2 进程状态 10 1.1.3 TASK_RUNNING状态的进程链表 11 1.1.4 进程间关系 12 1.2 Linux的线程——轻量级进程 15 1.3 进程创建——do_fork()函数详解 19 1.4 执行进程间切换 33 1.4.1 进程切换之前的工作 33 1.4.2 进程切换实务 —— switch_to宏 37 1.4.3 __switch_to函数 39 1.5 fork与vfock系统调用的区别 42 1.6 内核线程 46 1.7 挂起状态进程的组织 49 1.7.1 等待队列头 49 1.7.2 等待队列的操作 50 1.7.3 进程资源限制 55 1.8 系统调用execve() 56 1.8.1 拷贝用户态参数 57 1.8.2 重要的数据结构 61 1.8.3 search_binary_handler函数 66 1.8.4 目标文件的装载和投入运行 69 1.8.5 库函数 92 2 中断控制 94 2.1 中断的分类 94 2.2 中断的硬件环境 95 2.2.1 外部中断请求IRQ 95 2.2.2 中断描述符表 96 2.2.3 中断和异常的硬件处理 97 2.3 中断描述符表 99 2.3.1 中断门、陷阱门及系统门 99 2.3.2 IDT的初步初始化 100 2.4 异常处理 101 2.5 中断处理 106 2.5.1 中断向量 107 2.5.2 IRQ数据结构 108 2.5.3 do_IRQ()函数 113 2.5.4 中断服务例程 115 2.5.5 IRQ线的动态分配 116 2.6 下半部分 117 2.6.1 软中断 118 2.6.2 tasklet 121 2.6.3 工作队列 122 2.7定时器中断 124 2.7.1 时钟与定时器 124 2.7.2 定时器中断相关的数据结构 127 2.7.3 定时器中断的上半部分 129 3 进程调度 138 3.1 进程调度的概念 138 3.2 进程调度的数据结构和优先级 141 3.2.1 进程的优先级 141 3.2.2 数据结构 145 3.3 调度程序所使用的函数 151 3.3.1 scheduler_tick函数 151 3.3.2 try_to_wake_up函数 156 3.3.3 recalc_task_prio函数 160 3.4 schedule()函数 163 3.4.1 直接调用 163 3.4.2 延迟调用 164 3.4.3 进程切换之前所做的工作 168 3.4.4 完成进程切换时所执行的操作 171 3.4.5 进程切换后所执行的操作 173 3.5 多处理器运行队列的平衡 175 3.5.1 调度域 176 3.5.2 rebalance_tick()函数 178 3.5.3 load_balance()函数 180 3.5.4 move_tasks()函数 183 3.6 进程退出 187 3.6.1 进程终止 187 3.6.2 进程删除 189 4 进程的并发性体现 191 4.1 内核抢占 193 4.1.1 内核抢占概念 193 4.1.2 同步技术总揽 196 4.2 每CPU变量 197 4.3 原子操作 199 4.4 优化屏障和内存壁垒 203 4.4.1 优化屏障 204 4.4.2 内存壁垒 204 4.5 自旋锁 206 4.6 读写自旋锁 211 4.6.1 为读获取和释放一个锁 213 4.6.2 为写获取或释放一个锁 214 4.7 顺序锁 215 4.8 RCU机制 217 4.9 信号量 219 4.9.1 获取和释放信号量 221 4.9.2 读/写信号量 224 4.9.3 补充信号量 225 4.10 禁止本地中断 226 4.10.1 禁止本地中断 227 4.10.2 禁止下半部(可延迟函数) 229 4.11 一些避免竞争条件的实例 231 4.11.1 引用计数器 231 4.11.2 大内核锁 231 4.11.3 内存描述符读/写信号量 232 4.11.4 slab高速缓存链表的信号量 233 4.11.5 索引节点的信号量 233 4.12 内核同步与互斥的总结 233

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

起飞的风筝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值