linux:命名管道与共享内存

1.命名管道

匿名管道,只能有血缘关系的进程进行通信,没有对应的实体文件,在进程之中,也就是在内核空间 中的一个内存缓冲区, 不会在任何目录下显示。而命名管道以一个特殊的管道文件(类型为p)存在于文件系统中,但只用了它的"名字空间"和"元数据管理功能",而数据的实际存储完全不用文件系统。但文件大小始终显示为0。

我们可以使用mkfifo 创建一个 p 属性的命名管道文件,有文件名和文件目录,就可以找到这个文件,我这里创建一个名字为 fifo 的文件

bash 复制代码
mkfifo fifo

一个终端输入:

另一个终端输入:

这样第一个终端就会结束:

echo 和 cat 通过命名管道fifo 进行了通信,

该管道文件大小一直时0。

它实际上是创建符号,在被打开进行数据通信的时候,所以它的内容也不会向磁盘刷新。

如何做到两个进程看到同一份资源:

  • 宏观:不同路径为什么可以看到同一份资源,因为路径是唯一的,所以使用路径+文件名,然后inode相同来确定同一份资源。分别以读写方式打开,
  • 微观:操作系统同一个文件数据不会加载两次,即属性和缓冲区(内容)是不用再加载的, struct file 需要再次创建,因为读写位置可能不同。普通文件也是这样的原理,只不过管道文件不需要向磁盘刷新,跟匿名管道也是相同原理,相同的缓冲区,但是会创建不同的struct file

命名管道也是管道,也是是符合单向通信的。

要需要不相关的管道之间进行通信,需要两个可执行程序,但他们实际还是 shell 的子进程,兄弟关系,但我们不使用。

这里我们实现两个进程,client写,server读取。makefile 一次只能形成一个可执行,我们已这种方式设计makefile :

bash 复制代码
.PHONY:all
all:server client
client:client.cc
	g++ -o $@ $^ -std=c++11

server:server.cc
	g++ -o $@  $^ -std=c++11

.PHONY:clean
clean:
	rm -f server client fifo

Linux一切皆文件,命名管道也是文件,实现两个进程的通信和文件操作是相似的,即

  1. 打开文件
  2. 操作
  3. 关闭文件

在进行通信之前我们先使用mkfifo 创建好命名管道文件,对于两个进程谁创建文件都可以,然后需要都打开文件,只有一端打开会阻塞,只有两个进程都打开命名管道里各自的程序才会继续执行。

**server.cc:**实现读操作

cpp 复制代码
#define FILENAME "fifo" //.隐藏文件,不想显示出来

int Mkfifo()
{
    int n = mkfifo(FILENAME,0666);//不写路径,直接使用当前路径
    if(n == -1)
    {
        std::cerr << "errno:" << errno << ",errstring:" << strerror(errno)<<std::endl;
        return 0;
    }
    std::cout<<"mkfifo success... "<<std::endl;
    return 1;
}

int main()
{
    //只有一端打开这里会阻塞,需要读写都打开

Start:
    //文件操作读方式 打开文件
    int rfd = open(FILENAME,O_RDONLY);//只读
    if(rfd == -1)
    {
        std::cerr << "errno:" << errno << ",errstring:" << strerror(errno)<<std::endl;
        if(Mkfifo()) goto Start;//创建好管道,还需要打开
        else return 1;
    }
    std::cout<<"open fifo success... read"<<std::endl;

    char buffer[2048];
    while (true)
    {
        ssize_t s = read(rfd, buffer, sizeof(buffer) - 1); // 写入的时候要求不写入\0, -1为了防止越界,最后一位填上\0
        if (s > 0)
        {
            buffer[s] = 0; // 表示是字符串
            std::cout << "client say# " << buffer << endl;
        }
        else if(s == 0)//写端关闭,读端也关闭
        {
            std::cout<< "client quit,server quit too!"<<endl;
            break;
        }
    }

    close(rfd);
    std::cout<<"close fifo success...read"<<std::endl;
    return 0;
}

client.cc实现写操作

