目录

目录

目录

程序和进程的基本概念

单道程序设计和多道程序设计

并行和并发

进程控制块PCB

进程常用的命令 

环境变量

环境变量操作函数

进程控制

进程间通信(IPC方法)


程序和进程的基本概念

程序: 是指编译好的二进制文件,在磁盘上,不占用系统资源。

         包含了一系列如何创建一个进程的信息。

        1.二进制格式标识:每一个程序文件的包含用于描述可执行文件格式的信息

                                        内核利用此信息来解释文件中的其他信息。

        2.机器语言指令:对程序算法进行编码

        3.程序入口地址:标识程序开始执行时的起始指令位置

        4.数据

        5.符号表及重定位表

        6.共享库和动态链接信息

        7.其他信息

进程:运行着的程序,在内存中,占用系统资源(cpu、文件描述符、锁...)

        进程是最基本的分配单位。进程可以看作是程序的一个实例。 

单道程序设计和多道程序设计

单道程序设计:

        所有进程一个一个排队执行。

        

       如果A阻塞,B必须等待。A发生等待事件才会进入阻塞状态哦,所以此时A并没有占用CPU,那么就会导致CPU处于空闲状态。导致CPU资源的浪费(人们总是希望CPU能够永远不休息的工作)

 多道程序设计:

        在计算机内存中同时存放多个独立的程序,上一篇文章讲到了虚拟地址空间,我个人认为,虚拟地址空间存在极大的方便了多道程序设计,虚拟地址空间相当于中间层,避免了直接使用内存而导致进程空间的不隔离、效率低、地址的不确定等问题。

        多道程序能够实现离不开时钟中断技术,这是在硬件上的机制,对于进程而言具有不可抵抗力。(CPU对于每一个进程而言是特别好吃的食物,没有哪个进程愿意主动的放弃CPU,只能降维打击)。操作系统通过中断处理函数来负责调度程序的执行。

        中断处理函数:是不是就会设计到怎么去实现。这个就是我们在学校学习操作系统时学习的一系列算法了,先来先服务、时间片轮转、短进程优先等等

并行和并发

前面说到了,多道程序设计...不可避免就会涉及到并发和并行的概念

并行:

        我们经常说,多核多核..其实说的就是处理器的数量,处理器即CPU。

        给大家看一下吧:

           8个

        一个CPU在同一时刻只能被一个进程使用,但是如果有两个CPU,同时就可以执行两个进程,那么这两个进程在同一时刻被同时执行,那么就称这两个进程时并行的

给大家画个图:

           

 并发:

        我们说并行是站在多个CPU的角度,而我们讨论并发是站在一个CPU的角度。

        一个时间段,有多个进程都处于从开始运行到运行完毕的状态,但是每一个时刻只有一个程序在运行。

         CPU会产生很多时钟轮片分配给不同的进程。

在多道程序设计模型中,多个进程轮流使用CPU(分时复用CPU)。 

形成"宏观并行,微观串行"

CPU处理速度是纳秒级别,一秒钟可以执行10亿条指令

进程控制块PCB

引入:在虚拟地址空间中,有个内核空间3G-4G(至于为什么是0-4G之前也讲过,因为32位平台下,指针占用4个字节,这么一来就限定了指针的寻址范围是0x00000000-0xFFFFFFFF 刚好对应0-4G),好像有点偏题了。我们回到内存空间,内核空间主要是冠以设备管理、进程管理、内存管理等一些内容,其中进程管理就是我们现在要关注的对象了,它是通过进程控制块也称为进程描述符来实现的,其实它就是一个结构体,结构体一个很重要的作用不就是实现数据封装吗....

所以说PCB就是一个结构体,里面包含了进程相关的一系列信息。

主要包含的信息:(理解不了就强制背下来)

进程id:唯一标识每一个进程(相当于学号),类型 pid_t,其实是int类型  (typedef int pid_t)

进程的状态:开始、就绪、阻塞、运行、终止

进程切换所需要保存的和恢复的现场信息:其实就是寄存器里的值

描述虚拟地址空间的信息

描述当前控制终端的信息

当前工作目录

umask掩码:保护文件权限

文件描述符表

信号相关信息

会话和进程组:守护进程会用到

用户ID和组ID

进程的资源上限: ulimit  -a

