进程控制(详解)

一.进程创建

1.fork函数

在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进 程为⽗进程。

  • #include <unistd.h>
  • pid_t fork(void);
  • 返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1

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

  1. 分配新的内存块和内核数据结构给⼦进程
  2. 将⽗进程部分数据结构内容拷⻉⾄⼦进程
  3. 添加⼦进程到系统进程列表当中
  4. fork返回,开始调度器调度
  5. fork之后,谁先执⾏完 全由调度器决定

2.写时拷贝

当父进程创建子进程时fork,父进程首先要做的是,直接将父进程代码和数据对应页表项的权限,全部改成只读权限,然后子进程继承下来的页表也全部是只读权限,当子进程尝试通过代码对某些数据进行修改的时候,那么页表就立马识别到当前正在对只读区域进行写入操作,就会触发系统错误,触发系统错误的时候,系统就要进行判断,是真的错了,还是要进行写时拷贝,因为毕竟也会有野指针异常访问空间,系统错误后,就会触发缺页中断(后面学习会讲解,这里只需要了解就行),让系统去做检测,如果发现写入的区域是代码区,直接进行杀进程,但一旦发现写入的区域是数据区,就判定成发生写时拷贝,然后系统向物理内存申请空间,进行拷贝,修改页表映射,恢复权限。

要是写入时,这个内存空间不在物理内存里呢?

也是触发错误,发生缺页中断,系统检测,发生页表置换,从磁盘调入物理内存

写时拷贝就是时间换空间的做法!!!

为什么还要做一次拷贝呢?不能直接开辟空间吗?

因为你的写入操作!=对目标区域进行覆盖,也有可能会用到原有数据,比如:count++。

二.进程终止

main函数的返回值,是返回给父进程或者是系统,最终表示程序对还是不对!!

回顾一下查看上一次进程退出码的指令:echo $? 查看上次进程的退出信息,命令行中,最近一次程序的退出码。退出码表示错误原因。

一般0表示成功,非0 表示错误,用不同的数字,约定或表明出错的原因,系统提供了一批错误码,也可以自己约定错误码。

errno表示获取错误码,而strerror则是将该错误码转换成字符串

数字是给系统看的,字符串是给用户看到

看下面代码,如果不存在该文件,打开一定是失败的,则会返回错误码errno,然后通过strerror将数字转换成字符串。

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>

int main()
{
    printf("before: errno: %d,errstring: %s\n", errno, strerror(errno));

    FILE *fp = fopen("./log.txt", "r");
    if (fp == nullptr)
    {
        printf("after: errno: %d,errstring: %s\n", errno, strerror(errno));
        return errno;
    }
    return 0;
}

所有再用echo $?查看也是与errno一样。

在Linux下系统错误码有0-133,Windows下有0-140.

在两种系统分别执行下面代码查看:

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>

int main()
{
    for(int i = 0;i<200;i++)
    {
        std::cout<<"code: "<<i<<", errstring: "<<strerror(i)<<std::endl;
    }
    return 0;
}

1.进程退出的时候,main函数结束代表进程退出,mian函数返回值代表进程所对应的退出码。

2.进程退出码可以由系统默认的错误码来提供,也可以约定自己的退出码。

1.进程终止的方式

1.main函数return

2.进程调用exit,exit在代码任何地方,表示进程结束,非mian函数的return,只表示函数结束,exit表进程结束

3._exit,系统接口,也是终止进程

补充:系统级头文件都是**.h**.语言级头文件,比如<stdio.h>,在c++中可以写成<cstdio>,但系统级头文件不行。

观察下面代码结果:

cpp 复制代码
int main()
{
    printf("进程运行结束!");
    sleep(2);
    exit(23);
    sleep(2);
    return 0;
}

然后对比_exit,看看区别:

cpp 复制代码
int main()
{
    printf("进程运行结束!");
    sleep(2);
    _exit(23);
    sleep(2);
    return 0;
}

