【Linux】进程控制篇

进程创建

用户空间 && 内核空间

  • 内核空间
    Linux操作系统和驱动程序运行在内核空间。
    系统调用函数都是在内核空间运行的,因为是OS提供的函数

  • 用户空间
    应用程序都是运行在用户空间的(程序员自己写的代码)
    注:若程序员写的代码调用了系统调用函数,则会切换到内核空间执行,系统调用函数执行完毕后,再返回到用户空间继续执行用户代码

  • 图解
    在遇到系统调用函数的时候,会转到内核空间执行该函数,执行完毕后,返回到用户空间继续执行代码
    在这里插入图片描述

fork函数

  • 作用
    让正在运行的进程创建出来一个子进程。从已存在的进程中创建一个新进程(子进程),原进程为父进程

函数原型

#include <unistd.h>
pid_t fork(void)
返回值:子进程中返回0,父进程中返回子进程的pid,出错返回-1

  • fork函数创建子进程的一些特性
    父子进程是独立运行的,互不干扰。各自有各自的进程虚拟地址空间和页表,数据不会窜
    父子进程是抢占式运行。谁先执行谁后执行本质上是OS调度决定的(也和自身准备情况有关—>就绪 阻塞 运行)
    子进程是从fork之后开始运行。(程序计数器+上下文指针)
    代码共享,数据独有

  • fork的一些用法
    守护进程:
    父进程创建子进程,让子进程执行真正的业务(进程程序替换),父进程负责守护子进程
    当子进程在执行业务的时候意外“挂掉了”,父进程负责重新启动子进程,让子进程继续提供服务

fork函数内部完成的功能

创建子进程,子进程拷贝父进程的PCB
具体过程如下:

  • 分配新的内存和内核数据结构(task_struct)给子进程
  • 将父进程部分数据结构拷贝至子进程
  • 添加子进程到系统列表中,添加到双向链表当中
  • fork返回,开始调度器调度(OS开始调度)

图解:

  • 成功创建子进程
    在这里插入图片描述
  • 创建完成后的父子进程如何执行代码
    fork之前父进程独立执行,fork之后,父子两个执行流分别执行
    注:fork之后谁先执行完全由调度器决定
    在这里插入图片描述

写时拷贝

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

  1. 父进程创建出来子进程,子进程的PCB拷贝自父进程,页表也是拷贝父进程的。
  2. 起初,OS并没有给子进程当中的变量分配空间进行存储,子进程的变量还是原来父进程物理地址当中的内容
  3. 如果不改变变量值,父子进程共享一个数据
  4. 如果改变变量值,才以写时拷贝的方式拷贝一份。此时父子进程通过各自的页表,指向不同的物理地址

结合图示理解:
在这里插入图片描述

进程终止

场景

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

正常终止 && 异常终止

  • 正常终止
    可以通过echo $? 来查看进程的退出码
    退出方式(三种)
    ①从main函数的return返回
    注:并不是任何函数的return语句都可以结束进程,必须是main函数的return语句
    在这里插入图片描述
    在这里插入图片描述
    ②调用exit函数(C标准库函数)

#include <stdlib.h>
void exit(int status);
参数:进程退出时的退出码
作用:谁调用,终止谁

在这里插入图片描述
在这里插入图片描述
③调用_exit函数(系统调用函数)

#include <unistd.h>
void _exit(int status)
参数:进程退出时的退出码
作用:谁调用,终止谁
在这里插入图片描述
在这里插入图片描述

  • 异常终止
    程序崩溃(内存访问越界、访问空指针等)

Ctrl+c命令
在这里插入图片描述
在这里插入图片描述

exit和_exit函数的区别

exit函数比_exit函数多执行了两个步骤

  1. 执行用户自定义的清理函数
    回调函数:在代码当中注册一个函数,在特定的时间执行