进程常用的命令 

ps  aux / ajx 

a:显示所有进程

u:显示进程的详细信息

x:显示没有控制终端的信息

j:显示与作用控制相关的信息

top(任务管理器)

实时显示进程动态

 kill -l  显示所有信号

 kill -9 进程ID    杀死某一个进程

环境变量

操作系统中指定操作系统运行环境的一些参数。char *[]数组,数组名environ,内部存储字符串,NULL作为结束标志。

你会发现里面配的是一些路径,我们在说动态库使用的时候有说过

如何使用:

需要先什么全局变量 environ     

extern  char ** environ;          

说白了就是这些全局变量放到了environ中,需要使用的话必须先声明

#include <iostream>

using namespace std;

extern char ** environ;

int main(void)
{
    
    for(int i=0;environ[i]!=NULL;++i)
    {
        cout<<environ[i]<<endl;
    }


    return 0;
}

常见的环境变量

PATH可执行文件的搜索路径
SHELL当前的shell
TERM当前终端
LANG语言
HOME当前用户主目录的路径

环境变量操作函数

char * getenv(const char *name);

int  setenv(const char *name,const char *value,int overwrite);

int  unsetenv(const char *name);

#include <iostream>
#include <stdlib.h>

using namespace std;

int main(void)
{

    char * p = getenv("PATH");

    cout<<p<<endl;
    return 0;
}

进程控制

fork函数:创建一个子进程

pid_t fork(void);

失败返回 -1,成功父进程返回子进程的pid,子进程返回0

fork之后形成的父子进程关系,其实子进程只是把父进程的内容拷贝了一份。遵从"读时共享,写时复制"。这句话是说当父子进程有写操作的时候,就会单独给子进程映射一块物理内存,防止父子进程操作同一个数据而导致数据错误。注意这里说的是物理内存,不是虚拟内存,每一个进程都有一个自己的虚拟内存空间,这个是进程刚创建的时候操作系统就完成。

循环创建n个子进程

#include <unistd.h>
#include <iostream>

using namespace std;

int main(void)
{
    int i;
    for(i=0;i<3;++i)
    {
        pid_t pid = fork();
        
        if(pid == 0)
        {
            break;
        } 
    }

    sleep(i);
    if(i < 3)
    {
        printf("I am child:%d my parent:%d\n",getpid(),getppid());
    }
    else
    {
        printf("I am parent:%d\n",getpid());
    }

    return 0;
}

如果不让 pid==0及时的break,那么子进程和父进程在下一次循环都会frok..那么创建出来的进程数目 2^n

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

 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将数据写入,返回实际写入到字节数

管道的优缺点

        优点:简单

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

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

有名管道FIFO

FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于"有血缘关系"的进程间。但通过FIFO不相关的进程也能交换数据。

FIFO是Linux基础文件类型中的一种。但,FIFO文件磁盘上没有数据块,仅仅用来标识内核中的一条通道。实际上是在读写内存通道,这样就是实现了进程间通信。

有名管道使用注意事项

一个为只读而打开一个管道的进程会阻塞,直到另一个进程为只写打开管道

一个为只写而打开一个管道的进程会阻塞,直到另一个进程为只读打开管道

创建方式

命令:mkfifo  管道名

库函数:int mkfifo(const char* pathname,mode_t mode);

使用:

        像普通文件一样使用就可以

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
using namespace std;

int main(void)
{

    //以只读的方式打开fifo
    int fd = open("myfifo",O_RDONLY);
    if(fd == -1)
    {
        perror("open");
        exit(-1);
    }

    //不断的读数据
    char buf[256];
    while(1)
    {
        if(read(fd,buf,sizeof(buf)) == 0)
        {
            break; 
        }
        cout<<buf<<endl;
    }

    return 0;
}

//=====================================================

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

using namespace std;

int main(void)
{
    //先创建一个fifo
    int ret = mkfifo("myfifo",0664);

    //以只写的方式打开
    int fd = open("myfifo",O_WRONLY);
    if(fd == -1)
    {
        perror("open");
        exit(-1);
    }

    //向管道中写入数据
    char buf[256];
    strcpy(buf,"我是你爹...");
    while(1)
    {
        sleep(5);
        write(fd,buf,strlen(buf));
    }

    return 0;
}