结论:语言级exit会把打印从缓冲区刷新出来,再结束进程,而系统级_exit则不会把打印从缓冲区刷新出来,直接结束进程。

1.刷新缓冲区的区别

2.上下层的区别

我们知道语言级函数,往往是封装系统调用接口,更接近上层,

我们试想一下我们之前认为的缓冲区在哪个位置?

这个缓冲区一定不在OS内部,因为如果在OS内部那么printf打印的信息也一定在OS中,所不管调用哪个函数,都会刷新缓冲区,所有这个缓冲区一定不在OS中。

结论:这个缓冲区是语言级缓冲区,由C/C++提供的!!!

调用exit,fflush从语言层把缓冲区内容刷新到OS中,在刷新到屏幕上。

调用_exit,则直接会杀死进程,数据还在缓冲区内,没机会刷新。

三.进程等待

对于我们创建出来的子进程,作为父进程,必须等待这个子进程,因为是父进程创建的,就必须对子进程负责,对子进程进行回收,如果不回收,根据进程状态那一节内容,子进程就是变成僵尸进程,如果忘了可以进行回顾:进程的状态

我们看下面代码,如果不进行回收,子进程就会变成僵尸:

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include<unistd.h>

int main()
{
    pid_t id = fork();
    if(id<0)
    {
        printf("errno: %d,errstring: %s\n",errno,strerror(errno));
        return errno;
    }
    else if(id == 0)
    {
        int cnt = 10;
        while(cnt)
        {
            printf("子进程运行中,pid: %d\n",getpid());
            sleep(1);
            cnt--;
        }
        exit(0);
    }
    else
    {
        while (true)
        {
            printf("我是父进程,pid: %d\n",getpid());
            sleep(1);
        }
        
    }
    return 0;
}

1.wait

学习两个回收子进程函数:

1.先看wait,一般而言,父进程创建子进程就要等待子进程,直到结束

wait

1.回收子进程的僵尸状态

2.等待的时候子进程,如果不退,父进程就要一直阻塞在wait内部,类似scanf。

3.返回值,大于0,表示成功回收子进程,小于0,表示回收失败

4.等待期间,父进程阻塞式等待,等待成功一般返回子进程pid

5.作用:等待任意一个子进程退出

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id<0)
    {
        printf("errno: %d,errstring: %s\n",errno,strerror(errno));
        return errno;
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("子进程运行中,pid: %d\n",getpid());
            sleep(1);
            cnt--;
        }
        exit(0);
    }
    else
    {
        sleep(10);
        pid_t rid = wait(nullptr);
        if(rid > 0)
        {
            printf("wait sub processon,rid: %d\n",rid);
        }
        while (true)
        {
            printf("我是父进程,pid: %d\n",getpid());
            sleep(1);
        }
        
    }
    return 0;
}

如图结果,直接回收僵尸状态。

2.waitpid

子进程退出了,想不想知道,子进程把任务完成的怎么样,运行的怎么样,你怎么知道子进程把任务完成的怎么样呢?

父进程要知道子进程的退出信息,想办法获取子进程的退出信息,比如:退出码。父进程不光要回收子进程,还要知道子进程运行结果对还是不对。

1.如果pid > 0,指定一个进程,pid == -1,表示任意一个进程

2.status表明子进程的退出信息,帮助父进程获取子进程的退出信息,OS把子进程PCB中退出信息写入这里,是一个输出型参数

3.如果options == 0,则阻塞式等待,options == WNOHANG,表示非阻塞式等待

补充:输入型参数,是数据进入函数的通道,输出型参数,是函数将结果传出来的通道。

等待错误的子进程,就是传存在进程的id:

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id<0)
    {
        printf("errno: %d,errstring: %s\n",errno,strerror(errno));
        return errno;
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("子进程运行中,pid: %d\n",getpid());
            sleep(1);
            cnt--;
        }
        exit(0);
    }
    else
    {
        sleep(10);
        pid_t rid = waitpid(id+1,nullptr,0);// ==wait(nullptr)
        if(rid > 0)
        {
            printf("wait sub processon,rid: %d\n",rid);
        }
        else
        {
            perror("waitpid");
        }
        while (true)
        {
            printf("我是父进程,pid: %d\n",getpid());
            sleep(1);
        }
        
    }
    return 0;
}

