进程地址空间

一个程序

cpp 复制代码
#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
        int val=0;
        pid_t pid=fork();
        if(pid==0)
        {
                val+=10;
                cout<<"我是子进程"<<"pid是"<<" " <<getpid()<<"父进程id是"<<getppid()<<" "<<"val是"<<val<<" " <<"val的地址是"<<&val<<endl;
        }
        else
        {
                cout<<"我是父进程"<<"pid是"<<" "<<getpid()<<"val是"<<val<<" "<<"val的地址是"<<&val<<endl;
        }


        return 0;
}

运行结果

我们发现,父子进程的中val的地址都是一样的,但是打印出来的结果却不一样,原因如下打印的地址是逻辑地址,并非是val的物理地址,在创建子进程的时候,子进程会将父进程的代码数据继承过去,如果数据没有改变,他们的数据会指向同一个地址,但是如果子进程改变了数据,就会发生写时拷贝,逻辑地址不变,但是将实际的数据拷贝一份,放入新的物理地址,图片如下

再来解释一下刚开始的时候,每个进程都有自己的PCB,每一个PCB,也就是一个结构体,当创建一个子进程的时候会将mm_struct拷贝一份,这个时候父子进程数据的页表中的虚拟地址和物理地址都是一样的,但是当子进程改变了数据的话,子进程的数据的物理地址会改变的

进程的创建

我们来探讨一下写时拷贝相关的原理

子进程的创建本质上就是父进程代码和数据的拷贝,当然,页表中的数据也是一样会被拷贝的,页表中除了有虚拟地址到物理地址的映射,还有数据的读写权限,当子进程想要修改数据的时候,发现数据是只有读的权限的时候,会触发错误,系统检测到发生了错误就会判断是否要进行写时拷贝,如果是写时拷贝的话,就会修改页表,申请空间之类的操作

进程的终止

main是有返回值的,我们写代码都是很清楚的,但是为什么要返回要有返回值嘞?这个返回值就是错误码,是用来方便父进程知道子进程的运行情况的,如果返回的是0,就表示这个程序运行成功了,如果是非0的话,就表明这个程序运行失败了,这也是为什么我们的在写main程序的时候是返回0的原因了

下面我们来介绍几个命令

echo $?

这个命令返回的是最近的一个进程结束的错误码

errno和strerror

这两个函数一个是错误码,一个是错误码对应的错误信息,通常在程序里面使用

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

using namespace std;


int main()
{
        FILE *fd=fopen("./no_such_file.txt","w");
        cout<<"errno"<<errno<<" "<<"错误信息"<<strerror(errno)<<endl;
        return 0;
}

运行结果

我们可以来打印一看各种状态码对应的状态信息

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

using namespace std;


int main()
{
        for(int i=0;i<=20;i++)
        {
                cout<<"errno"<<i<<" "<<"状态码"<<strerror(i)<<endl;
        }
        return 0;
}

运行结果

进程的终止

进程的终止有下面两种方式

1.main函数里面的return 0

2.exit

cpp 复制代码
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>

using namespace std;

void func()
{
        cout<<"你好"<<endl;
        exit(0);
}

int main()
{
        func();
        cout<<"hello world"<<endl;
        return 0;
}

函数运行结果

从代码的执行情况来看,exit函数无论在哪一层栈帧,只要出现,就会终止整个进程,mian函数里面的hello world都没有打印

和_exit()函数的区别:exit函数在会将缓冲区里面的内容打印在出来,_exit不会

cpp 复制代码
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
#include<cstdio>
using namespace std;


int main()
{

        cout<<"hello world"<<endl;
        exit(0);
}

运行打印:hello world

cpp 复制代码
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
#include<cstdio>
#include<unistd.h>
using namespace std;


int main()
{

        printf("hello world/n");
        _exit(0);
}                                  //不会打印hello world

进程等待

在父子进程当中,如果子进程运行完毕,但是父进程父进程没有运行完的话,父进程就会卡在哪里去先去回收子进程,类似于scanf函数一样等待用户输入