文件实现IPC

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

using namespace std;

int main(void)
{

    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork");
        exit(-1);
    }
    if(pid > 0)
    {
        int fd = open("a.txt",O_WRONLY|O_CREAT,0777);
        char buf[256];
        strcpy(buf,"草泥马...故事开始在哪个梦中...");
        write(fd,buf,strlen(buf));
    }
    else if(pid == 0)
    {
        int fd = open("a.txt",O_RDONLY);
        sleep(2);
        char buf[256];
        while(1)
        {
            int ret = read(fd,buf,sizeof(buf));  
            if(ret == 0)
            {
                break;
            }
            cout<<buf<<endl;
        }
          
    }
    return 0;
}

内存映射(存储映射IO) 

        内存映射是将磁盘文件的数据映射到内存,用户通过修改内存就能够修改磁盘文件。

使用这种方法,首先应通知内核,将一个指定文件映射到存储区中。这个映射工作可以通过mmap函数实现。

        

 mmap函数

        void  *mmap(void *adrr,size_t length,int prot,int flags,int fd,off_t offset);

        成功返回创建的映射区首地址,失败返回 MAP_FAILED宏

        adrr:建立映射区的首地址,传NULL

        length:欲创建映射区的大小

        prot:映射区权限(PROT_READ PROT_WRITE PORT_READ|PORT_WRITE)

        flags:标志位参数

                MAP_SHARED:会将映射区所做的操作反映到物理设备上,映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项

                MAP_PRTVATE:不同步

        fd:用来建立映射区的文件描述符

        offset:映射文件的偏移量(4K的整数倍)

munmap函数

        释放建立的映射区

        int munmap(void **addr,size_t length);

        成功0  失败-1

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>
using namespace std;

int main(void)
{

    int fd = open("a.txt",O_RDWR|O_CREAT,0777);
    if(fd == -1)
    {
        perror("open");
        exit(-1);
    }

    

    ftruncate(fd, 100);


    char *p = (char *)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(p == MAP_FAILED)
    {
        perror("mmap");
        exit(-2);
    }

    strcpy(p,"aaa");

    munmap(p,4);


    return 0;
}

使用mmap注意事项

1.创建映射区的过程中,隐含着一次对映射文件的读操作

2.当MAP_SHARED时,要求文件具有打开去权限

3.映射区的释放与文件关闭无关。只要映射区建立成功,文件可以随时关闭

    因为我们是对映射区操作就可以了

4.当文件大小为0时,不能创建映射区。否则容易出现总线错误

5.munmap传入的地址一定是首地址

6.文件偏移量必须为4K的整数倍

7.mmap创建映射区的出错概率很高,一定要检查返回值

mmap父子进程通信

        父子等有血缘关系的进程之间可以通过mmap建立映射区来完成数据通信。fork之后父子进程间的mmap映射区是共享的。

        1.打开文件

        2.mmap建立的映射区(但必须要使用NAP_SHARED)

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>


using namespace std;

int main(void)
{

    //打开一个文件
    int fd = open("a.txt",O_RDWR);

    //父进程创建一个内存映射
    char *p = (char *)mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(p == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }
    //可以及时关闭这个文件了
    close(fd);

    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork");
        exit(-1);
    }
    if(pid > 0)
    {
        sleep(2);
        //父读子写(这个无所谓哈)
        cout<<p<<endl;
        wait(NULL);
        munmap(p,100);

    }
    else if(pid == 0)
    {
        
       strcpy(p,"aaaa");
    }

    if(pid > 0)
        munmap(p,4);

    return 0;
}

  不知道你们能不能体会到这种感觉,其实内存映射很简单。及时通过文件进程通信而已,只不过这样操作会更快更方便而言,本质上与文件实现IPC一样。

   仔细一想,这样带来的函数是什么呢?每一个对比就没用伤害,频繁的在用户态和内核态切换还是蛮浪费时间的。通过文件来通信,减少了切换的频率,而且我们可以看到我们是在用户空间中建立的内存空闲区的。

    通过映射,底层应该是做了值的拷贝。我们只需要操作内存区看可以实现父子之间进行通信。就像操作数组一样方便。其根基是因为fork之后父子进程的内存映射区是一样的。

匿名映射
       
