C++项目实战-多进程(一篇文章)(二)

简介: C++项目实战-多进程(一篇文章)(二)

进程共享(父子进程虚拟地址空间情况)

fork之后父子进程的异同:

相同:全局变量、data、bss、.txt、堆栈、环境变量、用户ID、当前工作目录.....

不同:进程ID   fork的返回值  父进程ID   进程运行的时间   定时器    未决信号集

似乎子进程复制了父进程0~3G用户空间的内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要讲父进程0-3G地址完全拷贝一份吗?然后再映射至物理内存吗?

前面说到,虚拟地址空间并不是说真的需要4G的空间,往往只需要实现逻辑结构的页表、页目可以了。而且父子进程间遵循读时共享写时复制的原则。无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。(如果只有读操作==>父子进程共享一块物理内存,只要有了写操作==>进行复制物理内存)

在这里我们一定要分清楚虚拟地址空间和物理内存,虚拟地址空间相当于在程序与物理内存之间的中间层。

父子进程共享:文件描述符表   mmap建立的映射区(重点)

GBD多进程调试(重点)

使用GDB调试的时候,gdb只能跟踪一个进程。可以在fork之前,通过指令设置gdb调试工具跟踪父进程还是子进程。默认跟踪父进程

set follow-fork-mode child

set follow-fork-mode parent

如果有多个子进程怎么处理呢?

使用gdb条件调试:b if i = 2     set follow-fork-mode child

默认情况下,当调试一个进程时,其他的进程也在运行。

如果想在调试某一个进程的时候,其他进程被挂起可以设置调试模式:

set detach-on-fork on/off

查看调试的进程:info inferiors

切换当前调试的进程: inferior id

使进程脱离GDB调试:defach inferiors id

exec函数族(重点掌握两个就可以了)

fork创建子进程后执行的是父进程相同的程序(但有可能执行不同的代码分支)。

通过exec函数可以使子进程执行另一个程序。

原理:当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行(你就理解位从main函数执行就可以了)。调用exec函数不创建新进程,只是进程代码和数据的替换,故进程id不会发生改变。

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 execve(const char *path,char *argv[],char *const envp[]);

#include <iostream>
#include <unistd.h>
using namespace std;
int main(void)
{
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork");
        exit(-1);
    }
    if(pid == 0)
    {
        execl("/bin/ls","ls","-l",NULL);
    }
    else
    {
        cout<<"草泥马..."<<endl;
    }
    return 0;
}
#include <iostream>
#include <unistd.h>
using namespace std;
int main(void)
{
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork");
        exit(-1);
    }
    if(pid == 0)
    {
       execlp("ls","ls","-l",NULL);
    }
    else
    {
        cout<<"草泥马..."<<endl;
    }
    return 0;
}
#include <iostream>
#include <unistd.h>
using namespace std;
int main(void)
{
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork");
        exit(-1);
    }
    if(pid == 0)
    {
        execl("./a","a",NULL);
    }
    else
    {
        cout<<"草泥马..."<<endl;
    }
    return 0;
}
#include <iostream>
using namespace std;
int main(void)
{
    cout<<"我是你大爷...."<<endl;
    return 0;
}
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
int main(void)
{
   int fd;
   fd = open("ps.out",O_RDWR|O_CREAT,0664);
   dup2(fd,STDOUT_FILENO);
   execlp("ps","ps","aux",NULL);
    return 0;
}

回收子进程

孤儿进程

       父进程先于子进程结束,则子进程成为孤儿进程,会被进程孤儿院收养。

       进程孤儿院:init (1号进程)

       孤儿进程不会有影响,安全

       说一下init进程:

               init进程会循环的wait()它的已经退出的子进程。这样当一个孤儿进程凄凉的结束了,init进程就会代表党和政府处理一切善后工作。

僵尸进程

       进程终止,父进程尚未回收,子进程残留资源仍然存放在内核中,变成僵尸进程。【 每一个进程结束之后,都会释放自己的用户区,内核的部分。但内核中PCB没有办 法自己释放掉,需要父进程区释放。进程终止时,父进程尚未回收,子进程残留资源存在内核中,成为僵尸进程。如此以来就会导致一个问题,如果父进程不调用wait()或waitpid()的话,那么保留的那段信息将不会被释放掉。那么该进程的进程号就一直处于被占用状态,但是系统能够提供的进程号数量是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致不能产生新的进程

       如何解决处理僵尸进程:

       1.让僵尸进程变成孤儿进程,杀死其父进程就可以了。那么这个僵尸进程就会被init收养,init内部调用了wait函数来回收子进程    kill -9  父进程ID

       2.父进程通过wait或者waitpid函数回收子进程

