花狗

导航

 

信号量

//用于初始化一个信号量,pshared参数指定信号量的类型,如果为0表示这个信号量是当前进程的局部信号量,
//否则该信号量就可以在多个进程之间共享,value参数指定信号量的初始值,注意不能初始化一个已经初始化的信号量,
//否则会导致不可预期的结果
int sem_init(sem_t*, int pshared, unsigned int value) //销毁信号量 int sem_destory(sem_t* sem); //将信号量的值减一,如果为0将sem_wait将被阻塞 int sem_wait(sem_t* sem); int sem_trywait(sem_t* sem); //将信号量加一 int sem_post(sem_t* sem);

条件变量

条件变量与信号量的区别

  • 使用条件变量可以一次唤醒所有等待者,而这个信号量没有的功能,感觉是最大区别。

  • 信号量是有一个值(状态的),而条件变量是没有的,没有地方记录唤醒(发送信号)过多少次,也没有地方记录唤醒线程(wait返回)过多少次。从实现上来说一个信号量可以是用mutex + counter + condition variable实现的。因为信号量有一个状态,如果想精准的同步,那么信号量可y能会有特殊的地方。信号量可以解决条件变量中存在的唤醒丢失问题。

  • 在Posix.1基本原理一文声称,有了互斥锁和条件变量还提供信号量的原因是:“本标准提供信号量的而主要目的是提供一种进程间同步的方式;这些进程可能共享也可能不共享内存区。互斥锁和条件变量是作为线程间的同步机制说明的;这些线程总是共享(某个)内存区。这两者都是已广泛使用了多年的同步方式。每组原语都特别适合于特定的问题”。尽管信号量的意图在于进程间同步,互斥锁和条件变量的意图在于线程间同步,但是信号量也可用于线程间,互斥锁和条件变量也可用于进程间。应当根据实际的情况进行决定。信号量最有用的场景是用以指明可用资源的数量

共享内存

共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量。

共享内存的通信原理

在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。 img

当两个进程通过页表将虚拟地址映射到物理地址时,在物理地址中有一块共同的内存区,即共享内存,这块内存可以被两个进程同时看到。这样当一个进程进行写操作,另一个进程读操作就可以实现进程间通信。但是,我们要确保一个进程在写的时候不能被读,因此我们使用信号量来实现同步与互斥。

对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。

共享内存的实现方法

以两个进程使用共享内存来通信为例: 1、调用API,让OS在物理内存上开辟出一大段缓存空间 2、让各自进程空间与开辟出的缓存空间建立映射关系

建立了映射关系后,每个进程都可以通过映射后的虚拟地址来共享操作实现通信了

共享内存存在的问题

当实现多个进程映射到同一片空间进行数据共享时,在写数据时就会出现互相干扰。

比如A进程写一半时切到B进程造成A的数据被打断。因为CPU都是不停的在不同进程之间切换运行,每个进程都有自己的时间片,本来A进程被打断后,等下次重新恢复运行时继续在自己内存上写数据,但是因为和B进程共享内存,导致在A打断期间运行B时也写入了数据,也是写入到与A的共享内存上,等恢复A运行时,其内存上已经写入了其他数据(B写入的)。这样就造成数据的错误。

这就需要加保护措施,这里先不对此详细叙述

共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。

 

  1. 共享内存使用流程
1. 向内核申请一块内存 -> 指定大小
2. 如果有两个进程, 需要通信, 可以使用这块共享内存来完成, 先创建出这两个进程
    - 进程A
    - 进程B
3. 进程A和进程B分别和共享内存进行关联
    - 拿到共享内存的地址 -> 首地址
4. 两个进程可以通过这个首地址对共享内存进行读/写操作
5. 如果这个进程不再使用这块共享内存, 需要和共享内存断开关联
    - 进程退出, 对共享内存是没有任何影响的
6. 当不再使用共享内存的时候, 需要将共享内存销毁
  1. 共享内存头文件