我们看内存映射,还得先打开一个文件,以文件描述符为桥梁来操作,是不是感觉太麻烦了。有没有一种方式不用打开文件就可以操作呢?通过匿名映射

        使用MAP_ANONYMOUS(或MAP_ANON)

        mmap(NULL,4,PROT_READ|PORT_WRITE,MAP_SHARED|MAP_ANON,-1,0);

#include <iostream>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
using namespace std;

int main(void)
{

    //创建匿名映射区
    char *p = (char *)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
    if(p == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }

    //创建子进程
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork");
        exit(-2);
    }

    if(pid > 0)
    {
        sleep(2);
        cout<<p<<endl;
        wait(NULL);
        munmap(p,4);
    }
    else if(pid == 0)
    {
        strcpy(p,"aaa");
    }



    return 0;
}

这里做一个小小的总结:

1.有关系的进程(父子进程)

        -还没有子进程的时候

                父进程先创建内存映射区

        -创建子进程

        -父子进程共享创建的内存映射区

2.没有关系的进程件通信(通过映射同一块磁盘文件)

        -准备一个IO文件(大小不能为0)

        -进程1:通过磁盘文件创建内存映射区

                      得到内存映射区首地址

        -进程2:通过磁盘文件创建内存映射区

                       得到内存映射区首地址

        -使用内存映射区通信

读端:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <string>
#include <unistd.h>
using namespace std;

#define PATHNAME "./a.txt"

int main(void)
{

    int fd = open(PATHNAME,O_RDONLY);
    if(fd == -1)
    {
        perror("open err");
        exit(-1);
    }

    char *addr = (char *)mmap(NULL,100,PROT_READ,MAP_SHARED,fd,0);
    if(addr == MAP_FAILED)
    {
        perror("mmap err");
        exit(-1);
    }

    while(1)
    {
        sleep(3);
        cout<<addr<<endl;
    }
    
   
   

    munmap(addr,100);


    return 0;
}

写端:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <string>
#include <sys/types.h>
#include <unistd.h>
using namespace std;

#define PATHNAME "./a.txt"

int main(void)
{

    int fd = open(PATHNAME,O_RDWR|O_CREAT,0777);
    if(fd == -1)
    {
        perror("open err");
        exit(-1);
    }
    truncate(PATHNAME,100);
    char *addr = (char *)mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(addr == MAP_FAILED)
    {
        perror("mmap err");
        exit(-1);
    }

   for(int i=0;i<100;i++)
   {    
        strcpy(addr,"aaa");
        sleep(1);
   }


    munmap(addr,100);


    return 0;
}

共享内存

基本概念

1.共享内存允许两个或多个进程共享物理内存同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间一部分,因此这种PIC机制无需内核介入。所需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他进程共享同一个段的进程可用。

2.与管道等要求发送进程将数据从用户空间的缓冲区复制进内核和接受进程数据从内核内存复制进用户空间的缓冲区相比,这种IPC技术速度更快。

  害,我都懒得说了。这个不就跟前面一样嘛,我们之前说过所以进程的内核区共享一块物理内存,管道是不是就是在内核去建立一个缓冲区,那不是跟这个原理一样嘛。所以我常说一定要把原理搞清楚。

数据实际上是写在物理内存中的,之前说虚拟地址空间,虚拟地址空间只需要实现数据结构上的页目和页表就可以了。它是方便我们操作的,更方便多进程的实现。

使用步骤(5步,分别调用5个函数)

1.调用shmget()创建一个新共享内存段或者取得一个已经存在的共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中用到的共享内存标识符。

2.使用shmat()来附上共享内存段,即使用该段成为调用进程的虚拟地址内存的一部分

3.为了引用这块共享内存,程序需要使用由shmat()调用返回addr值,它指向进程的虚拟地址空间中该共享内存段的起点。

4.调用shmdt()来分离共享内存

5.调用shmctl()来删除共享内存段。只有当当前所有附加内存段的进程的与值分离之后内存段才会销毁。只有一个进程需要执行这一步

int       shmget(key_t key,size_t size,int shmflg);

void *  shmat(int shmid,const void* shmaddr,int shmflg);

int       shmdt(const void *shmaddr);

int       shmctl(int shmid,int cmd,struct shmid_ds *buf);