子进程一直会是僵尸状态。

正常等待进程,传正确的pid:

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id<0)
    {
        printf("errno: %d,errstring: %s\n",errno,strerror(errno));
        return errno;
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("子进程运行中,pid: %d\n",getpid());
            sleep(1);
            cnt--;
        }
        exit(1);
    }
    else
    {
        sleep(10);
        int status = 0;
        pid_t rid = waitpid(id,&status,0);// ==wait(nullptr)
        if(rid > 0)
        {
            printf("wait sub processon,rid: %d,status: %d\n",rid,status);
        }
        else
        {
            perror("waitpid");
        }
        while (true)
        {
            printf("我是父进程,pid: %d\n",getpid());
            sleep(1);
        }
        
    }
    return 0;
}

这里我们会发现我们用的exir(1),不应该是错退出码是1吗,为什么是256?

因为status里面不仅包含了程序的退出码还包含退出信号。

因为进程不光会正常结束,还会异常结束,异常结束,OS就会提前使用信号杀死进程,这时进程退出信息中就会记录退出信号,是因为什么导致的退出。

status,不是一个完整的整数,他是一个位图,32个比特位,只考虑低16位,次8位是退出码,地位是退出信号。

如果想获取退出码,就要 右移8位,然后&上0xFF:

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id<0)
    {
        printf("errno: %d,errstring: %s\n",errno,strerror(errno));
        return errno;
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("子进程运行中,pid: %d\n",getpid());
            sleep(1);
            cnt--;
        }
        exit(1);
    }
    else
    {
        sleep(10);
        int status = 0;
        pid_t rid = waitpid(id,&status,0);// ==wait(nullptr)
        if(rid > 0)
        {
            printf("wait sub processon,rid: %d,status: %d\n",rid,(status>>8)&0xFF);
        }
        else
        {
            perror("waitpid");
        }
        while (true)
        {
            printf("我是父进程,pid: %d\n",getpid());
            sleep(1);
        }
        
    }
    return 0;
}

这样就可以获取到正确的退出码。

如果想要知道退出信号呢?我们信号有哪些?

我们可以看到为什么没有0号信号,因为0代表成功退出,不是异常退出。

如果想要看知道退出型号,status&0x7F:

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id<0)
    {
        printf("errno: %d,errstring: %s\n",errno,strerror(errno));
        return errno;
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("子进程运行中,pid: %d\n",getpid());
            sleep(1);
            cnt--;
        }
        exit(1);
    }
    else
    {
        sleep(10);
        int status = 0;
        pid_t rid = waitpid(id,&status,0);// ==wait(nullptr)
        if(rid > 0)
        {
            printf("wait sub processon,rid: %d,status: %d, exit signal: %d\n",rid,(status>>8)&0xFF,status&0x7F);
        }
        else
        {
            perror("waitpid");
        }
        while (true)
        {
            printf("我是父进程,pid: %d\n",getpid());
            sleep(1);
        }
        
    }
    return 0;
}

1.子进程退出,可不可以使用全局变量,来获取子进程的退出码呢?

不行,因为全局数据一修改,就会发生写时拷贝,父进程看不见,因为进程具有独立性,地址一样,内容不同。

这就是为什么我们只能通过系统调用接口来获取退出信息,系统调用waitpid的时候,他是OS提供的接口,OS帮我们拿到子进程的PCB中的退出信息,通过status给我们返回。

重新谈进程退出:

1.代码跑完,结果对,return 0;

2.代码跑完,结果不对,return !0;

3.进程异常退出,OS提前使用信号终止了你的进程,进程PCB退出信息中会记录退出信号

status不光会获取退出码,又会获取退出信号,一般想看到进程结果是否正确,前提是这个进程退出信号为0,没有收到信号,证明这个代码是正常跑完的,结果是对还是不对,我们通过退出码来进一步判断。