介绍一个函数

pid_t wait(int *status),父进程要使用这个函数阻塞等待去回收子进程,status返回子进程的状态信息,等待成功返回子进程的pid,失败返回-1

cpp 复制代码
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
#include<cstdio>
#include<unistd.h>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;


int main()
{

        pid_t pid=fork();
        if(pid==0)
        {
                printf("hello world\n");
                sleep(5);
                exit(5);

        }
        else
        {
                int status=0;
                wait(&status);
                printf("我是父进程\n");
                printf("%d",status);

        }

        exit(0);
}

等待子进程运行完5秒之后父进程才开始运行

waitpid方法

#include <sys/types.h>

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);
1. pid - 指定要等待的子进程

  • pid > 0:等待进程ID等于pid的特定子进程

  • pid = -1:等待任意子进程(等同于wait

  • pid = 0:等待与调用进程在同一进程组的任意子进程

  • pid < -1:等待进程组ID等于|pid|的任意子进程

2. status - 存储子进程退出状态

  • 如果不为NULL,存储状态信息

  • 可用宏解析状态:

    • WIFEXITED(status):是否正常退出

    • WEXITSTATUS(status):获取退出码

    • WIFSIGNALED(status):是否被信号终止

    • WTERMSIG(status):获取终止信号

    • WIFSTOPPED(status):是否被信号暂停

    • WSTOPSIG(status):获取暂停信号

3. options - 控制行为选项

  • 0:阻塞等待直到子进程结束

  • WNOHANG:非阻塞,若无子进程退出立即返回0

  • WUNTRACED:也返回被暂停的子进程状态

  • WCONTINUED:也返回被恢复的子进程状态(Linux 2.6.10+)

返回值

  • 成功:返回状态变化的子进程ID

  • 失败:返回 -1(设置errno)

  • WNOHANG 且无子进程退出:返回0

cpp 复制代码
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
#include<cstdio>
#include<unistd.h>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;


int main()
{
        pid_t id=fork();
        if(id==0)
        {
                printf("我是子进程,pid是%d\n",getpid());
                exit(123);                  //自定义退出码
        }
        else
        {
                int status=0;
                pid_t ret=waitpid(id,&status,0);         //0:第三个参数为0,阻塞到子进程结束
                if(ret==-1)                               //等待失败
                {
                        perror("waitpid");
                        exit(1);
                }
                //子进程正常退出
                printf("我是父进程,子进程正常退出,退出码是%d\n",status>>8);

        }
        return 0;

}

运行结果

在上面打印退出码的时候status需要右移动8位才能得到真正的退出码

因为次高八位才是存储状态码的地方,低八位是退出的信号值

正常退出

8-15位: 退出状态码(0-255)

bits 0-7: 全为 0
被信号终止

bits 0-6: 终止信号编号(1-31)

bit 7: 是否产生core dump(通常为1)

bits 8-15: 0

再实际使用中,如果子进程一直没有运行完毕,那需要一直让父进程去等待他吗,答案是否定的,因此我们需要设置第三个参数

cpp 复制代码
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
#include<cstdio>
#include<unistd.h>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;


int main()
{
        pid_t pid = fork();
        if (pid == 0)
         {
                sleep(5);
                exit(0);
        }
        else
         {
        int status;
        pid_t ret;

        while ((ret = waitpid(pid, &status, WNOHANG)) == 0) {
                printf("子进程仍在运行,执行其他任务...\n");
                sleep(1);
         }

        if (ret == pid)
         {
                printf("子进程已结束\n");
        }
}

运行结果:

这样子进程没有运行结束父进程就可以去做其他的事情了,这叫做非阻塞等待

进程程序替换

下面我们来介绍几个函数

excel

cpp 复制代码
 int execl(const char *path, const char *arg, ...);

参数说明

  • path :可执行文件的完整路径(如 /bin/ls

  • arg:可变参数列表,依次传递命令行参数

    • 第一个参数通常是程序名(惯例)

    • 最后一个参数必须是 (char *)NULL 作为结束标记

  • 返回值:成功不返回(当前进程被替换),失败返回 -1

cpp 复制代码
#include<iostream>
#include<unistd.h>
using namespace std;


int main()
{
        execl("/bin/ls","ls","-a","-l",nullptr);
        return 0;
}

运行结果

注意,这里是程序替换,也就是说,是用参数里面的可执行程序去替代现在的进程

我们还可以用来执行自己的程序

cpp 复制代码
#include<iostream>
#include<unistd.h>
using namespace std;


int main()
{
        execl("/home/LiHao/practice/test202645/helloworld","helloworld",nullptr);
        return 0;
}

其中helloworld是helloworld.c的可执行程序,里面的内容是打印hello world

execlv

cpp 复制代码
 int execv(const char *path, char *const argv[]);
参数 含义
path 可执行文件的路径(绝对路径或相对路径)-3-8
argv[] 参数数组,第一个元素通常是程序名,最后一个必须是 NULL

比如下面的

cpp 复制代码
int main()
{
        const char *argv[]={"ls","-a","-l",NULL};
        execv("/bin/ls",argv);
}

execlp

cpp 复制代码
int execlp(const char *file, const char *arg, ...);

关键特性

  1. 不会返回 :执行成功时,execlp 不会返回,当前进程的代码被新程序完全替换

  2. 查找路径 :通过 PATH 环境变量查找 file,无需写绝对路径

  3. 参数格式

    • 第一个 arg 通常是程序名(可自定义)

    • 后面的 arg 依次对应命令行参数

    • 最后必须(char *)NULL 结尾

  4. 返回值

    • 成功:不返回

    • 失败:返回 -1,并设置 errno

cpp 复制代码
#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
        execlp("ls","ls","-a","-l",nullptr);
        return 0;
}

运行结果

但是会不会觉得第一个参数和第二个参数有点重复了?不是这样的

第一个参数 :是给内核和操作系统看的,用来定位磁盘上的文件。

第二个参数 :是给新启动的程序看的,是它的"人设"或第一个输入数据。所以,这两个参数是可以不同的

cpp 复制代码
#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
        execlp("ls","echo","-a","-l",nullptr);
        return 0;
}

我们将第二个参数变成echo,发现运行结果是一样的

execvp

cpp 复制代码
int execvp(const char *file, char *const argv[]);

这个函数也不需要带绝对路径,只是需要将参数放在一个数组里就行了,其他的返回值之类的和上一个函数是一样的

cpp 复制代码
#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
        char *argv[]={"ls","-a","-l",nullptr};  //以nullptr结尾
        execvp("ls",argv);
        return 0;
}