#include <sys/ipc.h>
#include <sys/shm.h>
  1. 共享内存操作函数
      • 创建或打开一块共享内存区

        // 创建共享内存
        // 共享内存已经存在, 打开共享内存
        // 可以创建多块共享内存
        int shmget(key_t key, size_t size, int shmflg);
            参数:
                - key: 通过这个key记录共享内存在内核中的位置, 需要是一个>0的整数, ==0不行
                    随便指定一个数就可以, 后边会介绍一个函数ftok
                - size: 创建共享内存的时候, 指定共享内存的大小
                    - 如果是打开一个已经存在的共享内存, size写0就可以
                - shmflg: 创建共享内存的时候使用, 类似于open函数的flag
                    - IPC_CREAT: 创建共享内存
                        - 创建的时候需要给共享内存一个操作权限
                            - IPC_CREAT | 0664
                    - IPC_CREAT | IPC_EXCL: 检测共享内存是否存在
                        - 如果存在函数返回-1
                        - 不存在, 返回0
            返回值:
                成功: 创建/打开成功, 得到一个整形数 -> 对应这块共享内存
                失败: -1
        // 应用
        // 1. 创建共享内存
        int shmid = shmget(100, 4096,  IPC_CREAT | 0664);
        int shmid = shmget(200, 4096,  IPC_CREAT | 0664);
        // 2. 打开共享内存
        int shmid = shmget(100, 0, 0);  
      • 将当前进程和共享内存关联到一起

        // 进程和共享内存产生关系
        void *shmat(int shmid, const void *shmaddr, int shmflg);
            参数:
                - shmid: 通过这个参数访问共享内存, shmget()函数的返回值
                - shmaddr: 指定共享内存在内核中的位置, 写NULL -> 委托内核区指定
                - shmflg: 关联成功之后对共享内存的操作权限
                    - SHM_RDONLY: 只读
                    - 0: 读写
            返回值:
                成功: 共享内存的地址 (起始地址)
                失败:  (void *) -1
        // 函数调用:
        void* ptr = shmat(shmid, NULL, 0);
        // 写内存
        memcpy(ptr, "xxxx", len);
        // 读内存
        printf("%s", (char*)prt);
      • 将共享内存和当前进程分离

        // 进程和共享内存分离 -> 二者就没有关系了
        int shmdt(const void *shmaddr);
            参数: 共享内存的起始地址, shmat()返回值
            返回值:
                - 成功: 0
                - 失败: -1
      • 共享内存操作 -( 删除共享内存 )

        // fcntl
        // setsockopt
        // getsockopt
        // 对共享内存进程操作
        int shmctl(int shmid, int cmd, struct shmid_ds *buf);
            参数: 
                - shmid: 通过这个参数访问共享内存, shmget()函数的返回值
                - cmd: 对共享内存的操作
                    - IPC_STAT: 获取共享内存的状态
                    - IPC_SET: 设置共享内存状态
                    - IPC_RMID: 标记共享内存要被销毁
                - buf: 为第二个参数服务的
                    cmd==IPC_STAT: 获取共享内存具体状态信息
                    cmd==IPC_SET: 自定义共享内存状态, 设置到内核的共享内存中
                    cmd==IPC_RMID: 这个参数没有用了, 指定为NULL
            返回值:
                成功: 0
                失败: -1
        // 删除共享内存
        shmctl(shmid, IPC_RMID, NULL);

shm和mmap的区别

    1. shm可以直接创建, 内存映射区创建的时候需要依赖磁盘文件

      • 内存映射区匿名映射不能进行无血缘关系的进程通信

    2. shm效率更高

      • shm直接对内存操作

      • mmap需要同步磁盘文件

    3. 内存共享

      • 所有的进程操作的是同一块内存 -> shm

      • 内存映射区操作:

        • 每个进程都会在自己的虚拟地址空间中有一块独立的内存

    4. 数据安全性

      • 进程突然退出

        • 共享内存还在

        • 内存映射区消失了

      • 运行进程的电脑突然挂了 -> 死机

        • 数据存储在共享内存中 -> 没有

        • 内存映射区中的数据 -> 还有

          • 内存映射区需要关联磁盘文件, 二者是同步的

    5. 生命周期

      • 内存映射区: 进程退出, 内存映射区销毁

      • 共享内存: 进程退出, 共享内存还在, 手动删除, 或者关机

  1. ftok 函数原型

    key_t ftok(const char *pathname, int proj_id);
        - 参数pathname: 对应某个存在的路径或路径中的对应的文件名 - 绝对路径
            - /
            - /home/robin/a/b/c
            - /usr/local/lib
            - /home/itcast/a.txt
        - 参数proj_id: 目前只使用了该变量占用的内存的一部分(1个字节)
            取值范围: 0 - 255
    key_t t = ftok("/home/", 'a');
    shmget(t, 0, 0);

MMAP

mmap内存映射的实现过程,总的来说可以分为三个阶段:

  • 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

    • 进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

    • 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址

    • 为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化

    • 将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中

  • 调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

    • 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。

    • 通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。

    • 内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。

    • 通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。

  • 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

    • 注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

    • 进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。

    • 缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。

    • 调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。

    • 之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

    • 修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

    • 总结:

      • 总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

      • 而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

      • 总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

 



posted on 2021-10-30 11:08  花狗  阅读(44)  评论(0)    收藏  举报