1.创建子进程,不关注子进程退出码退出结果,调用waitpid,传nullptr给status

2.关注退出码退出结果,调用waitpid,通过status得知子进程退出信息

我们来看看Linux内核中,进程PCB是否有退出码和退出信号:

这里我们认识两个宏:WIFEXITED,WEXITSTATUS。

WIFEXITED:用于判断子进程是否正常终止。如果子进程是通过调用exit函数或从main函数中正常返回而终止的,那么WIFEXITED返回非零值(通常为 1);否则,如果子进程是由于收到信号等异常原因而终止的,WIFEXITED返回 0 。
WEXITSTATUS:当WIDEXITED返回非零值,即确定子进程是正常终止时,WEXITSTATUS用于获取子进程的返回值。子进程在正常退出时可以通过exit函数传递一个返回值给父进程,WEXITSTATUS就是用来提取这个返回值的。

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id<0)
    {
        printf("errno: %d,errstring: %s\n",errno,strerror(errno));
        return errno;
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("子进程运行中,pid: %d\n",getpid());
            sleep(1);
            cnt--;
        }
        exit(1);
    }
    else
    {
        sleep(10);
        int status = 0;
        pid_t rid = waitpid(id,&status,0);// ==wait(nullptr)
        if(rid > 0)
        {
            if(WIFEXITED(status))//如果成功退出
            {
                printf("wait sub processon,rid: %d,status: %d\n",rid,WEXITSTATUS(status));
            }
            else//异常退出
            {
                printf("child process quit error!\n");
            }
        }
        else
        {
            perror("waitpid");
        }
        while (true)
        {
            printf("我是父进程,pid: %d\n",getpid());
            sleep(1);
        }
        
    }
    return 0;
}

1.我们想让子进程帮我们去完成某种任务,举一个例子:让子进程进行备份操作:

这里我们就使用了自己约定的退出码,用枚举列出来,下面是阻塞式的备份操作:

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>

enum{
    OPEN_FILE_ERROR = 1,
    OK = 0,
};

const std::string gsep = " ";
std::vector<int> data;

int SaveBegin()
{
    std::string name = std::to_string(time(nullptr));
    name += ".backup";
    //打开文件
    FILE *fp = fopen(name.c_str(),"w");
    //判断文件打开是否失败
    if(fp == nullptr) return OPEN_FILE_ERROR;
    //文件打开成功 ,备份
    std::string dataStr;//储存vector里面值
    for(auto d: data)
    {
        dataStr += std::to_string(d);
        dataStr += gsep;//加空格符
    }
    //把dataStr数据,放入fp文件中
    fputs(dataStr.c_str(),fp);
    //关闭文件
    fclose(fp);
    return OK;
}

void Save()
{
    pid_t id = fork();
    if(id == 0)//child
    {
        //备份任务
        int code = SaveBegin();
        exit(code);
    }
    //父进程
    int status = 0;
    //等待回收子进程
    pid_t rid = waitpid(id,&status,0);
    if(rid > 0)//成功退出
    {
        int code = WEXITSTATUS(status);//获取退出码
        if(code == 0) printf("备份成功!,exit code: %d\n",code);
        else printf("备份失败!,exit code: %d\n",code);
    }
    else//异常退出
    {
        perror("waitpid");
    }
}

int main()
{
    int cnt = 1;
    while(true)
    {
        data.push_back(cnt++);
        sleep(1);

        if(cnt % 10 == 0)
        {
            Save();
        }
    }
    return 0;
}

非阻塞轮询调度,这里给waitpid第三个参数传WNOHANG(W表示wait,NO HANG表示不挂起)