wait()和waitpid函数

       一个进程在终止时会关闭所有的文件描述符,释放在用户空间分配的内存,但它的PCB中还保留着,内核在其中保留了一些信息。如果是正常退出保存退出状态,如果是异常终止则保留着导致异常终止的信号。其父进程可以调用wait和waitpid获取这些信息,然后彻底清除。

       

       pid_t wait(int *status);

       成功:清除掉的子进程ID   失败:-1

       功能:

               阻塞等待子进程退出

               回收子进程残留资源

               获取子进程结束状态(退出状态)

        如何获取退出状态(系统定义的宏,调用这些宏就可以了):

       1.WIFEXITED(status)  为非0    --> 进程正常退出

          WEXITSTATUS(status)  --> 获取进程退出状态

       2.WIFSIGNALED(status) 为非0 --> 进程异常终止

           WTERMSIG(status)  --> 获取导致进程终止的信号编号

       3.WIFSTOPPED(status) --> 进程处于暂停状态

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main(void)
{
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork");
        exit(-1);
    }
    if(pid > 0)
    {
        cout<<"我是你爹爹"<<endl;
        int status;
        int res = wait(&status); //阻塞等待
        if(WIFEXITED(status))
        {
            cout<<WEXITSTATUS(status)<<endl;
        }
    }
    else if(pid == 0)
    {
        cout<<"我是你崽...wuwu"<<endl;
        exit(-3);
    }
    return 0;
}

通过信号来终止子进程

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main(void)
{
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork");
        exit(-1);
    }
    if(pid > 0)
    {
        cout<<"我是你爹爹"<<endl;
        int status;
        int res = wait(&status); //阻塞等待
        if(WIFSIGNALED(status))
        {
            cout<<WTERMSIG(status)<<endl;
        }
    }
    else if(pid == 0)
    {
        cout<<getpid()<<endl;
        while(1)
        {
            sleep(2);
            cout<<"我是你崽...wuwu"<<endl;
        }
    }
    return 0;
}

waitpid函数

wait()和waitpid()函数的功能一样,区别在于wait()函数会阻塞,waitpid()可以设置不阻塞,waitpid()还可以指定等待哪个子进程结束。

一次wait和waitpid调用只能清理一个子进程,清理多个子进程应使用循环

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

参3:

0   阻塞

WNOHANG 非阻塞

进程退出

#include <stdlib.h>

void exit(int status);

------------------------------------------

#include <unistd.h>

void _exit(int status);

进程间通信(IPC方法)

基本概念:

进程是一个独立的资源分配单元(进程使操作系统分配资源的基本单位),不同进程之间的资源使独立的,没有联系的,不能在一个进程中直接访问另一个进程的资源。但是,进程不是孤立存在的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。

进程间通信的目的:

数据传输:一个进程需要将它的数据发送给另一个进程

通知事件:一个进程需要向另一个或一组进程发消息,

                 通知它发生了某种事程(如进程终止要通知给父进程)

资源共享:多个进程之间共享同样的资源

进程控制:一些进程希望完全控制另一个进程的执行(DEBUG)

Linux进程间通信的方式

同一主机:管道(有名管道、匿名管道)  信号  消息队列  共享内存 内存映射 本地套接字

不同主机:socket套接字

进程间通信的基础

所有进程的内核空间所映射的物理内存是同一块

匿名管道

       我们通常称的管道就是匿名管道,它是UNIX系统IPC(进程间通信)的最古老的方式,所有UNIX系统都支持这种通信机制。

       统计一个目录中文件的数目命令:

       ls | wc -l  (ls和wc为两个不同的进程 |是管道符)

       

 

管道的特点

       管道其实是一个在内核中维护的缓冲区,这个缓冲区的存储能力是有限的。ulimit -a

       管道拥有文件的特质,读操作和写操作,匿名管道没有文件实体(伪函数),有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。

       一个管道是一个字节流,使用管道是不存在消息或消息边界的概念。

       通过管道传递的数据是顺序的,从管道中读取出来的字节顺序和它们被写入管道的顺序是完全一样的。

       管道是半双工的

       匿名管道只能在具有公共祖先的进程之间使用

管道的数据结构

       循环队列

       

管道的使用

       int pipe(int pipefd[2]);

       功能:创建一个管道

       参数:int pipefd[2]这个数组是一个传出参数

                  pipefd[0]  对应的是管道的读端

                  pipefd[1]  对应的是管道的写端

       返回值:

                 成功 0

                 失败 -1

       注意:匿名管道只能用于具有关系的进程之间的通信(父子,兄弟)

       管道默认是阻塞的,如果管道中没有数据,read阻塞,如果管道满了,write阻塞

       

       管道创建成功之后,创建管道的进程拥有管道的读端和写端....父子进程之间想要通信

       