#include<stdlib.h>
int atexit(void(*function)(void))
功能:注册一个函数,在进程终止的时候调用(注:并没有在注册的时候调用)
被调用的函数只能是返回值类型为void的无参函数

在这里插入图片描述
在这里插入图片描述
2. 冲刷缓冲区、关闭流等
缓冲区:C标准库定义的,而非内核
建立缓冲区的目的是:减少IO次数,因为IO操作比较耗费时间
当触发刷新缓冲区的条件后,缓冲区的内容才会继续进行IO操作

关闭流:标准输入、标准输出、标准错误

图解:
在这里插入图片描述
代码验证:
exit函数:
在这里插入图片描述
_exit函数
在这里插入图片描述

刷新缓冲区的方式

在这里插入图片描述

缓冲方式

在这里插入图片描述

进程等待

为什么要进程等待(必要性)

  • 已知子进程先于父进程退出,父进程如果不管不顾,子进程就会变成僵尸进程,进而造成内存泄漏问题
  • 进程一旦进入僵尸状态,就会刀枪不入,“杀人魔王”kill -9也无能为力,因为谁也没有办法杀死一个死去的进程
  • 但是,父进程给子进程的任务它完成的如何,我们需要知道。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出状态信息

总而言之:父进程进行进程等待,等待子进程退出之后。回收子进程的退出状态信息,防止子进程变成僵尸进程

进程等待函数wait &&waitpid

函数原型

#include<sys/wait.h>
pid_t wait(int* status);
返回值:成功:返回被等待进程的pid 失败:返回-1
参数:输出型参数,获取子进程状态,不关心可以设置为NULL

pid_t waitpid(pid_t pid,int* status,int options);
返回值:
正常返回:返回收集到的进程的pid
设置了WNOHANG选项,调用过程中waitpid发现没有已退出的子进程可以收集,则返回0

如果调用出错,返回-1 ,这时errno会被设置为相应的值以指示错误所在

参数:
1、pid:
pid = -1,等待任一一个子进程,与wait等效
pid > 0 等待进程ID与pid相等的子进程
2、status:后面统一总结。因为两个函数的参数status含义一样
3、options:
WNOHANG:
若pid指定的子进程没有结束,则waitpid函数返回0;不予等待(并没有完成函数功能
若正常结束,则返回该子进程的pid)

函数特性

  1. wait函数
    阻塞的:谁调用,谁等待。直到等待的子进程退出
    两种情况:
    ①发起阻塞,资源存在:无需等待,直接执行函数功能后返回
    ②发起阻塞,资源不在:等待资源到来后,执行完函数功能返回

  2. waitpid函数
    参数options被设置为WNOHANG后,为非阻塞
    非阻塞:
    当调用一个非阻塞函数的时候,函数会判断资源是否准备好
    准备好:执行函数功能返回
    没准备好:函数报错返回(注:函数功能并没有完成)
    要点:非阻塞要搭配循环来使用

参数status的含义

  • wait和waitpid都有一个参数status,该参数为输出型参数,由OS填充
  • 如果传递NULL,表示不关心子进程退出状态信息
  • 否则,OS会修改该参数,将子进程的退出信息反馈给父进程
  • status不能简单的当做整型来看,。我们只用四字节中的低两个字节(即低16比特位)具体情况见下图:
    在这里插入图片描述
    子进程正常退出 && 异常退出时status的值的情况
    在这里插入图片描述

如何判断子进程正常退出还是异常退出??
根据函数的返回值以及status的值判断:

  • 正常退出:返回值>0 && 退出信号没有被设置(==0)
  • 异常退出:返回值>0 && 退出信号被设置(>0)

代码验证

  1. 验证wait函数
    验证一:父进程调用wait函数,子进程先于父进程退出

①子进程还是僵尸进程吗?
不是!!!
在这里插入图片描述

在这里插入图片描述