父进程一边做自己的事情,一边等待子进程退出:

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if (id == 0) // 子进程
    {
        while (true)
        {
            printf("我是子进程,pid: %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }
    // 父进程
    while (true)
    {
        sleep(1);
        pid_t rid = waitpid(id, nullptr, WNOHANG);
        if(rid > 0)
        {
            printf("等待子进程%d成功\n",rid);
            break;
        }
        else if (rid < 0)
        {
            printf("等待子进程失败\n");
            break;
        }
        else
        {
            printf("子进程尚未退出\n");
            //父进程做自己的事情
            printf("我是父进程!\n");

        }
        
    }
}

waitpid返回值:

1.如果大于0,等待成功,返回目标子进程的pid

2.如果等于0,阻塞等待一般不会返回这,在非阻塞等待中,表示等待成功,但子进程还没有退

3.如果小于0,等待失败

非阻塞式等待,让父进程进行完成任务:

task.h

cpp 复制代码
#pragma once
#include <iostream>

//打印日志任务
void PrintLog();

//下载任务
void DownLoag();

//备份任务
void BackUp();

task.cc

cpp 复制代码
#include "task.h"

//打印日志任务
void PrintLog()
{
    std::cout << "Print Log task" << std::endl;
}

//下载任务
void DownLoag()
{
    std::cout << "DownLoad task" << std::endl;
}

//备份任务
void BackUp()
{
    std::cout << "BackUp task" << std::endl;
}

main.cc

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
#include "task.h"

typedef std::function<void()> task_t;
// using task_t = std::function<void()>;

void LoadTask(std::vector<task_t> &tasks)
{
    tasks.push_back(PrintLog);
    tasks.push_back(DownLoag);
    tasks.push_back(BackUp);
}

int main()
{
    // 任务列表
    std::vector<task_t> tasks;
    LoadTask(tasks); // 加载任务

    pid_t id = fork();
    if (id == 0) // 子进程
    {
        while (true)
        {
            printf("我是子进程,pid: %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }
    // 父进程
    while (true)
    {
        sleep(1);
        pid_t rid = waitpid(id, nullptr, WNOHANG);
        if (rid > 0)
        {
            printf("等待子进程%d成功\n", rid);
            break;
        }
        else if (rid < 0)
        {
            printf("等待子进程失败\n");
            break;
        }
        else
        {
            printf("子进程尚未退出\n");
            // 父进程做自己的事情
            for (auto &task : tasks)
            {
                task();
            }
        }
    }
}

四.进程程序替换

1.快速见一见

在这章内容前面写的代码,子进程执行的永远是父进程代码的一部分,如果想执行新的程序呢?该怎么办呢?

使用exec系列函数。

1.execl

先看execl,他的作用就是执行指定路径下的程序,执行方法为在命令行上输入的形式,也就是命令行怎么写,参数怎么传!!!

这种特性就叫做,进程的程序替换

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>

int main()
{
    execl("/usr/bin/top","top",nullptr);
    return 0;
}

就可以直接执行top指令!!!

1.程序替换是创建了新的进程吗?

不是!替换知识替换了代码和数据,然后改改页表的映射就可以了

验证问题:

1.没有创建新的进程,观看如下代码,其执行结果
other.c

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

int main()
{
    printf("我是other进程,pid: %d\n",getpid());
    return 0;
}

main.c c

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>

int main()
{
    printf("我是myexec,pid: %d\n",getpid());
    execl("./other","other",nullptr);

    //execl("/bin/ls","ls","-1","-a","-n",nullptr);
    //execl("/usr/bin/top","top",nullptr);
    printf("hello");
    return 0;
}

如果没有创建新的进程,二者打印的pid相同,否则pid不同,如上结果,所有程序替换没有创建新的进程,但执行了不同的程序。

细心的人会观察到,为什么没有打印hello呢?

因为上面也说了,程序替换的本质就是覆盖原来代码和数据,原来代码数据被覆盖,自然而然就没法打印。

2.execl返回值

成功就没有返回值,因为后面代码数据都被覆盖了所以不可能有返回值,失败了就没覆盖成功,返回-1.

如下代码演示了失败的情况:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>

int main()
{
    printf("我是myexec,pid: %d\n",getpid());
    //execl("./other","other",nullptr);

    int n = execl("/bin/lsssssss","ls","-1","-a","-n","--color",nullptr);

    printf("execl return val: %d\n",n);
    //execl("/usr/bin/top","top",nullptr);
    printf("hello");
    return 0;
}

这意味着,只要返回,就是失败,exit也有返回值,但因为直接进程终止,不需要考虑。

OS把程序加载进内存中,所有OS肯定会给留系统调用,所以可以通过exec*系列进行加载,这些接口的本质就相当于把可执行程序加载进内存。

如果替换自己本身,就会无线循环递归:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>

int main()
{
    printf("我是myexec,pid: %d\n",getpid());
    //execl("./other","other",nullptr);

    //int n = execl("/bin/ls","ls","-1","-a","-n","--color",nullptr);
    int n = execl("./myexec","myexec",nullptr);

    printf("execl return val: %d\n",n);
    //execl("/usr/bin/top","top",nullptr);
    printf("hello");
    return 0;
}

再来看下面代码,给下面代码套上一个死循环,每次获取命令行上输入的指令,然后fork,让子进程执行输入的字符串,灵活调用exec*系列的函数,就可以执行任何命令,这样既可以完成一个仿命令行解释器,原理就是这样:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

//由子进程执行程序
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        sleep(3);
        // child
        execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
        //走到这一定是失败的
        exit(1);
    }
    pid_t rid = waitpid(id,nullptr,0);
    if(rid > 0)
    {
        printf("等待子进程成功!\n");
    }
    return 0;
}

在Linux中,所有的进程都是由父进程创建的,比如登陆Linux系统,在命令行上执行命令,全部都是shell的子进程,所有系统是怎么把我们程序跑起来的呢?先fork,然后做程序替换,不就可以把我们程序跑起来了吗。

所以之前的问题,是先有数据结构还是先有代码数据?

因为你所有的程序都是子进程,得先是一个子进程,所以先创建PCB,怎么创建PCB,fork,

fork的时候, 不就没有代码没有数据吗,是父进程代码数据,fork之后,先把PCB创建出来,想运行你自己的程序,直接使用exec*系列的函数,不久有了一个新的进程吗,

所以Linux下,所有的软件,都是fork,然后exec*系列跑起来的,

所有有这个思想,既然能跑起来我们的程序,那么创建python,java程序一样可以调用起来。

父进程fork,子进程调用exec*系列函数替换程序,就要发生写时拷贝,所以独立开了,进程就可以彻底独立
堆和栈,进程程序替换,如果历史上用过,系统会,重新把堆栈初始化,恢复到最开始,如果没使用,就不会和父进程干扰。

2.execv

execv与execl区别,把第二个往后的参数放入指针数组里面,argv是不是很熟悉,就是之前命令行参数里面讲的参数列表,命令行参数

看看下面代码,看看如何使用execv:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

//由子进程执行程序
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        char *const argv[] = {
            (char *)"ls",//因为是字符串常量,强转一下避免警告
            (char *)"--color",
            (char *)"-a",
            (char *)"-l",
            nullptr
        };
        execv("/usr/bin/ls",argv);
        // child
        //execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
        //走到这一定是失败的
        exit(1);
    }
    pid_t rid = waitpid(id,nullptr,0);
    if(rid > 0)
    {
        printf("等待子进程成功!\n");
    }

    return 0;
}

execv与execl中,l代表list,v代表vector

我们程序必须从新的程序的mian函数开始执行,都要从main函数开始传递参数,所以命令行参数是怎么传递给main函数的呢,可以通过调用execv,自己的程序是先fork,然后exec*创建出来的,命令行给我们构建出对应的数组,然后可以通过execv把函数传递给我们自己程序的main函数,execv不是系统调用吗,系统可以帮我们找到main函数,把参数传递过去。

execl函数后面的可变参数,这个函数内部会自动把这些参数转换成argv这个表,顺便可以把个数也统计出来

3.execlp

根据上面俩个函数,可以知道,有l说明是可变参数,那么p又是什么呢?

p代表的是,不用带路径,他会自动根据环境变量PATH中的路径中去找对应的命令

第一个参数永远是你想执行谁,后面参数永远是你想怎么执行!!如下代码,重复两个ls,虽然内容一样,但是表达的含有不同。

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

//由子进程执行程序
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        execlp("ls","ls","--color","-a","-n","-l",nullptr);
        //execv("/usr/bin/ls",argv);
        // child
        //execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
        //走到这一定是失败的
        exit(1);
    }
    pid_t rid = waitpid(id,nullptr,0);
    if(rid > 0)
    {
        printf("等待子进程成功!\n");
    }
    return 0;
}