key_t  fork(const char*pathname,int proj_id);

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

int  shmget(key_t key,size_t size,int shmflg);

==功能:创建一个新的共享内存段或者获取一个既有的共享内存段的标识,新创建的内存段中的数据都会被初始化为0

==参数:

        key:       key_t类型是一个整型,通过这个找到或者创建一个共享内存。

                        一般以16进制表示,非0值

        size:      共享内存的大小(页的整数倍)

        shmflg:  属性

                        访问权限

                        附加属性:创建/判断共享内存是不是存在(IPC_EXCL)

                                          创建:IPC_CREAT

                                          判断共享内存是否存在:IPC_EXCL|IPC_CREAT|0664

==返回值

        失败 -1    并设置 error

        成功 >0   返回共享内存的引用的ID,后面操作共享内存都是通过这个值

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

int *shmat(int shmid,const void *shmaddr,int shmflg);

功能:和当前的进程进行关联

参数:

           shmid:共享内存的标识符(ID),由shmget返回值获取

           shmaddr:申请的共享内存的起始地址,指定NULL,内存指定

           shmflg:对共享内存的操作

                        读:SHM_RDONLY

                        读写:0

返回值:

        成功:返回共享内存的首地址,失败(void *)-1

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

int shmdt(const void* shmaddr);

功能:解除当前进程和共享内存的关联

参数:

           shmaddr   共享内存的首地址

返回值:

            成功 0,失败 -1

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

int shmctl(int shmid,int cmd,struct shmid_ds *buf);

功能:删除共享内存,共享内存要删除才会消失

           创建共享内存的进程被销毁了对共享内存没用影响

参数:

           shmid:共享内存的ID

           cmd:   要做的操作

                      IPC_STAT  获取共享内存当前的状态

                      IPC_SET    设置共享内存的状态

                      IPC_RMAD  标记共享内存需要被销毁

           buf: 需要设置或者获取的共享内存的属性信息

                      IPC_STAT   buf存储数据

                      IPC_SET     buf中需要初始化数据

                      IPC_RMID  没有用,NULL

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

key_t fork(const char* pathname,int proj_id);

功能:根据指定路径名和int,生成一个共享内存的key

参数:

        -pathname:指定一个存在的路径

        -proj_id:int类型的值,但是系统调用只会使用其中一个1字节   (范围0~255)

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

案例:

server端读数据

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>

using namespace std;

#define PATHNAME "./"
#define PROJ_ID 0x666
#define SIZE    4096

int main(void)
{

    //生成一个共享内存 key
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key == -1)
    {
        perror("fork err");
        exit(-1);
    }

    //创建一个共享内存,如果存在则获取。拿到id
    int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0664);
    if(shmid == -1)
    {
        perror("shmget err:");
        exit(-1);
    }
    cout<<"shmid:"<<shmid<<endl;
    //和当前进程进行关联,读写操作
    char *addr = (char *)shmat(shmid,NULL,0);
    //进行数据交换
    while(1)
    {
        sleep(1);
        cout<<addr<<endl;
    }
    //分离当前进程
    shmdt(addr);
    //删除共享内存段
    int ret = shmctl(shmid,IPC_RMID,NULL);
    if(ret == -1)
    {
        perror("shmctl err");
        exit(-1);
    }

    return 0;
}

如果出现如下错误: 这个id已经被占用,移除就可

终端输入:

ipcs -m

ipcrm -m 对应的shmid

然后再./server

client端写数据

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
using namespace std;

#define PATHNAME "./"
#define PROJ_ID 0x666
#define SIZE    4096

int main(void)
{

    //先获取key
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key == -1)
    {
        perror("ftok err");
        exit(-1);
    }
    //获取共享内存块的id
    int shmid = shmget(key,SIZE,0);
    if(shmid == -1)
    {
        perror("shmget err");
        exit(-1);
    }
    //和的当前进程进行关联
    char *addr = (char *)shmat(shmid,NULL,0);
    //进行数据交换
    for(int i=0;i<10;++i)
    {
        strcpy(addr,"aaaaa");
    }
    //分离
    shmdt(addr);

    return 0;
}

本来想把信号也放到这里,信号要是进程间通信的一种方式嘛...

感觉篇幅有点长了,我另写一篇文章

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