②父进程会在子进程之前退出吗?(使用pstack + 进程号)
不会退出!!
在这里插入图片描述
验证二:
正常情况下,获取status的值

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    pid_t ret = fork();

    if(-1 == ret)
    {
        return -1;
    }
    else if(0 == ret)
    {
        //child
        printf("I am child process,pid is %d\n",getpid());
        sleep(5);
        exit(100);
    }
    else
    {
        //parent
        int status = 0;
        pid_t result = wait(&status);
        if(-1 == result)
        {
            return -1;
        }
        else if(result > 0)
        {
            if((status&0x7f) == 0)
            {
                //子进程是正常退出的
                printf("child process return code is %d\n",(status>>8)&0xff);
            }
            else
            {
                //子进程异常退出
                printf("child process receive signal is %d, coredump flag is %d\n ",status&0x7f,(status>>7)&0x1);
            }
        }
    }

    return 0;
}

在这里插入图片描述

验证三:
异常情况下,获取status的值

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    pid_t ret = fork();

    if(-1 == ret)
    {
        return -1;
    }
    else if(0 == ret)
    {
        //child
        //构造异常退出场景
        int* point = NULL;
        *point = 100;
    }
    else
    {
        //parent
        int status = 0;
        pid_t result = wait(&status);
        if(-1 == result)
        {
            return -1;
        }
        else if(result > 0)
        {
            if((status&0x7f) == 0)
            {
                //子进程是正常退出的
                printf("child process return code is %d\n",(status>>8)&0xff);
            }
            else
            {
                //子进程异常退出
                printf("child process receive signal is %d, coredump flag is %d\n ",status&0x7f,(status>>7)&0x1);
            }
        }
    }

    return 0;
}

在这里插入图片描述
注意:为什么在异常退出的情况下,coredump标志位依旧是0呢?
原因就是没有设置coredump文件
具体解决办法如下:
1、使用 ulimit -a查看core file size
在这里插入图片描述
2、使用ulimit -c unlimited将core file size设置为unlimited
在这里插入图片描述
再次测试异常情况下coredump标志位是否为1
在这里插入图片描述

  1. 验证waitpid函数
    ①正常退出
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>

int main()
{
    pid_t pid = fork();
    if(-1 == pid)
    {
        return -1;
    }
    else if(0 == pid)
    {
        //child
        printf("I am child, my pid is %d\n",getpid());
        sleep(5);
        exit(100);
    }
    else
    {
        //parent
        int status = 0;
        pid_t ret = 0;
        do
        {
            ret = waitpid(pid,&status,WNOHANG);
        }while(ret == 0);
        if(ret == 0)
        {
            //没有已退出的进程可以回收
            return 0;
        }
        else if(-1 == ret)
        {
            //调用出错
            return -1;
        }
        else
        {
            //正常返回,返回收集到的子进程的pid
            if((status&0x7f) == 0)
            {
                //子进程正常退出
                printf("child process return code id %d\n",(status>>8)&0xff);
            }
            else
            {
                printf("child process recivice signal is %d,coredump flag is %d\n",(status&0x7f),(status>>7)&0x1);
            }
        }
    }
    return 0;
}

在这里插入图片描述

②异常退出

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>


int main()
{
    pid_t pid = fork();
    if(-1 == pid)
    {
        return -1;
    }
    else if(0 == pid)
    {
        //child
        printf("I am child, my pid is %d\n",getpid());
        sleep(5);

        //测试异常退出
       int* p = NULL;
       *p = 100;
    }
    else
    {
        //parent
        int status = 0;
        pid_t ret = 0;
        do
        {
            ret = waitpid(pid,&status,WNOHANG);
        }while(ret == 0);
        if(ret == 0)
        {
            //没有已退出的进程可以回收
            return 0;
        }
        else if(-1 == ret)
        {
            //调用出错
            return -1;
        }
        else
        {
            //正常返回,返回收集到的子进程的pid
            if((status&0x7f) == 0)
            {
                //子进程正常退出
                printf("child process return code id %d\n",(status>>8)&0xff);
            }
            else
            {
                printf("child process recivice signal is %d,coredump flag is %d\n",(status&0x7f),(status>>7)&0x1);
            }
        }
    }
    return 0;
}