4.execvp

根据上面讲的,v就是传参数列表,p就是不用带路径

如下代码使用方式:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

//由子进程执行程序
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        char *const argv[] = {
            (char *)"ls",//因为是字符串常量,强转一下避免警告
            (char *)"--color",
            (char *)"-a",
            (char *)"-l",
            nullptr
        };
        execvp("ls",argv);
        //execlp("ls","ls","--color","-a","-n","-l",nullptr);
        //execv("/usr/bin/ls",argv);
        // child
        //execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
        //走到这一定是失败的
        exit(1);
    }
    pid_t rid = waitpid(id,nullptr,0);
    if(rid > 0)
    {
        printf("等待子进程成功!\n");
    }
    return 0;
}

下面一种传参形式更加优雅:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

//由子进程执行程序
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        char *const argv[] = {
            (char *)"ls",//因为是字符串常量,强转一下避免警告
            (char *)"--color",
            (char *)"-a",
            (char *)"-l",
            nullptr
        };
        execvp(argv[0],argv);
        //execlp("ls","ls","--color","-a","-n","-l",nullptr);
        //execv("/usr/bin/ls",argv);
        // child
        //execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
        //走到这一定是失败的
        exit(1);
    }
    pid_t rid = waitpid(id,nullptr,0);
    if(rid > 0)
    {
        printf("等待子进程成功!\n");
    }
    return 0;
}

