当前位置: 首页 > news >正文

LINUX基础 [三] - 进程创建

目录

前言

 进程创建的初次了解(创建进程的原理)

什么是fork函数?

初识fork函数

写时拷贝

fork函数存在的意义

fork调用失败的原因 

进程终止

运行完毕结果不正确

 main函数返回

库函数函数exit 

系统调用接口_exit

进程异常终止

进程等待

进程等待是什么

进程等待为什么要进行

进程等待怎么做

阻塞和非阻塞轮询


前言

上节我们已经讲了进程的概念了,大家应该对进程有感悟同时也有更深入的思考,上节课介绍了那么多进程,但是进程该怎么创建呢,今天就来给大家讲解一下 进程的创建

 进程创建的初次了解(创建进程的原理)

创建新进程在Linux的下是由父进程来完成的,创建完成的新进程是子进程。
新进程的地址空间有两种可能性:

子进程是父进程的复制品(除了PID和task_struct是子进程自己的,其余的都从父进程复制而来)
子进程装入另一个程序。
在Linux下的fork函数用于创建一个新的进程,使用fork函数来创建一个进程时,子进程只是完全复制父进程的资源。这样得到的子进程和父进程是独立的,具有良好的并发性。但是进程间通信需要专门的机制。

什么是fork函数?

之前我们在Linux下启动一个进程的时候利用的是./可执行程序那是否有其他办法去启动一个进程呢? 

初识fork函数

当然是有的,那就是使用fork()这个函数。在使用之前呢我们要先去查看一下这个函数该如何使用------ 使用man 手册查询一下 fork 函数的使用

man 2 fork
  • 可以看到,这个函数的功能就是去创建一个子进程,其返回值为pid_t
  • 注意:这里的 pid_t 类型 是无符号整数

 函数说明:

  • 通过复制调用进程创建一个新进程。

  • fork 有两个返回值。

  • 父子进程代码共享,数据各自私有一份(采用写时拷贝)。

接下来,我们来测试一段代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>    // getpid, getppid, fork, sleep
#include <sys/types.h>  // getpid, getppid
 
int main()
{
   printf("before: I am a process\n");
 
   fork();
 
   printf("after: 创建一个新进程\n");
 
   return 0;
}

调用fork函数后,内核做了下面的工作:

1、创建了一个子进程的PCB结构体、并拷贝一份相同的进程地址空间和页表(PCB结构体中的一个指针指向该空间)

2、子进程和父进程起初共享代码和数据,并且页表中的虚拟地址和物理地址的映射关系是一样的,所以也指向相同的物理空间。    

3、fork返回后将子进程添加到系统的进程列表中,由调度器调用(每个进程开始自己的旅程)

4、一旦其中任意一方尝试修改数据,那么就会发生写时拷贝,会开辟一块新的物理内存,然后改变页表的映射关系。


写时拷贝

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

当父进程形成子进程之后,子进程写入,发生写时拷贝,重新申请空间,进行拷贝,修改页表(OS)

但是,我们怎么知道发生了写时拷贝呢?写时拷贝的内容都是由操作系统来完成的 

其实父进程创建子进程的时候首先将自己的读写权限改成只读然后再创建子进程。

此时是操作系统在做,用户并不知道,而且用户可能会对某一数据进行写入,这时页表转换就会出现问题,操作系统就会介入,就触发了我们重新申请内存拷贝内容的策略机制

fork函数存在的意义

fork函数常规用法:

1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