在这里插入图片描述

进程程序替换

为什么要有进程程序替换

本质原因:想让进程去执行不一样的代码

因为父进程创建出来的子进程和父进程拥有相同的代码段,所以,子进程看到的代码和父进程是一样的。
当我们想要让子进程去执行不同的程序代码的时候,就需要让子进程调用进程程序替换的接口,从而让子进程执行不一样的代码

替换原理

替换进程的代码段和数据段&&更新堆栈

图解:
在这里插入图片描述

替换函数—exec函数簇

有6种以exec开头的函数,统称为exec函数。
这些函数都是在<unistg.h>

  1. execl函数

int execl(const char* path,const char* arg ...)
参数:

path:带路径的可执行程序(需要路径)
arg:传递给可执行程序的命令行参数,第一个参数必须是可执行程序本身如果要传递多个参数,则用 ‘,’将其隔开,最后以NULL结尾

返回值:

调用成功:加载新的程序从启动代码(main)开始执行,不再返回
调用失败:返回-1

函数测试:

#include<stdio.h>

#include<unistd.h>

int main()
{
    printf("before: You can see me!\n");

    int ret = execl("/usr/bin/ls","ls","-a","-l",NULL);

    //说明execl函数调用失败!
    printf("After: You can see me!ret = %d\n",ret);
    return 0;
}

在这里插入图片描述

  1. execlp函数

int execlp(const char* file,const char* arg ...)
参数:

file:可执行程序,可以不带路径,也可以带路径
剩余参数与execl函数一致
为什么execlp第一个参数不用带路径呢?
execlp这个函数会去搜索PATH这个环境变量
若可执行程序在PATH中:正常替换,执行替换后的程序
若不在PATH中:报错返回,替换失败

函数测试

#include<stdio.h>

#include<unistd.h>

int main()
{
    printf("before: You can see me!\n");

    int ret = execlp("ls","ls","-a","-l",NULL);

    //说明execlp函数调用失败!
    printf("After: You can see me!ret = %d\n",ret);
    return 0;
}

在这里插入图片描述

  1. execle函数

int execle(const char* path,const char* arg,...,char* const envp[])
参数:相较于execl,增加了一个envp[],剩下的完全一致

envp:
程序员传递的环境变量
程序员在调用该函数的时候,需要自己组织环境变量传递给函数

函数测试:(自己写一个获取当前环境变量的函数,分别在一下两种情况中观察结果)
①自己组织环境变量


//mygetenv。c
#include<stdio.h>
#include<stdlib.h>


int main()
{
    printf("%s\n",getenv("PATH"));

    return 0;
}
  

/

//execle.c文件
#include<stdio.h>

#include<unistd.h>

int main()
{
    extern char** environ;

    printf("before: You can see me!\n");

    int ret = execle("/home/gyj/linux-coding/Process/ProcessCSDN/ProcessCtrl/ProcessReplace/Test_execle/mtgetenv",\
           "mygetenv",NULL,environ);
     
    //说明execl函数调用失败!
    printf("After: You can see me!ret = %d\n",ret);
    return 0;
}

在这里插入图片描述

②环境变量给为NULL


//mygetenv。c
#include<stdio.h>
#include<stdlib.h>


int main()
{
    printf("%s\n",getenv("PATH"));

    return 0;
}
  

/

//execle.c文件
#include<stdio.h>

#include<unistd.h>

int main()
{
    printf("before: You can see me!\n");

    int ret = execle("/home/gyj/linux-coding/Process/ProcessCSDN/ProcessCtrl/ProcessReplace/Test_execle/mtgetenv",\
           "mygetenv",NULL,NULL);
     
    //说明execl函数调用失败!
    printf("After: You can see me!ret = %d\n",ret);
    return 0;
}