5.execvpe

这里的e又是什么意思呢?

这里的e表示传环境变量env。环境变量

子进程在创建的时候即使不跟我们传环境变量,也是会被子进程拿到,通过全局指针environ来拿到的

不传环境变量:

main.cc

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

//由子进程执行程序
int main()
{
    pid_t id = fork();
    if (id == 0)
    {

        execl("./other","other",nullptr);

        //execvp(argv[0],argv);
        //execlp("ls","ls","--color","-a","-n","-l",nullptr);
        //execv("/usr/bin/ls",argv);
        // child
        //execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
        //走到这一定是失败的
        exit(1);
    }
    pid_t rid = waitpid(id,nullptr,0);
    if(rid > 0)
    {
        printf("等待子进程成功!\n");
    }
    return 0;
}

other.c

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

extern char**environ;

int main()
{
    for(int i = 0;environ[i];i++)
    {
        printf("evn[%d]: %s\n",i,environ[i]);
    }
    return 0;
}

程序替换不影响环境变量,因为具有全局性,可以被所以人看到。

下面我们看看来手动传环境变量:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

//由子进程执行程序
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        char *const argv[] = {
            (char *)"other",
            // (char *)"ls",//因为是字符串常量,强转一下避免警告
            // (char *)"--color",
            // (char *)"-a",
            // (char *)"-l",
            nullptr
        };
        char * const env[] = {
            (char*)"HELLO=bite",
            (char*)"HELLO1=bite1",
            (char*)"HELLO2=bite2",
            (char*)"HELLO3=bite3",
            nullptr
        };
        execvpe("./other",argv,env);

        //execl("./other","other",nullptr);

        //execvp(argv[0],argv);
        //execlp("ls","ls","--color","-a","-n","-l",nullptr);
        //execv("/usr/bin/ls",argv);
        // child
        //execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
        //走到这一定是失败的
        exit(1);
    }
    pid_t rid = waitpid(id,nullptr,0);
    if(rid > 0)
    {
        printf("等待子进程成功!\n");
    }
    return 0;
}
cpp 复制代码
#include <stdio.h>
#include <unistd.h>