运行结果

execvpe

这个是带环境变量的,如果需要自己传递环境变量,可以自己定义

cpp 复制代码
int execvpe(const char *file, char *const argv[],
                   char *const envp[]);

具体先不举例子了

上面的函数可能都不好记忆:带l的是列表,带v的是vector,带p的是path,带e的是environment环境变量

相关推荐
Byte不洛2 小时前
LeetCode双指针经典题
c++·算法·leetcode·双指针
Tanecious.2 小时前
蓝桥杯备赛:Day7- P10424 [蓝桥杯 2024 省 B] 好数
c++·蓝桥杯
汀、人工智能2 小时前
[特殊字符] 第16课:最小覆盖子串
数据结构·算法·数据库架构·图论·bfs·最小覆盖子串
米粒12 小时前
力扣算法刷题 Day 34
算法·leetcode·职场和发展
Albert Edison2 小时前
【C++11】特殊类设计
开发语言·c++·单例模式·饿汉模式·懒汉模式
代码改善世界2 小时前
【C++初阶】vector 核心接口和模拟实现
开发语言·c++
田梓燊2 小时前
leetcode 189
算法·leetcode·职场和发展
今晚打老虎2 小时前
限时回归了
c++
老四啊laosi2 小时前
[C++进阶] 22. unordered_set && unordered_map使用
c++·unordered_map·unordered_set