看管道缓冲区的大小

       ulimit -a

       long fpathconf(int fd,int name)

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main(void)
{
    //创建管道
    int pipefd[2];
    int ret = 0;
    ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        exit(-1);
    }
    //fork子进程
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork");
        exit(-2);
    }
    if(pid > 0)
    {
        char buf[256] = "aaaaa";
        close(pipefd[0]);   //关闭读端
        write(pipefd[1],buf,sizeof(buf));
        int status;
        wait(&status);
    }
    else if(pid == 0)
    {
        close(pipefd[1]);   //关闭写端
        char buf[256];
        while(1)
        {
            int ret = read(pipefd[0],buf,sizeof(buf));
            if(ret == -1)
            {
                perror("read");
                exit(-3);
            }
            if(ret == 0)
            {
                break;
            }
            cout<<buf<<endl;
        }
    }
    return 0;
}

管道读写行为  

       读管道:

               1.管道中有数据,read返回实际读到的字节数

               2.管道中无数据:

                               管道写端被全部关闭,read返回0

                               管道写端没有全部关闭,read阻塞等待

       写管道:        

               1.管道读端全部关闭,进程异常终止(SIGPIPE)

               2.管道读端没有全部关闭

                               管道已满,write阻塞

                               管道未满,write将数据写入,返回实际写入到字节数

管道的优缺点

       优点:简单

       缺点:只能单向通信,双向通信需要建立两个管道

               只能用于有血缘关系的进程间

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
7月前
|
存储 Serverless C++
【C++航海王:追寻罗杰的编程之路】一篇文章带你认识哈希
【C++航海王:追寻罗杰的编程之路】一篇文章带你认识哈希
58 0
|
8月前
|
Python Windows
从菜鸟到大神:一篇文章带你彻底搞懂Python并发编程——线程篇与进程篇的深度较量!
【7月更文挑战第10天】Python并发编程对比线程与进程。线程适合IO密集型任务,利用`threading`模块,但GIL限制CPU并行。进程适用于CPU密集型任务,通过`multiprocessing`实现,独立内存空间,启动成本高。例子展示了如何创建和管理线程与进程以提高效率。选择取决于任务类型和资源需求。
53 0
|
8月前
|
监控 关系型数据库 MySQL
守护进程到底是什么?如何创建?(图文并茂,你不得不看的一篇文章)
**守护进程(Daemon Process)详解**:守护进程是后台运行的无终端关联的系统进程,常在启动时启动,提供持续服务,如网络服务、日志记录和定时任务。其特点包括脱离终端、后台运行、持久服务、资源管理和错误处理。创建守护进程涉及重定向文件描述符、创建新会话、改变工作目录等步骤。`ps` 和 `top` 命令用于查看守护进程,前者提供进程快照,后者显示实时资源使用情况。
535 0
|
9月前
|
存储 缓存 NoSQL
Redis系列学习文章分享---第十三篇(Redis多级缓存--JVM进程缓存+Lua语法)
Redis系列学习文章分享---第十三篇(Redis多级缓存--JVM进程缓存+Lua语法)
99 1
|
9月前
|
Unix Linux 调度
一篇文章讲明白linux僵死进程
一篇文章讲明白linux僵死进程
69 0
|
9月前
|
存储 C++
【C++航海王:追寻罗杰的编程之路】一篇文章带你了解二叉搜索树
【C++航海王:追寻罗杰的编程之路】一篇文章带你了解二叉搜索树
48 1
|
9月前
|
存储 调度 C++
【操作系统】进程与线程的区别及总结(非常非常重要,面试必考题,其它文章可以不看,但这篇文章最后的总结你必须要看,满满的全是干货......)
【操作系统】进程与线程的区别及总结(非常非常重要,面试必考题,其它文章可以不看,但这篇文章最后的总结你必须要看,满满的全是干货......)
339 1
|
10月前
|
C++ iOS开发
C++ 文件操作的技术性文章
C++ 文件操作的技术性文章
50 0
|
10月前
|
消息中间件 存储 算法
【C/C++ 泡沫精选面试题04】在实际项目中,多进程和多线程如何选择?
【C/C++ 泡沫精选面试题04】在实际项目中,多进程和多线程如何选择?
282 1
|
存储 安全 编译器
【C++】C++入门详解 II【深入浅出 C++入门 这一篇文章就够了】(下)
【C++】C++入门详解 II【深入浅出 C++入门 这一篇文章就够了】(上)
115 0

相关实验场景

更多