extern char**environ;

int main()
{
    for(int i = 0;environ[i];i++)
    {
        printf("evn[%d]: %s\n",i,environ[i]);
    }
    return 0;
}

使用execvp进行手动去传环境变量 ,他的意思是使用全新的环境变量,传递给目标程序,而不是追加传递

关于环境变量:

1.让子进程继承父进程全部的环境变量(默认)

2.如果要使用全新的环境变量(自己定义传递)

3.如果要追加传递呢?
我们回顾一下获取环境变量有一个方式是getenv, 还有一个增加环境变量的方式putenv。

如下代码,在全局自定义一个环境变量myenv,然后定义一个全局指针environ,使用putenv把该变量追加到环境变量中,然后使用execvpe,通过传environ,把父进程的环境变量传过去,这样子进程就能拿到追加后的环境变量。

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

const std::string myenv = "HELLO=AAAAAAAAAAAAAAA";


extern char **environ;
//由子进程执行程序
int main() 
{
    putenv((char*)myenv.c_str());
    pid_t id = fork();
    if (id == 0)
    {
        char *const argv[] = {
            (char *)"other",
            // (char *)"ls",//因为是字符串常量,强转一下避免警告
            // (char *)"--color",
            // (char *)"-a",
            // (char *)"-l",
            nullptr
        };
        char * const env[] = {
            (char*)"HELLO=bite",
            (char*)"HELLO1=bite1",
            (char*)"HELLO2=bite2",
            (char*)"HELLO3=bite3",
            nullptr
        };

        execvpe("./other",argv,environ);

        //execl("./other","other",nullptr);

        //execvp(argv[0],argv);
        //execlp("ls","ls","--color","-a","-n","-l",nullptr);
        //execv("/usr/bin/ls",argv);
        // child
        //execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
        //走到这一定是失败的
        exit(1);
    }
    pid_t rid = waitpid(id,nullptr,0);
    if(rid > 0)
    {
        printf("等待子进程成功!\n");
    }

    return 0;
}

putenv返回值,如果成功返回0,如果失败返回非0值,并设置错误码来提示
结论:程序替换不影响环境变量和命令行参数

6.execle和execve

认识上面知识后,就可以轻易知道这两个函数怎么使用。

execle,传可变参数,要传路径,要传环境变量。

execve,传参数列表,传路径,传环境变量。

补充:

除了了execve是系统调用接口,其他的都是语言级接口,底层都是封装的execve!!!

相关推荐
Luke~~30 分钟前
实验室服务器Ubuntu安装使用全流程
linux·运维·ubuntu
hc_bmxxf35 分钟前
Linux应用软件编程-多任务处理(线程)
linux·线程
看星猩的柴狗39 分钟前
密码学原理技术-第二章-Stream Ciphers
服务器·网络·密码学
机器视觉知识推荐、就业指导41 分钟前
C++设计模式:解释器模式(简单的数学表达式解析器)
c++·设计模式·解释器模式
海螺姑娘的小魏41 分钟前
Effective C++ 条款 16:成对使用 `new` 和 `delete` 时要采取相同形式
开发语言·c++
tjjingpan1 小时前
在国产电脑上运行PDFSAM软件使用pdf分割合并交替混合处理pdf文档
linux·pdf
hope_wisdom1 小时前
Linux系统编程之目录遍历
linux·linux编程·readdir·目录遍历·scandir
点云SLAM2 小时前
C++创建文件夹和文件夹下相关操作
开发语言·c++·算法
存储服务专家StorageExpert2 小时前
墙裂推荐:console, CLI命令行和日志使用字体-Cascadia
运维·服务器·netapp存储·存储维护·emc存储
CodeClimb2 小时前
【华为OD-E卷 - 猜字谜100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od