在这里插入图片描述
段错误,拿不到环境变量PATH

  1. execv函数

int execv(const char* path,char* const argv[]);
参数:

argv:也是传递给可执行程序的命令行参数但是必须以指针数组的方式进行传递
剩下的与execl一致
返回值
与execl一致

函数测试:

#include<stdio.h>

#include<unistd.h>

int main()
{
    printf("before: You can see me!\n");

    char* argv[10] = {NULL};
    argv[0] = "ls";
    argv[1] = "-a";
    argv[2] = "-l";
    int ret = execv("/usr/bin/ls",argv);

    //说明execl函数调用失败!
    printf("After: You can see me!ret = %d\n",ret);
    return 0;
}

在这里插入图片描述

  1. execvp函数

int execvp(const char* file,char* const argv[])
参数

file:可执行程序,可以不用带有路径,也可以带
argv:传递给可执行程序的命令行参数,以指针数组的方式传递
envp:程序员自己组织的环境变量
返回值
与execl一致

函数验证

#include<stdio.h>

#include<unistd.h>

int main()
{
    printf("before: You can see me!\n");

    char* argv[10] = {NULL};
    argv[0] = "ls";
    argv[1] = "-a";
    argv[2] = "-l";
    int ret = execvp("ls",argv);

    //说明execl函数调用失败!
    printf("After: You can see me!ret = %d\n",ret);
    return 0;
}

在这里插入图片描述

  1. execve函数

int execve(const char* path,char* const argv[],char* const envp[])
参数:

path:需要带路径的可执行程序
argv:传递给可执行程序的命令行参数,以指针数组的方式传递
envp:程序员自己组织的环境变量
返回值
与execl一致

函数验证
①提供envp

#include<stdio.h>

#include<unistd.h>

int main()
{

    extern char** environ;
    printf("before: You can see me!\n");

    char* argv[10] = {NULL};
    argv[0] = "ls";
    argv[1] = "-a";
    argv[2] = "-l";
    int ret = execve("/usr/bin/ls",argv,environ);

    //说明execl函数调用失败!
    printf("After: You can see me!ret = %d\n",ret);
    return 0;
}

在这里插入图片描述

②环境变量为NULL

#include<stdio.h>

#include<unistd.h>

int main()
{
    printf("before: You can see me!\n");

    char* argv[10] = {NULL};
    argv[0] = "mygetenv";

    int ret = execve("/home/gyj/linux-coding/Process/ProcessCSDN/ProcessCtrl/ProcessReplace/Test_execle/mtgetenv",argv,NULL);

    //说明execl函数调用失败!
    printf("After: You can see me!ret = %d\n",ret);
    return 0;
}

在这里插入图片描述

总结:

  • 对于上述6个函数的函数名理解:
  • 在这里插入图片描述
  • 函数之间的关系与区别
    execve是系统调用函数,其他五个函数都属于C标准库函数
    具体关系见下图:
    在这里插入图片描述

代码验证

  • 进程程序替换+fork+进程等待:想让子进程进行进程程序替换,执行其他程序
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>


int main()
{
    pid_t pid = fork();
    if(pid < 0)
    {
        return 0;
    }
    else if(pid == 0)
    {
        //child
        printf("Before:I start replace!\n");
        int ret = execl("/usr/bin/ls","-a","-l",NULL);
        printf("replace failed:%d",ret);
    }
    else
    {
        //father
        printf("I am father, I prepare to wait child process!\n");
        wait(NULL);

    }
    return 0;
}

在这里插入图片描述

!!!
各位看官,以上就是进程控制篇的所有内容了~如果感觉还不错的话还请你一键三连多多支持!!
感谢你的阅读~我们下期再见!!
在这里插入图片描述

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Suk-god

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

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

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

打赏作者

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

抵扣说明:

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

余额充值