cpp 复制代码
#define FILENAME "fifo" //.隐藏文件,不想显示出来

int main()
{
    //打开文件
    int wfd = open(FILENAME,O_WRONLY);
    if(wfd == -1)
    {
        std::cerr << "errno:" << errno << ",errstring:" << strerror(errno)<<std::endl;
        return 1;
    }
    std::cout<<"open fifo success...write"<<std::endl;

    //写
    std::string message;
    while(true)
    {
        std::cout<<"Please Enter# ";
        std::getline(std::cin,message);
        ssize_t s = write(wfd,message.c_str(),message.size());

        if(s == -1)
        {
            std::cerr << "errno:" << errno << ",errstring:" << strerror(errno)<<std::endl;
            break;
        }
    }

    close(wfd);
    std::cout<<"close fifo success...write"<<std::endl;

    return 0;
}

2.System V IPC

System V 就是 Linux 里一套 古老、经典、跨进程通信的 IPC 标准。 管道通信是复用了系统文件代码,不属于system v,system v是一个单独模块,用来专门负责进程间通信。只有匿名管道仅支持父子进程之间的通信,而其他通信方式都支持非父子关系的通信。

2.1 shm 原理

进程间通信的原理,必须让不同的进程看到同一份资源(资源由操作系统提供),管道中打开文件使用的文件缓存区就是操作系统提供的。

管道文件属于内核数据结构,在内核空间,访问需要调用系统调用。

操作系统一定会允许系统中同时存在多个共享内存,操作系统为了管理要创建一个struct shm 包含各种属性,比如谁创建的,什么时候创建。对共享内存的管理就变成了对所有共享内存数据结构对象的增删查改。共享内存,也要被操作系统管理 :先描述,后组织。

如何保证第二个之后参入共享内存通信的进程,看到的就是同一个共享内存呢?就要求共享内存必须有唯一的标识,如何做到并给另一个进程呢?

2.2 认识系统接口

shmget返回共享内存的shmid:

key 通信双方约定的数字,标识一份共享内存,保证看到同一份共享内存,不建议手动设置,因为可能与系统中的其他共享内存起冲突,我们使用一个**frok()**算法函数生成,可以降低冲突概率

proj_id 就是自己捏造的数字了。通信双方使用同一个pathnameproj_id 就可以形成相同 key,就可以找到同样的共享内存。

当一个进程使用 shmgetkey 在物理内存中创建一份空间后,另外的通信进程就可以通过相同的key找到这份空间,实现共享内存。

