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

引言

在计算机科学的演进历程中,进程概念的出现标志着操作系统设计从批处理系统向多任务系统的重大飞跃。进程作为现代操作系统资源分配的基本单位,其创建、执行与终止构成了计算机程序生命周期管理的核心框架。Linux作为开源操作系统的典范,其进程管理机制体现了Unix哲学的简洁与优雅。本文旨在深入剖析Linux系统中进程创建的关键技术fork函数、进程终止的多种方式及其内在原理,为读者呈现一幅进程生命周期管理的清晰画卷。理解这些底层机制不仅是系统编程的基石,更是洞察操作系统设计思想的窗口。


  • 引言
    • 目录
    • 一、进程创建
      • [1.1 fork函数初识](#1.1 fork函数初识)
      • [1.2 fork函数返回值](#1.2 fork函数返回值)
      • [1.3 写时拷贝](#1.3 写时拷贝)
      • [1.4 fork常规用法](#1.4 fork常规用法)
      • [1.5 for调用失败的原因](#1.5 for调用失败的原因)
    • 二、进程终止
      • [2.1 进程退出场景](#2.1 进程退出场景)
      • [2.2 进程退出码](#2.2 进程退出码)
      • [2.3 strerror函数](#2.3 strerror函数)
      • [2.4 进程常见的推退出方法](#2.4 进程常见的推退出方法)
        • [2.4.1 return 和 exit 的区别](#2.4.1 return 和 exit 的区别)
        • [2.4.2 exit 和 _exit的区别](#2.4.2 exit 和 _exit的区别)
  • 总结

目录

一、进程创建

1.1 fork函数初识

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

cpp 复制代码
#include <unistd.h>//包含的头文件
pid_t fork(void);
返回值:子进程中返回0,⽗进程返回⼦进程id,出错返回-1

总而言之:

进程调用fork,当控制转移到内核中的fork代码后,内核做:

• 分配新的内存块和内核数据节后给子进程

• 将父进程部分数据结构内容拷贝到子进程中

• 添加子进程到系统进程列表当中

• fork返回,开始调度器调度


我们来看一段代码:

cpp 复制代码
#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;

运行结果:

  1. 这⾥看到了三行输出,⼀行before,两行after。进程5431先打印before消息,然后它又打印after。
  2. 另⼀个after由5432打印的。注意到进程5432没有打印before,为什么呢?如下图所⽰

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


1.2 fork函数返回值

我们先说结论:子进程返回0,父进程返回子进程的pid

所以我们可以利用这个结论来让父进程和子进程执行不同的执行流(代码块)

观察如下代码:

cpp 复制代码
#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 写时拷贝

关于写实拷贝,我们有在进程概念(六)中简单提到过【点击进入】

  • 我们通过下面这张图更加深入的了解写时拷贝

写时拷贝是操作系统为优化进程创建而设计的内存管理策略。当父进程创建子进程时,操作系统并不立即复制父进程的数据段,而是让父子进程共享同一物理内存页,仅将相关页表项标记为只读权限。

  1. 数据结构继承:子进程的内核数据结构完全以父进程为模板,包括:

    • 进程控制块(task_struct)
    • 内存描述符(mm_struct)
    • 页表结构
  2. 权限设置

    • 代码段:保持只读权限(代码本身不应修改)
    • 数据段:原本可读写的页被强制设置为只读权限,为写时拷贝做准备
  3. 当任一进程尝试修改共享数据时:

    • 访问异常:CPU检测到对只读页的写操作,触发页保护异常
    • 异常转换:操作系统识别此为写时拷贝场景,将异常转换为缺页中断
    • 内存分配内核分配新的物理内存页
    • 数据拷贝将原页内容复制到新页
    • 权限更新更新页表项,指向新物理页并恢复可读写权限

因为有写时拷贝技术的存在,所以父子进程得以彻底分离离!完成了进程独立性的技术保证! 写时拷贝是⼀种延时申请技术,可以提高整机内存的使用率。


1.4 fork常规用法

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

1.5 for调用失败的原因

fork函数创建子进程也可能会失败,有以下两种情况:

  1. 系统中有太多的进程,内存空间不足,子进程创建失败。
  2. 实际用户的进程数超过了限制,子进程创建失败。

二、进程终止

2.1 进程退出场景

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果错误
  3. 代码异常终止

2.2 进程退出码

我们看一段代码:

cpp 复制代码
#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函数可以用来获取退出码的对应的描述。

cpp 复制代码
#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() 返回给父进程。

代码如下:

cpp 复制代码
#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 $?查看进程退出码)

  1. main返回
  2. 调用exit
  3. _exit

异常退出

  1. ctrl + c,信号终止

returnexit 的区别


2.4.1 return 和 exit 的区别

区别一:

我们观看图片可以看到exit是包含在 stdlib.h 的头文件,然后参数status如下:

复制代码
exit(0);   // 表示成功(最常用)
exit(1);   // 表示失败
exit(42);  // 自定义退出码(0-255之间)

代码示例如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
	printf("hello linux\n");

	//exit(17);
	//return 7;
}

结果如下:

我们可以发现除了0和1,其他status是在0~255之间随机。


区别二:

cpp 复制代码
#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的

cpp 复制代码
#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

🌱 代码仓库不呆头 · Gitee

📌 专栏系列

💬 座右铭 : "不患无位,患所以立。"

相关推荐
翼龙云_cloud1 小时前
阿里云渠道商:连接无影云电脑时最常见的问题有哪些?
服务器·阿里云·云计算·电脑·玩游戏
源代码•宸2 小时前
GoLang并发示例代码2(关于逻辑处理器运行顺序)
服务器·开发语言·经验分享·后端·golang
橘子真甜~2 小时前
C/C++ Linux网络编程9 - TCP服务器实现流程和独立运行
linux·运维·服务器·c++·守护进程·会话组
weixin_307779133 小时前
Jenkins GitHub插件1.45.0:深度集成与实践指南
运维·云原生·云计算·jenkins
_dindong9 小时前
Linux网络编程:结合内核数据结构详谈epoll的工作原理
linux·服务器·网络
了一梨9 小时前
在Ubuntu中配置适配泰山派的交叉编译环境
linux·c语言·ubuntu
buyutang_9 小时前
Linux网络编程:Socket套接字编程概念及常用API接口介绍
linux·服务器·网络·tcp/ip
小小哭包9 小时前
Nginx配置文件nginx.conf中文详解
运维·nginx
weixin_431697209 小时前
onlyoffice预览nginx代理的静态文件
运维·nginx