2、一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。 (进程替换

fork调用失败的原因 

系统中有太多的进程

实际用户的进程数超过了限制

进程终止

问题引入:为什么main函数要返回0?返回多少的意义是什么???

成功只有一种情况,但是失败可以有无数的原因和理由!! 所以main函数的本质是进程运行时是否是正确的结果,如果不是,可以用不同的数字表示不同的出错原因

进程退出场景:

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

运行完毕结果不正确

正常终止(可以通过 echo $? 查看进程退出码):       $?->保存最后一次进程退出的退出码

1. 从main返回

2. 调用exit

3. _exit

 main函数返回

 进程中,谁会关心我的运行情况呢??——>父进程 !

我们之前写代码中,main函数只能return 0吗?

答案是肯定不是!

在多进程环境中,我们创建子进程的目的就是协助父进程办事,但是父进程怎么知道子进程把事情办得怎么样?所以父进程要知道子进程办的怎么样,就有了退出码,而main函数的返回值,就是进程的退出码!

其实main函数本质上也是一个被别人调用的函数,所以他return的结果其实是想告诉他的父进程自己的运行情况。

返回 0 就表示成功,其他数字就表示进程失败的原因,每个不同的数字代表不同的原因!

我们可以通过 strerror 函数来直接查看每个数字代表的意义

它可以返回描述错误码的字符串

#include<stdio.h>
#include<string.h>

int main()
{
	for(int i = 0; i < 200; i++)
	{
		printf("%d: %s\n", i, strerror(i));
	}
	return 0;
}

我们打印结果来看看 

退出码 0 正好对应的是成功!

当我们134位置处时,发现已经没有错误信息了。

注意:错误码我们可以自己自定义!

main函数的退出码是可以被父进程获取的,用来判断子进程的运行结果

int main()
{
	return 31;
}

我们可以直接用 echo $? 指令查看进程的退出码: 

我们可以发现指令echo $?  返回的是上一个进程的错误码。当读取了一次之后,再读取就变成了0

库函数函数exit 

exit和return的区别:return和exit在main函数里是等价的,因为exit表示退出进程,而main函数恰好执行完return也会退出进程但是return在其他函数中代表的是函数返回。 

系统调用接口_exit

#include void _exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值

exit与_exit的区别

首先他们二者都可以让进程终止,并且使用方法也一样,那他们到底有什么区别呢?我们用代码来一探究竟!

//代码一:
int main()
{
	printf("Hello");
	exit(0); 
}

......
//代码二:
int main()
{
	printf("Hello");
    _exit(0); 
}

 为什么会出现这种情况呢?

printf打印如果不使用\n换行的话,数据会被存储到缓冲区里。exit函数会帮助我们刷新缓冲区的数据,然而_exit函数不会。因为exit函数在调用exit之前将所有缓存数据都写入了,所以在终止进程时,会将数据打印在屏幕上!

exit比_exit多做了一层最重要的工作就是刷新缓存我们还可以得出另一个结论就是:缓冲区绝对不在内核区!!因为如果在内核区的话,系统调用的_exit在终止的时候也必然会把缓冲区刷新一下,因为现代操作系统不做任何浪费时间和空间的事情,所以肯定不是由内核维护缓存区,而是由用户区在维护!!(_exit压根看不到缓冲区,所以这个工作只能有exit去完成)

进程异常终止

 用退出码可以告诉父进程自己的执行情况,那如果是异常中止了呢??那就连运行完毕这个条件都完成不了,更别谈结果是否正确了,所以我们可以知道异常必然是最先需要被知道的!因为一旦异常了,一般代码都没跑完,即使跑完了,错误码也不能让人相信,此时退出码就没有意义了!

举个例子:就好比我们平时考试一样,你考不好的时候大家会关心你为啥考不好,但如果你作弊了,性质就变了,即考得再好都让人觉得不可相信。

所以进程结束后应该优先判断该进程是否异常了,然后才能确定退出码能不能用!! 

// 当我们在运行这样的代码时

int a = 100;
a /= 0;
......
int *p = NULL;
*p = 100;
......

第一种情况: Floating point exception
第二种情况: Segmentation fault

当然不止这两个情况,但是它们都会让程序进程异常终止!

其实一旦程序出现了异常,操作系统就是通过 信号 的方式来杀掉这个进程!

而我们的前面两种情况正好对应了kill -8 kill -11

类似除0、野指针这样的错误,会触发一些硬件级别的错误,比如除0,cpu的状态寄存器会出现溢出的错误,而野指针,也就是们即将访问的虚拟地址在页表中找不到对应的映射,或者是建立的映射关系只有只读权限,反正最终会转化成一些硬件级别的信号来给操作系统。

所以,父进程需要关心子进程为什么异常,以及发生何种异常,系统会通过信号来告诉我们的进程发生了异常!! 

while(1)
{
	printf("hello Linux, pid: %d\n", getpid());
	sleep(1);
}

所以我们最关键的是要看父进程是否收到了信号,如果没有收到就没有异常(具体如何收到,就涉及到进程等待的知识)

进程等待

进程等待是什么

首先在开始之前我们提个问题,到底什么是进程等待? - 是什么

进程等待的概念:

  • 我们通常说的进程等待其实是通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程,父进程必须等待这个子进程结束后,处理它的代码和数据!

进程等待为什么要进行

在了解完进程等待的概念后,新的问题出现了,我们为什么要进行进程等待?-为什么

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

进程等待怎么做

我们进行了进程等待分析,发现进程等待非常的有必要。那么进程等待具体是怎么做的? - 如何做

父进程通过调用wait/waitpid方法来解决僵尸进程回收问题,以及获取子进程退出情况

wait方法

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

pid_t wait(int* status);
  • 返回值:成功,返回被等待进程的 pid,失败返回-1。

  • 参数:输出型参数,获取子进程的退出状态,不关心则可以设置成为 NULL。

waitpid方法

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

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

返回值:

  • 当正常返回的时候 waitpid 返回等待到的子进程的进程 ID;
  • 如果设置了选项 WNOHANG,而调用的过程中没有子进程退出,则返回0;
  • 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在。

pid:

  • pid = -1 表示等待任意一个子进程。与 wait 等效;
  • pid > 0 表示等待进程 ID 与 pid 相等的子进程。

status:

  • WIFEXITED(status):查看子进程是否正常退出。若为正常终止子进程返回的状态,则为真;WEXITSTATUS(status):查看进程的退出码。若非零,提取子进程的退出码。

options:

  • 0:表示父进程以阻塞的方式等待子进程,即子进程如果处在其它状态,不处在僵尸状态(Z状态),父进程会变成 S 状态,操作系统会把父进程放到子进程 PCB 对象中维护的等待队列中,以阻塞的方式等待子进程变成僵尸状态,当子进程运行结束,操作系统会检测到,把父进程重新唤醒,然后回收子进程;
  • WNOHANG:非阻塞轮询等待,若 pid 指定的子进程没有结束,处于其它状态,则 waitpid() 函数返回0,不予等待。若正常结束,则返回该子进程的 ID。

小Tipswait 和 waitpid 都只能等待该进程的子进程,如果等待了其它的进程那么就会出错。

阻塞和非阻塞轮询

父进程只等待一个进程(阻塞式等待)

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
    
int main()    
{    
    pid_t id = fork();    
    if(id < 0)    
    {    
        perror("fork");    
        return 1;    
    }    
    else if(id == 0)    
    {    
        // child    
        int cnt = 5;    
        while(cnt)    
        {    
            printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);    
            sleep(1);    
        }    
        exit(0);    
    }    
    else    
    {    
        int cnt = 10;    
        // parent    
        while(cnt)    
        {    
            printf("I am parent, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);    
            sleep(1);    
        }    
    
        int ret = wait(NULL);    
        if(ret == id)    
        {    
            printf("wait success!\n");    
        }    
        sleep(5);                                                                                                                                               
    }    
    
    return 0;    
}

前五秒父子进程同时运行,紧接着子进程退出变成僵尸状态,五秒钟后父进程对子进程进行了等待,成功将子进程释放掉,最后再五秒钟后父进程也退出,整个程序执行结束。 

 父进程等待多个子进程(阻塞式等待)

一个 wait 只能等待任意一个子进程,因此父进程如果要等待多个子进程可以通过循环来多次调用 wait 实现等待多个子进程。

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
    
#define N 5    
// 父进程等待多个子进程    
void RunChild()    
{    
    int cnt = 5;    
    while(cnt--)    
    {    
        printf("I am child, pid:%d, ppid:%d\n", getpid(), getppid());    
        sleep(1);    
    }    
    return;    
}    
int main()    
{    
    for(int i = 0; i < N; i++)    
    {    
        pid_t id = fork();// 创建一批子进程    
        if(id == 0)    
        {    
            // 子进程    
            RunChild();    
            exit(0);    
        }    
        // 父进程    
        printf("Creat process sucess:%d\n", id);    
    }    
    
    sleep(10);    
    
    for(int i = 0; i < N; i++)    
    {    
        pid_t id = wait(NULL);                                                                                
        if(id > 0)    
        {    
            printf("Wait process:%d, success!\n", id);    
        }    
    }    
    
    sleep(5);    
    return 0;    
}

如果子进程不退出,父进程在执行 wait 系统调用的时候也不返回(默认情况),默认叫做阻塞状态。由此可以看出,一个进程不仅可以等待硬件资源,也可以等待软件资源,这里的子进程就是软件。

获取子进程的退出信息(阻塞式等待)

相关文章:

  • vue h5实现车牌号输入框
  • 【CH32V307】教程 + IDE配置
  • Java基于SpringBoot的宽带业务管理系统,附源码+文档说明
  • VSCode中使用Markdown以及Mermaid实现流程图和甘特图等效果
  • 基于数据挖掘的网络入侵检测关键技术研究
  • 设计模式-结构型模式-组合模式
  • 比利时政府网站再遭黑客攻击,我们应当如何应对DDoS?
  • 论文解读:《Word embedding factor based multi-head attention》——基于词嵌入因子的多头注意力
  • hyperf中关于时间的设定
  • 【设计模式】代理模式(Proxy Pattern)详解
  • C++:类和对象(一)
  • css—— object-fit 属性
  • ENSP学习day10
  • 建立虚拟用户的账号数据库并为vsftpd服务器添加虚拟用户支持的脚本
  • vcd波形转仿真激励
  • JS自动装箱(Auto-boxing)机制深度解析
  • 基于ESP32和TinyUSB实现虚拟U盘功能的完整指南
  • Android实践开发制作小猴子摘桃小游戏
  • Qt 线程和 QObjects
  • [新闻.AI]国产大模型新突破:阿里开源 Qwen2.5-VL-32B 与 DeepSeek 升级 V3 模型
  • wordpress怎么进行页面修改/杭州seo建站
  • python做网站吗/微信营销软件群发
  • 电子商务综合实训报告网站建设/培训机构连锁加盟
  • 江门企业免费建站/小程序定制开发公司
  • 自己做静态网站的步骤/seo搜索排名优化方法
  • 贾汪网站建设/网上宣传方法有哪些