Linux之进程控制

这篇具有很好参考价值的文章主要介绍了Linux之进程控制。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

进程控制

一.进程创建(再谈fork)

当一个程序被加载到内存中以后,这个程序就变成了一个进程。

此外还可以通过调用fork函数创建子进程,子进程和父进程共享fork之后的代码,可以采用对fork返回值进行判断的办法来让父子进程分别执行后续代码的一部分。

1.一个函数在执行return语句之前就已经完成了这个函数的主要工作,因此fork函数能有两个返回值的原因就是在执行return语句之前,在fork函数内部就已经将子进程创建出来了,return语句被父子进程各执行了一次,所有就有两个返回值。

2.fork给父进程返回子进程的PID是为了方便后续父进程对子进程进行资源回收

3.如果fork函数调用成功,操作系统会给子进程分配内存块并创建对应的内核数据结构(PCB,页表,进程地址空间),fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。

4.只要是函数就有可能被调用失败,当一个操作系统中的进程太多时,fork函数就会失败。

二.进程退出

我们平常在写main函数时总是习惯在最后写一个return 0,这个返回值其实是main函数退出时的退出码,退出码标定的是一个进程是否正常退出。当我们不关心进程的退出码时就可以直接设置成0,如果关心的话就要设置特定的数字来标定特定的情况。

1.进程退出的情况

一个进程退出无非就三种情况:

1.代码跑完了,结果正确(直接返回0)

2.代码跑完了,结果不正确。

此时程序的退出码就可以帮我们标定错误,使用echo $?就可以查看最近一个进程的退出码

Linux之进程控制

每个退出码都有对应的退出信息,一般用0表示程序正常退出,用非0表示错误,库中给我们提供了134个错误码,可以将其对应的错误信息都打印出来看看:

Linux之进程控制

3.代码没跑完,程序异常了(退出码无意义)

2.exit和_exit

可以使用exit或_exit为一个进程设置退出码,在数据结构阶段我经常看到这样的代码:

int *tmp=(int*)malloc(4*sizeof(int));
if(tmp==NULL)
{
    perror("malloc fail\n");
    exit(-1);
}

当使用malloc开辟空间失败以后就使用exit函数并将退出码设置成-1用来表示错误


下面通过这样一段代码来看看两者之间的区别:

int main()
{
	printf("hello world");
	sleep(2);
	exit(0);//_exit(0);
	return 0;
}

有了前面的基础我们知道缓冲区是行刷新的,没有\n虽然printf是先执行,但是也会在程序退出以后才打印语句

首先来看使用exit时的结果:

Linux之进程控制

再来看看使用_exit时的结果:

Linux之进程控制

可以看到两者之间最大的区别就是exit在程序结束时会将缓冲区内的数据刷新出来,但是_exit却不会将缓冲区刷新出来。

那么缓冲区在哪里?

计算机是一个层状结构,我们不能跳过某一层去跳跃式的访问某一层。

exit是一个库函数,而_exit是一个系统调用。

也就是说如果缓冲区在内核当中,那么必须要使用系统调用接口去申请刷新缓冲区。但这里的结果显示,系统调用接口没有刷新缓冲区,库函数却刷新了。因此可以得到一个结论:缓冲区并不在内核空间当中,而是一个用户级的缓冲区。

Linux之进程控制

三.进程等待

当子进程退出以后,如果父进程一直不回收子进程的资源,那么子进程就会处于僵尸状态,会造成内存泄漏的问题。

父进程创建一个子进程是为了让它帮我们去执行某一项操作,当子进程将这个操作执行完毕以后会将退出结果保存在PCB中。也就是说当一个进程执行结束以后,它对应的代码和数据可以被释放,但是它的PCB是不能被释放的,要等待父进程读取完退出结果后由父进程来释放。父进程可以通过进程等待(使用系统调用wait/waitpid)的方式来回收子进程对应的资源。

1.wait

Linux之进程控制

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

#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)
    {
        //子进程
        int cnt=5;
        while(cnt--)
        {
            printf("子进程:%d  父进程:%d  %d\n",getpid(),getppid(),cnt);
            sleep(1);
        }
        exit(0);//子进程退出
    }
    sleep(10);//让子进程处于僵尸状态五秒
    pid_t ret =wait(NULL);
    if(id>0) 
    {
        //父进程
        printf("等待成功:%d\n",ret);
    }
    sleep(3);//回收完保持进程三秒
    return 0;
}

