【Linux】进程控制(一):进程的创建和终止

引言
在计算机科学的演进历程中,进程概念的出现标志着操作系统设计从批处理系统向多任务系统的重大飞跃。进程作为现代操作系统资源分配的基本单位,其创建、执行与终止构成了计算机程序生命周期管理的核心框架。Linux作为开源操作系统的典范,其进程管理机制体现了Unix哲学的简洁与优雅。本文旨在深入剖析Linux系统中进程创建的关键技术fork函数、进程终止的多种方式及其内在原理,为读者呈现一幅进程生命周期管理的清晰画卷。理解这些底层机制不仅是系统编程的基石,更是洞察操作系统设计思想的窗口。
目录
一、进程创建
1.1 fork函数初识
在进程概念(一)中已经讲解大部分fork知识【点击进入】
在Linux中fork是一个非常重要的函数,它从已存在的进程中创建一个新的进程 。新的进程为子进程,而原来的进程为父进程。
#include <unistd.h>//包含的头文件
pid_t fork(void);
返回值:子进程中返回0,⽗进程返回⼦进程id,出错返回-1
总而言之:
进程调用fork,当控制转移到内核中的fork代码后,内核做:
• 分配新的内存块和内核数据节后给子进程
• 将父进程部分数据结构内容拷贝到子进程中
• 添加子进程到系统进程列表当中
• fork返回,开始调度器调度
我们来看一段代码:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
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;
运行结果:
这⾥看到了三行输出,⼀行before,两行after。进程5431先打印before消息,然后它又打印after。
另⼀个after由5432打印的。注意到进程5432没有打印before,为什么呢?如下图所⽰
结论:所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
1.2 fork函数返回值
我们先说结论:子进程返回0,父进程返回子进程的pid
所以我们可以利用这个结论来让父进程和子进程执行不同的执行流(代码块)
观察如下代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
  printf("i am parent,pid: %d, ppid %d\n\n", getpid(), getppid());
  pid_t id = fork();
  if(id == 0)
  {
    while(1)
    {
      printf("i am child,pid: %d, ppid %d\n", getpid(), getppid());
      sleep(1);
    }
  }
  else if(id > 0)
  {
    while(1)
    {
      printf("i am parent,pid: %d, ppid %d\n", getpid(), getppid());
      sleep(1);
    }
  }
  else
  {
  peeor("fork()");
  exit(1);
}
  return 0;
}
1.3 写时拷贝
关于写实拷贝,我们有在进程概念(六)中简单提到过:【点击进入】
我们通过下面这张图更加深入的了解写时拷贝
写时拷贝是操作系统为优化进程创建而设计的内存管理策略。当父进程创建子进程时,操作系统并不立即复制父进程的数据段,而是让父子进程共享同一物理内存页,仅将相关页表项标记为只读权限。
数据结构继承:子进程的内核数据结构完全以父进程为模板,包括:
进程控制块(task_struct)
内存描述符(mm_struct)
页表结构
权限设置:
代码段:保持只读权限(代码本身不应修改)
数据段:原本可读写的页被强制设置为只读权限,为写时拷贝做准备
当任一进程尝试修改共享数据时:
访问异常:CPU检测到对只读页的写操作,触发页保护异常
异常转换:操作系统识别此为写时拷贝场景,将异常转换为缺页中断
内存分配:内核分配新的物理内存页
数据拷贝:将原页内容复制到新页
权限更新:更新页表项,指向新物理页并恢复可读写权限
因为有写时拷贝技术的存在,所以父子进程得以彻底分离离!完成了进程独立性的技术保证! 写时拷贝是⼀种延时申请技术,可以提高整机内存的使用率。
1.4 fork常规用法
一个进程希望复制自己,使子进程同时执行不同的代码段。例如父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.5 for调用失败的原因
fork函数创建子进程也可能会失败,有以下两种情况:
系统中有太多的进程,内存空间不足,子进程创建失败。
实际用户的进程数超过了限制,子进程创建失败。
二、进程终止
2.1 进程退出场景
代码运行完毕,结果正确
代码运行完毕,结果错误
代码异常终止
2.2 进程退出码
我们看一段代码:
#include <stdio.h>
int main()
{
  printf("hello world\n");
  return 0;
}
我们从最开始学习C语言的时候结尾都是return 0,那么有没有考虑过为什么是return 0呢?
当我们用echo $?查看最近一次进程返回的退出码的时候发现也是0,这是为什么呢,有没有可能返回1,2,3等等?
其实0是程序执行成功后返回的退出码,除0以外的任何返回的退出码都是都说明有错误,以下图片介绍一下常见的退出码及其含义:
退出码 0 表⽰命令执⾏⽆误,这是完成命令的理想状态。
退出码 1我们也可以将其解释为 “不被允许的操作”。例如在没有 sudo 权限的情况下使⽤yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 let a = 1/0
130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等终⽌信号是⾮常典型的,它们属于 128+n 信号,其中 n 代表终⽌码。
可以使⽤strerror函数来获取退出码对应的描述
2.3 strerror函数
strerror函数可以用来获取退出码的对应的描述。
#include <stdio.h>
#include <string.h>
int main()
{
  int i = 0;
  for(; i < 200; i++)
  {
    printf("%d: %s\n", i, strerror(i));
  }
  return 0;
}
这里我们可以看到从0~133个错误码,其实具体个数取决于内核版本和配置。
由于上述的原理加上我们知道error储存最近一次的错误码我们可以得出一个结论:程序运行中发生错误时,可以通过 errno 获取系统错误码,并使用 strerror() 将其转换为可读信息。
程序可以根据错误类型,映射到合适的退出码,通过 exit() 返回给父进程。
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
  int ret = 0;
  char* daitou = (char*)malloc(4000000000);
  if(daitou == NULL)
  {
    ret = errno;
    printf("%d: %s\n",errno, strerror(errno));
  }
  else
  {
    printf("malloc success\n");
  }
  return ret;
}
结果:
如此我们便能够看到通过 errno 获取系统错误码12并且映射到合适的退出码,并使用 strerror() 将其转换为可读信息:无法分配内存 。
2.4 进程常见的推退出方法
正常终止(可以通过echo $?查看进程退出码)
从main返回
调用exit
_exit
异常退出:
5. ctrl + c,信号终止
return 和 exit 的区别
2.4.1 return 和 exit 的区别
区别一:
我们观看图片可以看到exit是包含在 stdlib.h 的头文件 ,然后参数status如下:
exit(0);   // 表示成功(最常用)
exit(1);   // 表示失败
exit(42);  // 自定义退出码(0-255之间)
代码示例如下:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello linux\n");
//exit(17);
//return 7;
}
结果如下:
我们可以发现除了0和1,其他status是在0~255之间随机。
区别二:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void show()
{
printf("hello linux, begin\n");
printf("hello linux, begin\n");
printf("hello linux, begin\n");
  //return;
  //exit(11)
printf("hello linux, end\n");
printf("hello linux, end\n");
printf("hello linux, end\n");
}
int main()
{
show();
printf("hello linux\n");
return 0;
}
即任意位置exit被调用都表示进程直接退出,但是return在其它函数中被调用只表示当前函数返回。
2.4.2 exit 和 _exit的区别
exit的代码现象上述已经展示过,我们来看看_exit的
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
  printf("hello linux");
  exit(17);
  _exit(17);
}
我们的hello linux并没有打印,原因我们调用的printf是将数据写入缓冲区,当遇到\n或者进程return或者exit会自动刷新缓冲区的内容
如果既没有\n也没有return也没有exit,所以自然而然我们的hello linux并不会打印所以我们可以得出 _echo并不会刷新缓冲区的内容
关于缓冲区的位置,我们在接下来的内容进行讲解!
总结
Linux进程管理机制的精髓在于平衡效率与安全性 ,兼顾资源利用与进程隔离。从fork函数的写时拷贝优化到exit系列函数的缓冲区处理,每一个设计决策都体现了操作系统设计者对性能与稳定性的深刻思考。进程退出码体系不仅为程序间通信提供了标准化接口,更是构建可靠软件系统的重要保障。通过深入理解进程创建与终止的内在机制,开发者能够编写出更加健壮、高效的系统程序,在资源受限的环境中实现最优的性能表现。这些底层知识构成了系统编程的坚实基础,也是通往高级操作系统理解的必经之路。
————————————————
版权声明:本文为CSDN博主「Halcyon.平安」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2401_86326742/article/details/155543213
上一篇 Smartctl
下一篇 请大家帮我看一下这个ip地址划分后是不是对的?