通过 shmflg 选项,shmget 既能创建,也可以获取,IPC_CREAT 如果不存在就创建,存在就获取并返回。IPC_EXCL 不单独使用,单独使用没有任何意义,一般要和 IPC_CREAT 按位或。(IPC_EXCL | IPC_CREATshm不存在就创建,存在就出错返回,可以保证创建的共享内存是全新的。

shmget成功返回标识符,失败返回-1

cpp 复制代码
key_t GetKey()
{
    key_t key = ftok(pathname.c_str(),proj_id);
    if(key < 0)//失败返回-1,路径不存在,路径无访问权限,会返回失败
    {
        std::cerr << "errno:" << errno << ",errstring:" <<strerror(errno) <<std::endl;
        exit(1);
    }
    return key;
}
const int size = 4096
int main()
{
    int shmid = shmget(key,size,IPC_CREAT|IPC_EXCL);
    if(shmid < 0)
    {
        std::cerr << "errno:" << errno << ",errstring:" <<strerror(errno) <<std::endl;
        exit(1);
    }
    return shmid;
}

结果可以看到,第一次是创建,后面因为共享内存存在出错返回,使用ipcs -m可以查看共享内存的状态,perms是权限,nattch是与它挂接的进程数量。还可以发现进程退出后共享内存还存在,与文件不同,必须用户自己主动释放。

可以使用**-ipcs -m shmid** 释放。key vs shmid。key:是共享内存的属性,不要在应用层使用,只用来在内核中表示共享内存的唯一性。shmid:应用这个共享内存的时候,我们使用shmid来进行操作共享内存。 key 偏底层像 fd,shmid 像 FILE*,我们使用时一般应用使用 shmid。

shmflg 也可以与权限直接按位或,就可以改变perms

cpp 复制代码
int shmid = shmget(key, size, IPC_CREAT|IPC_ECL|0644);

shmat:at->attach

把进程挂接到共享内存,shmid 表示要挂接的共享内存 id ,shmaddr 一般为 nullptr , 它的作用是将共享内存挂接到进程指定的虚拟地址中,而我们对虚拟地址空间的使用情况又不了解,所以一般不使用手动,让操作系统操作。shmflg代表共享内存挂接到共享内存时采用的读写方式,因为我们创建共享内存空间时已经设置过权限,所以直接设为默认0。成功返回进程虚拟地址空间挂接的其实地址,失败返回 (void*) -1 。

cpp 复制代码
char* s = (char*)shmat(shmid,nullptr,0);

当进程退出时,共享内存的nattch就会减一。

shmdt:

如何在进程不退出的情况下去掉与共享内存的关联呢?使用 shmdt函数,也就是取消页表的映射关系。dt->detach

cpp 复制代码
shmdt(s);

shmctl:

如何在进程中清除共享内存,使用 shmctl控制,可以实现改和查找

第一个操作表示要执行的操作,IPC_RMID表示删除,RM->remove ID->immidiately 或 shmid。

shmid_ds 可以存放很多共享内存的属性,它其实就是共享内存结构体的一个子集,专门提供给上层用户读取和修改的数据类型,我们不修改它的内容直接写nullptr就行。

cpp 复制代码
shmctl(shmid,IPC_RMID,nullptr);

为什么不让操作系统来帮我们来形成 key,而让用户层形成再到内核中使用?

在操作系统中,唯一值key只有操作系统和创建该共享内存的进程知道,想获取该共享内存的进程是不知道的,所以我们通过用户层让想获取共享内存的一方知道

不同进程通过挂接相同的共享内存可以看到相同的资源。挂载之后就可以进行通信了,直接读取和写入cout,cin不用像管道使用 read 和 write 了,也不需要等代对方。这里我的系统内核进行了初始化,如果没有也可以根据申请时共享内存的大小初始化。

cpp 复制代码
//server 读取---------
    while(true)
    {
        //直接读取
        std::cout << "共享内存的内容:"<< s <<std::endl;
        sleep(1);
    }

//client 写入-------------

    char c = 'a';
    for(;c<='z';c++)
    {
        s[c-'a'] = c;//写入
        sleep(5);
        std::cout << "write:" << c <<" done"<<std::endl;
        sleep(6);
    }

读端读了很多次,可以说明共享内存是没有任何同步机制的。共享内存是需要同步的,我们可以通过创建一个命名管道。

cpp 复制代码
//server 读取---------
    int fd = open(filename.c_str(),O_RDONLY);//双方都打开才会继续进行

    //TODO
    while (true)
    {
        int code = 0;
        ssize_t n = read(fd, &code, sizeof(code));
        if (n > 0)//通过命名管道收到指令后再读取
        // 直接读取
        {
            std::cout << "共享内存的内容:" << s << std::endl;
            sleep(1);
        }
        else if (s == 0)
        {
            break;
        }
    }

//client 写入-------------

    int fd = open(filename.c_str(),O_WRONLY);//双方都等对方打开

    char c = 'a';
    for(;c<='z';c++)
    { 
        s[c-'a'] = c;//写入
        sleep(5);
        std::cout << "write:" << c <<" done"<<std::endl;
        
        int code = 1;//发送的指令,不重要
        write(fd,&code,sizeof(code));
        sleep(6);
    }

所以说共享内存是直接裸露给所有的使用者的,一定要注意共享内存的使用安全问题。我们每次都是拷贝全部内容,如何移除内容管理起来内,我们可以在这个空间约定一些管理数据就行。

共享内存的特点:

  1. 不提供同步机制,共享内存提供给所有的使用者,一定要注意共享内存的使用安全问题
  2. 共享内存是所有进层间通信,速度最快的。(管道通信要通过文件操作,系统调用,将数据从自己进程空间的数据拷贝到管道(内核)再拷贝到要通信的进程空间。但凡是数据迁移都是拷贝!)例:键盘->A进程->管道->B进程->显示器 ,共享内存:键盘->共享内存->显示器 ,通过共享内存可以不用再通过用户空间。
  3. 共享内存可以提供较大的空间

2.3 编写代码

unlink也可以删掉一个文件。读端代码:通过 Init 类自动实现共享内存的创建和清除

cpp 复制代码
class Init
{
public:
    Init()
    {
        bool r = MakeFifo();
        if(!r) return ;

        //创建共享内存
        key_t key = GetKey();
        cout << "key:" << ToHex(key) <<endl;
        int shmid = CreateShm(key);
        cout<<"shmid:"<<shmid<<endl;
        sleep(5);
        std::cout << "开始将shm映射到进程的地址空间中"<<std::endl;
        s = (char*)shmat(shmid,nullptr,0);

        fd = open(filename.c_str(),O_RDONLY);//双方都打开才会继续进行
    }
    ~Init()
    {
        //去除挂接
        sleep(5);
        shmdt(s);
        std::cout << "开始将shm从进程虚拟地址空间中移除" << endl;
        
        //删除共享内存
        sleep(5);
        shmctl(shmid,IPC_RMID,nullptr);
        std::cout << "开始将shm从操纵系统物理内存中移除" << endl;

        close(fd);//关闭文件
        unlink(filename.c_str());//把文件删除
    }

    int shmid;
    int fd;
    char *s;
};


int main()
{
    Init init;
    //TODO
    while (true)
    {
        int code = 0;
        ssize_t n = read(init.fd, &code, sizeof(code));
        if (n > 0)//通过命名管道收到指令后再读取
        // 直接读取
        {
            std::cout << "共享内存的内容:" << init.s << std::endl;
            sleep(1);
        }
        else if (n == 0)
        {
            break;
        }
    }

    return 0;
}

写端代码:

cpp 复制代码
int main()
{
    key_t key = GetKey();
    int shmid = GetShm(key);//约定相同的key,与proj_id,获取相同的shmid

    char* s = (char*)shmat(shmid,nullptr,0);//挂接到自己进程地址空间
    std::cout << "attach shm done" << std::endl;
    sleep(5);
    
    int fd = open(filename.c_str(),O_WRONLY);//双方都等对方打开

    char c = 'a';
    for(;c<='z';c++)
    { 
        s[c-'a'] = c;//写入
        std::cout << "write:" << c <<" done"<<std::endl;
        
        int code = 1;//发送的指令,不重要
        write(fd,&code,sizeof(code));
        sleep(1);
    }

    shmdt(s);
    std::cout << "detach shm done" << std::endl;
    close(fd);//写端关闭,读端自动关闭

    return 0;

调用的函数:

cpp 复制代码
const std::string pathname="/home/wdz/linux_-tencent/26/test/5/15/shm";
const std::string filename = "fifo";
//但用 . 有一个大坑如果你在不同目录运行程序,生成的 key 会不一样!
const int proj_id = 0x11223344;

const int size = 4096;//共享内存的大小强烈建议设置成n*4096  ,因为底层是以4096 开辟的,
// 

key_t GetKey()
{
    key_t key = ftok(pathname.c_str(),proj_id);
    if(key < 0)//失败返回-1,路径不存在,路径无访问权限,会返回失败
    {
        std::cerr << "errno:" << errno << ",errstring:" <<strerror(errno) <<std::endl;
        exit(1);
    }
    return key;
}

std::string ToHex(int id)
{
    char buffer[1024];
    snprintf(buffer,sizeof(buffer),"0x%x",id);
    return buffer;
}

int CreateShmHeler(key_t key, int flag)
{
    int shmid = shmget(key,size,flag);
    if(shmid < 0)
    {
        std::cerr << "errno:" << errno << ",errstring:" <<strerror(errno) <<std::endl;
        exit(1);
    }
    return shmid;
}

int CreateShm(key_t key)
{
    return CreateShmHeler(key,IPC_CREAT|IPC_EXCL|0644);
}

int GetShm(key_t key)//获取共享内存也是通过key
{
    return CreateShmHeler(key,IPC_CREAT/*0也可以*/);
}

int MakeFifo()
{
    int n = mkfifo(filename.c_str(),0666);//不写路径,直接使用当前路径然后加上文件名称
    if(n == -1)
    {
        std::cerr << "errno:" << errno << ",errstring:" << strerror(errno)<<std::endl;
        return 0;
    }
    std::cout<<"mkfifo success... "<<std::endl;
    return 1;
}

通过上面的内容我们可以知道共享内存是内核中用数据结构管理起来的一块内存,内核中存在很多,那我们如何查看数据结构的内容呢

stmctl函数:

功能:用于控制共享内存

原型

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

参数

shmid:由shmget返回的共享内存标识码

cmd:将要采取的动作(有三个可取值)

buf:指向一个保存着共享内存的模式状态和访问权限的数据结构

返回值:成功返回0;失败返回-1

|----------|----------------------------------------------|
| 命令 | 说明 |
| TPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
| IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值 |
| IPC_RMID | 删除共享内存段 |

可以执行下面代码验证

cpp 复制代码
int main()
{
    Init init;
    struct shmid_ds ds;
    shmctl(init.shmid,IPC_STAT,&ds);
    std::cout << ToHex(ds.shm_perm.__key) << std::endl;
    std::cout << ds.shm_segsz <<std::endl;//大小
    std::cout << ds.shm_nattch <<std::endl;//连接值
}

能获取属性意味着操作系统帮我们维护了属性,shmget 通过 key 值进行查找,返回 shmid。

2.4 消息队列msg(了解)

  • 消息队列,提供一个进程给另一个进程发送数据块的能力,之前通信没有块的概念,想发多少发多少
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
  • 特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

获取消息队列,msgflg 选项与shmflg 相同,key 的含义也相同,要同时被通信双方知道。它的表现更像链表,程序员自己定义msgbuf ,数据放mtext,数据是 A 发的还是 B 发的可以通过 mtype 识别。msgsnd用来发数据,msgrvc用来接收数据,msgp是接收缓冲区地址,msgsz接收缓冲区大小,msgtype是判断要接受的对象。消息队列可以通过ipcs -q 看见的。

消息队列,系统中可以同时存在多个消息队列,消息队列也要在内核中把他管理起来:先描述再组织 消息队列= 队列 + 队列的属性。

这个结构第一个字段与共享内存中相同

cpp 复制代码
int main()
{
    key_t key = GetKey();
    cout << key <<endl;
    //创建消息队列
    int msgid = msgget(key,IPC_CREAT|IPC_EXCL);
    cout << "msgid:"<<msgid<<endl;

    struct msqid_ds ds;
    msgctl(msgid,IPC_STAT,&ds);
    cout << ds.msg_perm.__key<<endl;//__key 是保留字段,不向用户空间公开!,读到的是0
    //自动删除
    msgctl(msgid,IPC_RMID,nullptr); 
}

2.5 信号量(sem)(了解)

信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥。

进程互斥:

  • 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
  • 在进程中涉及到互斥资源的程序段叫临界区
  • 特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

信号量本质就是计数器

semaphore信号量,通过下面 semget 函数创建,nsems 表示创建一个信号量集,里面有多个信号量。

使用ipcs -s 查看当前系统的信号量

semctlsemnum 如果要删除的话,代表要删除第几个信号量集合,下标从0开始。

cpp 复制代码
int main()
{
    key_t key = GetKey();
    cout << key <<endl;
    //创建
    int mesid = semget(key,3,IPC_CREAT|IPC_EXCL);//一个信号量集里有3个信号量
    cout << "mesid:" << mesid << endl;
    semctl(mesid,0,IPC_RMID);//IPC_RMID 是删除整个信号量集合,不是删单个!
}

操作系统也要管理多个信号量集合,semid_ds

通过上面三种通信方式,他们都是使用XXXid_id 组织起来管理的,第一个字段都是 ipc_perm ,但这些都是用户级别 的,也就是OS 给你暴露出来的。那么内核是如何看待 IPC 资源的呢?

  • IPC资源返回的 key 唯一,是操作系统中单独设计的模块
  • 系统如何管理起来的呢,这三种通信方式第一个字段类型相同可以使用柔性数组 ,第一个参数是要管理的通信模块数量,第二个参数使用指针指向他们共用的ipc_perm ,就可以同一管理和查找key ,如果要访问他们各自内部的信息,可以通过强制指针转换实现。

类似于C++中的多态,基类是kern_ipc_perm,派生类是不同的semid_ds,shmid_ds,seggid_ds。

信号量相关概念:

信号量的本质是一把计数器,多个执行看到的同一分资源,公共资源,会发生并发访问,数据不一致问题回归,需要保护,需要护持和同步。

  • 如何对公共资源保护,一种是用户保护,例如共享内存。 一种是OS管道,消息队列。
  • 互斥:任何一个时刻只允许一个执行流访问公共资源,加锁完成。
  • 同步:多个执行流执行时,按照一定的顺序执行。
  • 被保护起来的资源:临界资源。不被保护是非临界资源。
  • 访问该临界资源的代码,我们叫临界区。不访问的是非临界区。维护临界资源,其实就是在维护临界区

原子性:只有两态,要么不做,要么做完。

信号量:表示资源数目的计数器,每一个执行流向访问公共资源内的某一份资源,不应该让执行流直接访问,而是先申请信号量资源,其实就是先对信号量计数器进行减减操作,本质上,只要减减 成功,完成了对资源的预定机制。所以访问公共资源要先访问信号量,成功就继续访问资源,不成功,执行流被挂起等待。

int sem = 1 二元信号量 --> 互斥锁 --> 完成互斥功能

结论:

  1. 要使进程能够使用同一个临界资源,意味着每个进程都得先看到同一个信号量资源,那只能有OS提供了,可以通过IPC体系实现。
  2. 信号量本质也是公共资源,因为进程访问临界资源要先访问信号量。对信号量的操作只需要做减减和加加就行,是原子操作。申请是P操作,申请是V操作。所以信号量也有PV操作。System V 中通过 semop() 函数实现PV操作,是原子的。
  3. 不能用全局变量替代信号量,因为它的操作不是原子性的,也不能被所有进程同时看到。
  4. 单个信号量:struct sem{ int count; sask_struct* wait_queue;};申请成功count-- ,失败进程链接入等待队列

本篇结束 !

相关推荐
Snasph1 小时前
Linux 日志流水线深度解析:syslog() → journald → rsyslog → /var/log/syslog
linux·syslog·rsyslog
凡人叶枫1 小时前
Effective C++ 条款08:别让异常逃离析构函数
java·linux·数据库·c++·嵌入式开发
新时代牛马1 小时前
内核调试方法
linux·学习
herinspace1 小时前
管家婆财工贸软件中关于价格常见问题小结
服务器·网络·数据库·电脑·管家婆软件
MXsoft6181 小时前
**智慧校园运维实践:多校区、老旧设备的统一监控方案**
运维·自动化
Sean‘1 小时前
在隔离内网机器上使用 Filebeat 全量采集日志并推送到 ELK 的实战
运维·服务器·elk
Promise微笑2 小时前
精准微阻测量:微欧计的分类、场景应用与高效选型决策指南
大数据·运维·网络·人工智能
MageGojo2 小时前
R-Shell开源项目实战解析:用Rust打造命令行SSH工具,支持连接管理、远程执行、SFTP与MCP
运维·rust·开源项目·命令行工具·ssh客户端·mcp
云飞云共享云桌面2 小时前
非标设计工厂8-10个SolidWorks研发共享一台高性能工作站
运维·服务器·自动化·电脑·制造