这个代码的结果应该是:刚开始有两个运行状态的进程,大概五秒以后子进程结束,但父进程没有去回收,子进程处于僵尸状态,又过来五秒,父进程调用wait系统调用回收子进程,子进程被回收,只剩下父进程,保持三秒后父进程也结束

Linux之进程控制


2.waitpid

同样是父进程用于回收子进程的系统调用,但这个系统调用还能顺便拿到子进程退出时的退出码和信号。

Linux之进程控制

对于status不能当作简单的整数来看,可以将其看作一个位图结构只关注它的低16位,其中次低8位中存放的是退出码,低7位中放的是退出信号(和退出码一样0信号表示无异常)

Linux之进程控制

如果在进程运行期间使用kill命令杀掉进程,那么也是相当于被信号所杀。

Linux之进程控制

除了使用status的低十六个比特位以外,还可以通过两个宏来得到子进程退出时的退出码和退出信号。

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

Linux之进程控制

3.阻塞式等待

可以看到我是用waitpid的时候第三个参数一直传的是0,这就表示采用的是阻塞式等待。所谓阻塞式等待就是如果子进程没退出,父进程就一直守着子进程直到子进程退出。

马上就要考试了,作为一个聪明但不爱学习的人,我找到了我班上听课最认真的张三同学,希望他能帮助我复习,他答应的很爽快,我觉得有点不好意思于是就提出要请他吃饭。在考试前一天中午我到他家楼下,我打电话给他表示我已经到他家楼下了让他下来和我去吃饭,但是他说他正在看书让我在楼下稍等上十几分钟。我说可以,但是电话不要挂,我们就一直这样打着电话。在他下楼我们挂掉电话之前我什么也干不了,只能一直保持着和他打电话的状态。

这种舔狗式的等待方式就是阻塞式等待,但是父进程一直保持着等待状态,直到子进程运行完毕父进程再去回收子进程的资源。

4.非阻塞式等待

在非阻塞等待中,父进程会采用轮询的方式检测子进程的状态,如果子进程没有退出,那么父进程就去继续做自己的事,如果在某一次询问中,父进程发现子进程已经结束了,那么父进程就会去回收子进程的资源。

又到了一次考试,我又找到张三帮我复习,在考试的前一天我又到了他家楼下给他打电话,他仍旧表示正在有事让我稍等。有了上次的教训,我这次直接把电话一挂开始玩手机,刷了一会抖音以后我又打了一个电话给张三并询问他好了没,张三说还没好,我又把电话一挂,掏出一本《C和指针》看一看。过一会我又给张三打电话询问他好了吗。

这个我不断给张三打电话询问他好了没的过程,就类似于父进程轮询检查子进程是否执行完毕,如果子进程还在运行,父进程不必一直等待子进程可以继续执行其他代码:

 #include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <stdlib.h>
#define NUM 10
typedef void (*func_t)();
func_t handlerTask[NUM];
void task1()
{
    printf("handler task1\n");
}
void task2()
{
    printf("handler task2\n");
}
void task3()
{
    printf("handler task3\n");
}
void loadTask()
{
    memset(handlerTask, 0, sizeof(handlerTask));
    handlerTask[0] = task1;
    handlerTask[1] = task2;
    handlerTask[2] = task3;
}
int main()
{
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        int cnt = 10;
        while (cnt)
        {
            printf("这是子进程pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt--);
            sleep(3);
        }
        exit(10);
    }
    loadTask();
    int status = 0;
    while (1)
    {
        pid_t ret = waitpid(id, &status, WNOHANG);
        //WNOHANG:非阻塞:子进程没有退出,父进程检测之后立即退出
        if (ret == 0)
        {
            //waitpid调用成功&&子进程没退出
            //子进程没有退出,我的waitpid没有等待失败,仅仅检测到而来子进程没有退出
            printf("wait done,but child is running...parent running other things\n");
            for (int i = 0; handlerTask[i] != NULL; i++)
            {
                handlerTask[i]();//回调
            }
        }
        else if (ret > 0)
        {
            //waitpid调用成功&&子进程退出
            printf("wait success,exit code:%d,sig:%d\n", (status >> 8) & 0xFF, status & 0x7F);
            break;
        }
        else
        {
            //waitpid调用失败
            printf("waitpid call failed\n");
            break;
        }
        sleep(1);
    }
    return 0;
}

Linux之进程控制

非阻塞式等待并不会占用父进程的全部精力,在等待期间父进程还可以去做其他的事情。

阻塞式等待和非阻塞式等待没有绝对的好坏,只有更适合的应用场景。甚至阻塞式应用的比非阻塞式还要多。

5.图解父进程等待子进程

Linux之进程控制

进程等待的本质是父进程检测子进程的退出信息,这个退出信息保存到status中供父进程读取

四.进程替换

1.什么是进程替换

进程替换就是在这个进程中通过调用exec*系列的函数,将指定的程序加载到内存中被执行,几乎是所有的后端语言都可以被替换

#include <unistd.h>//头文件
//execve的封装
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
//系统调用
int execve(const char *filename, char *const argv[],char *const envp[]);

l(list):表示参数通过列表式传参

p(path):表示不用传文件的路径,只需要传文件名就行,它会自动的去环境变量中查找

v(vector):将参数写入数组中,最后统一传递

e(env):环境变量,可以传入自己所写的环境变量

...表示可变参数列表,也就是说传参的个数是不确定的,但最后要以NULL结尾

这些函数只在调用失败时才有返回值(-1),因为如果调用成功,后续的代码都会被替换掉,返回值没有意义

#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("process is running\n");
    execl("/usr/bin/ls"/*要执行程序的路径*/,"ls","--color=auto","-a","-l",NULL/*如何执行*/);//一定要用NULL结尾
    
    printf("process is down·····\n");//这句话并不会被打印,因为后续代码和数据已经被execl函数替换了
    return 0;
}

Linux之进程控制

此外还可以切换成我们自己写的程序:
Linux之进程控制

但是这样一替换就将整个进程都替换了,所以进程替换一般都是通过创建一个子进程然后让子进程来完成替换的。

Linux之进程控制

可以看到尽管子进程使用了程序替换,但是父进程照样执行不受影响,这是因为有页表和写时拷贝的存在。

父子进程原本共享代码和数据,一旦子进程想修改共享的代码和数据,操作系统就会重新找一块空间并将原数据和代码拷贝一份供子进程修改,这就是写时拷贝(写的时候才拷贝)

进程各自都有独立的进程地址空间,通过页表与物理内存发生映射,所以一旦代码和数据的物理位置发生改变就只要改变页表的映射关系即可。

独立的进程地址空间,独立的代码和数据,保证了进程之间的独立性


各个函数使用示例

int main()
{
    char *const argv[] = {"ps", "-ef", NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
    execl("/bin/ps", "ps", "-ef", NULL);
    // 带p的,可以使用环境变量PATH,无需写全路径
    execlp("ps", "ps", "-ef", NULL);
    // 带e的,需要自己组装环境变量
    execle("ps", "ps", "-ef", NULL, envp);
    execv("/bin/ps", argv);
    // 带p的,可以使用环境变量PATH,无需写全路径
    execvp("ps", argv);
    // 带e的,需要自己组装环境变量
    execve("/bin/ps", argv, envp);
    exit(0);
}

其实这六个函数都是在调用execve这个系统调用,封装这六个函数是为了满足各种调用场景方便我们使用。

Linux之进程控制

2.替换的原理

程序替换的本质:用磁盘指定位置上的程序的代码和数据,覆盖进程自身的代码和数据,达到让进程执行指定程序的目的。

Linux之进程控制

3.main函数被加载的原理

Linux之进程控制
文章来源地址https://www.toymoban.com/news/detail-422616.html


到了这里,关于Linux之进程控制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • [Linux 进程(四)] 再谈环境变量,程序地址空间初识

    上一篇我们讲了环境变量,如果有不明白的先读一下上一篇文章:环境变量讲解 本篇文章我们继续完善环境变量这章剩下的内容,以及main函数第三个参数的详解,进程地址空间的初始。 看完上一篇文章的同学,肯定知道了如何查看环境变量,命令行输入 env: 我们查看一下

    2024年01月18日
    浏览(44)
  • 【Linux】Linux进程控制 --- 进程创建、终止、等待、替换、shell派生子进程的理解…

    柴犬: 你好啊,屏幕前的大帅哥or大美女,和我一起享受美好的今天叭😃😃😃 1. 在调用fork函数之后, 当执行的程序代码转移到内核中的fork代码后 ,内核需要分配 新的内存块 和 内核数据结构 给子进程, 内核数据结构包括PCB、mm_struct和页表,然后构建起映射关系 ,同时

    2024年01月16日
    浏览(58)
  • [Linux]进程控制详解!!(创建、终止、等待、替换)

            hello,大家好,这里是bang___bang_,在上两篇中我们讲解了进程的概念、状态和进程地址空间,本篇讲解进程的控制!!包含内容有进程创建、进程等待、进程替换、进程终止!! 附上前2篇文章链接: Linux——操作系统进程详解!!(建议收藏细品!!)_bang___ba

    2024年02月15日
    浏览(43)
  • [Linux]进程控制精讲,简单实现一个shell

    目录 前言 进程创建 fork函数初识 写时拷贝 fork常见用法 fork调用失败的原因 进程终止 进程退出场景 进程退出码 查看进程退出码 退出码的含义 进程常见退出方法 exit VS _exit exit函数 _exit函数 二者的区别 return退出 进程等待 进程等待必要性 进程等待的方法 wait方法 waitpid方法

    2023年04月26日
    浏览(50)
  • 【探索Linux】—— 强大的命令行工具 P.10(进程的控制——创建、终止、等待、程序替换)

    前面我们讲了C语言的基础知识,也了解了一些数据结构,并且讲了有关C++的一些知识,也学习了一些Linux的基本操作,也了解并学习了有关Linux开发工具vim 、gcc/g++ 使用、yum工具以及git 命令行提交代码也相信大家都掌握的不错,上一篇文章我们了解了关于进程的地址空间,今

    2024年02月08日
    浏览(54)
  • 进程控制相关 API-创建进程、进程分离、进程退出、进程阻塞

    目录 进程控制相关 API 父进程创建子进程 fork() 进程分离 exec 族函数 进程的退出 return/exit() 进程的阻塞 wait() 其它 API 进程控制相关 API p.s 进程控制中的状态转换 相关 API,用户很少用到,在此不提。 一般来说,这些内核标准 API,在执行出错(可能是资源不够、权限不够等等

    2024年02月10日
    浏览(43)
  • 使用fork函数创建一个进程

    pid_t fork(void) fork函数调用成功,返回两次 (1)返回值为0,代表当前进程是子进程 (2)返回值为非负数,代表当前进程是父进程 (3)调用失败,则返回-1 代码如下:

    2024年02月04日
    浏览(52)
  • 【Linux进程】查看进程&&fork创建进程

    目录 前言  1. 查看进程  2. 通过系统调用创建进程-fork初识 总结          你有没有想过在使用Linux操作系统时,后台运行的程序是如何管理的?在Linux中,进程是一个非常重要的概念。本文将介绍如何查看当前运行的进程,并且讨论如何使用fork创建新的进程。通过了解这些

    2024年01月22日
    浏览(54)
  • Linux——进程创建与进程终止

    📘北尘_ :个人主页 🌎个人专栏 :《Linux操作系统》《经典算法试题 》《C++》 《数据结构与算法》 ☀️走在路上,不忘来时的初心 在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。 #include unistd.h pid_t fork(void); 返

    2024年02月04日
    浏览(51)
  • Linux之进程(五)(进程控制)

    目录 一、进程创建 1、fork函数创建进程 2、fork函数的返回值 3、fork常规用法 4、fork调用失败的原因 二、进程终止 1、进程终止的方式 2、进程退出码 3、进程的退出方法 三、进程等待 1、进程等待的必要性 2、wait函数 3、waitpid函数 四、进程程序替换 1、概念 2、原理 3、进程替

    2024年02月04日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包