• Linux:进程间通信


    目录

    一、关于进程间通信

    二、管道

    pipe函数

    管道的特点

    匿名管道

    命名管道

    mkfifo

    三、system v共享内存

    shmget函数(创建)

    ftok函数(生成key)

    shmctl函数(删除)

    shmat/dt函数(挂接/去关联)

    四、初识信号量


    一、关于进程间通信

    首先我们都知道,进程运行是具有独立性的,所以两个进程想通信的话,难度较大

    进程间通信的本质:

    让不同的进程看到同一份资源(内存空间)

    这里的能看到的同一块内存,不能属于任何一个进程,而应该是共享的

    进程间通信的目的:

    为了交互数据、控制、通知等目的

    进程间通信的发展:

    管道

    system V进程间通信

    POSIX进程间通信

    进程间通信分类:

    管道:匿名管道pipe、命名管道

    system V IPC:system V 消息队列、共享内存、信号量

    POSIX IPC:消息队列、共享内存、信号量、互斥量、条件变量、读写锁


    二、管道

    管道是非常古老的进程间通信的形式

    管道是计算机通信领域的设计者设计得一种单向通信的方式

    从一个进程连接到另一个进程的一个数据流称为一个管道

    管道通信的背后是进程间通过管道进行通信的


    下面举个管道的例子,方便理解:

    pipe函数

    需要用到pipe创建管道,man查看pipe,pipe是系统调用接口

    需要的头文件是unistd.h

    作用是创建管道

    参数是pipefd[2],是输出型参数,希望通过调用它,得到被打开的文件fd

    返回值:创建成功返回0,失败返回-1

    代码如下:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. using namespace std;
    10. int main()
    11. {
    12. // 创建管道
    13. int pipefd[2] = {0};
    14. // 文件描述符0/1/2分别是stdin/stdout/stderr
    15. // 这里的pipefd[0]为3,表示读端
    16. // 这里的pipefd[1]为4,表示写端
    17. int n = pipe(pipefd);
    18. assert(n != -1);
    19. // debug下assert起作用,release下不起作用
    20. // 所以需要(void)n,表示n被使用过
    21. // 如果不(void)n,在release下会被认为n没有被使用
    22. (void)n;
    23. // 创建子进程
    24. pid_t id = fork();
    25. assert(id != -1);
    26. if (id == 0)
    27. {
    28. // 子进程 -> 读
    29. // 构建单向通信的信道,父进程写入,子进程读取
    30. // 关闭子进程不需要的fd,关闭写
    31. close(pipefd[1]);
    32. char buffer[1024]; // 缓冲区,用于读数据
    33. while (true) // 一直循环
    34. {
    35. // ssize_t类型是long int
    36. ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
    37. if (s > 0) // 读成功
    38. {
    39. // read是系统调用,所以读取完结尾不会有\0,自己加
    40. buffer[s] = 0;
    41. cout << "子进程[" << getpid() << "]获得父进程的message,"
    42. << buffer << endl;
    43. }
    44. }
    45. // 最后关闭读
    46. close(pipefd[0]);
    47. exit(0);
    48. }
    49. // 父进程 -> 写
    50. // 构建单向通信的信道,父进程写入,子进程读取
    51. // 关闭父进程不需要的fd,关闭读
    52. close(pipefd[0]);
    53. int count = 0; // 发送消息的条数
    54. string message = "父进程正在发送消息";
    55. char send_buffer[1024];
    56. while (true)
    57. {
    58. // 构建变化的字符串
    59. // 往send_buffer中写入
    60. snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",
    61. message.c_str(), getpid(), count++);
    62. // 写入
    63. write(pipefd[1], send_buffer, strlen(send_buffer));
    64. // 每次sleep方便观察
    65. sleep(1);
    66. }
    67. pid_t ret = waitpid(id, nullptr, 0);
    68. assert(ret > 0);
    69. (void)ret;
    70. // 最后关闭写
    71. close(pipefd[1]);
    72. return 0;
    73. }

    观察运行结果:

    通过结果可以发现,父进程每次写入后,子进程都能读取到,以管道的方式实现了进程间通信


    管道的特点

    1、管道是用来进行具有血缘关系的进程进行进程间通信的,常用于父子进程

    2、管道通过让进程间协同,提供了访问控制

    访问控制就是指:如果父进程写的慢,例如上述代码,每隔一秒写一次,子进程即使没有sleep,也只能每隔一秒再读;而如果写的很快,当把缓冲区写满了,在读取前也就不能再继续写了

    3、管道提供的是面向流式的通信服务(面向字节流,需要定制协议进行数据区分,后面博客会说到)

    流式服务:如果写的很快,读的很慢,读的时候一次就可以读一批消息

    4、管道是基于文件的,文件的生命周期是随进程的,所以管道的生命周期也是随进程的

    写入的一方的fd如果没有关闭,读取的一方有数据就读,没有数据就等

    写入的一方的fd如果关闭了,读取的一方read返回0,表示读到了文件的结尾

    5、管道是单向通信的,就是半双工通信的一种特殊情况

    半双工通信就是指两个人通信,我发你就不能发,你发我就不能发

    下面总结四种情况:

    ①写端快,读端慢,写满就不能再写了

    ②写端慢,读端快,管道没有数据时,读端必须等待

    ③写端关闭fd,读端read返回0,表示读到了文件结尾

    ④读端关闭fd,写端继续写,OS会自动终止写端

    前两种也就是上面提到的访问控制


    匿名管道

    下面引入一个例子了解匿名管道的使用,一个父进程有5个子进程,父进程与每一个子进程都建立对应的管道,每个子进程内部都有处理任务的方法,如果用户给了一个任务,父进程可以给子进程派发该任务,让子进程完成该任务,也就是实现一个小型的进程池

    首先实现Makefile:

    1. ProcPool:ProcPool.cc
    2. g++ -o $@ $^ -std=c++11
    3. .PHONY:clean
    4. clean:
    5. rm -f ProcPool

    ProcPool.cc(.cc和.cpp一样)代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include "task.hpp"
    10. using namespace std;
    11. #define PROCESS_NUM 5
    12. int waitcommand(int waitfd, bool& q)
    13. {
    14. //uint32_t是4字节
    15. uint32_t command = 0;
    16. //从文件描述符waitfd中读取,读取的内容写到command里
    17. ssize_t s = read(waitfd,&command,sizeof(command));
    18. if(s == 0)
    19. {
    20. q = true;
    21. return -1;
    22. }
    23. assert(s == sizeof(uint32_t));
    24. return command;
    25. }
    26. //给一个进程id,通过文件描述符fd,发送命令command
    27. void sendAndExec(pid_t id,int fd, uint32_t command)
    28. {
    29. write(fd,&command,sizeof(command));
    30. cout << "调用进程[" << id << "] 执行:`" << desc[command] << endl;
    31. }
    32. int main()
    33. {
    34. //将task中的方法装载进来
    35. load();
    36. //pair键值对:pid、pipefd
    37. vectorpid_t, int>> slots;
    38. //创建多个进程
    39. for(int i= 0; i < PROCESS_NUM; ++i)
    40. {
    41. //创建管道
    42. int pipefd[2] = {0};
    43. int n = pipe(pipefd);
    44. assert(n == 0);
    45. //这里的(void)n与前一个例子一样的作用
    46. //release下,assert就没用了,n会被认为没有使用
    47. (void)n;
    48. pid_t id = fork();
    49. assert(id != -1);
    50. //子进程进行读取
    51. if(id == 0)
    52. {
    53. //子进程关闭写端
    54. close(pipefd[1]);
    55. while(true)
    56. {
    57. //false表示不退出
    58. bool q = false;
    59. //子进程等命令,不发命令,就阻塞
    60. int command = waitcommand(pipefd[0], q);
    61. //执行对应的命令
    62. if(command >= 0 && command < tasksize())
    63. {
    64. //执行call中command对应的方法
    65. call[command]();
    66. }
    67. else
    68. {
    69. cout << "非法command: " << command << endl;
    70. }
    71. }
    72. exit(1);
    73. }
    74. //父进程关闭读端
    75. close(pipefd[0]);
    76. //存每个子进程的pid
    77. slots.push_back(pair<pid_t,int>(id,pipefd[1]));
    78. }
    79. //父进程派发任务
    80. //让数据源更加随机
    81. srand((unsigned long)time(nullptr) ^ getpid() ^ 89745213L);
    82. while(true)
    83. {
    84. //下面屏蔽的部分是自动选择任务,不需要人为输入
    85. //选择任务
    86. //int command = rand() % tasksize();
    87. //选择进程
    88. //int selectID = rand() % slots.size();
    89. //将任务交给子进程
    90. //sendAndExec(slots[selectID].first,slots[selectID].second,command);
    91. //sleep(1);
    92. int num;
    93. int command;
    94. cout << "##########################################" << endl;
    95. cout << "##### 1.展示功能 2.发送命令 #####" << endl;
    96. cout << "##########################################" << endl;
    97. cout << "请输入你的选择:";
    98. cin >> num;
    99. if(num == 1)
    100. show();
    101. else if(num == 2)
    102. {
    103. cout << "请选择你的命令:";
    104. //选择任务
    105. cin >> command;
    106. //选择进程
    107. int selectID = rand()%slots.size();
    108. //将任务交给子进程
    109. sendAndExec(slots[selectID].first,slots[selectID].second,command);
    110. }
    111. }
    112. //关闭fd,子进程会退出
    113. for(const auto& e : slots)
    114. {
    115. close(e.second);
    116. }
    117. //等待子进程退出,回收子进程信息
    118. for(const auto& e : slots)
    119. {
    120. //默认在阻塞状态去等待子进程
    121. waitpid(e.first,nullptr,0);
    122. }
    123. return 0;
    124. }

    Task.hpp

    (以hpp结尾,.cc的实现代码混入.h头文件当中,定义与实现都包含在同一文件)

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. //这里表示func这个函数类型,返回值都是void,没有参数
    9. typedef std::function<void()> func;
    10. //call中存各种func类型的函数
    11. std::vector call;
    12. //desc是描述各个命令编号,所对应的任务名称的
    13. std::unordered_map<int, std::string> desc;
    14. void execadd()
    15. {
    16. std::cout << "程序[" << getpid() << "] 执行加法操作的任务" << std::endl;
    17. }
    18. void execsub()
    19. {
    20. std::cout << "程序[" << getpid() << "] 执行减法操作的任务" << std::endl;
    21. }
    22. void execmul()
    23. {
    24. std::cout << "程序[" << getpid() << "] 执行乘法操作的任务" << std::endl;
    25. }
    26. void execdiv()
    27. {
    28. std::cout << "程序[" << getpid() << "] 执行除法操作的任务" << std::endl;
    29. }
    30. void load()
    31. {
    32. //即0号编号的任务是execadd
    33. desc.insert({call.size(),"execadd : 加法"});
    34. call.push_back(execadd);
    35. desc.insert({call.size(),"execsub : 减法"});
    36. call.push_back(execsub);
    37. desc.insert({call.size(),"execmul : 乘法"});
    38. call.push_back(execmul);
    39. desc.insert({call.size(),"execdiv : 除法"});
    40. call.push_back(execdiv);
    41. }
    42. void show()
    43. {
    44. for(const auto& e : desc)
    45. {
    46. std::cout << e.first << "->" << e.second << std::endl;
    47. }
    48. }
    49. int tasksize()
    50. {
    51. return call.size();
    52. }

    人为输入的结果如下:

    自动选择任务的结果如下:

    小型的进程池实现完毕


    命名管道

    关于匿名管道和命名管道,上面所举的例子都是匿名管道,匿名管道缺点是只能由有亲缘关系的进程进行通信,如果想要两个毫不相关的进程进行通信,就需要用到命名管道了

    匿名管道和命名管道一样,都是两个进程通过同一份文件,进行通信,只不过看到同一份文件的手段、途径是不一样的

    匿名管道是子进程通过继承父进程的方式,打开同一份文件

    命名管道是创建一个管道文件,并且让两个不相关的进程打开同一个文件


    创建管道文件需要用到mkfifo,可以在指定路径下创建命名管道

    man查看:

    下面就是创建一个name_pipe的管道文件:

    圈中的p就是指管道文件

    下面具体演示现象:

    首先复制SSH渠道,形成左右两个窗口(即有两个毫不相关的进程),在同一个路径下,:

    左边的窗口,先往name_pipe中重定向一句话hello,写到name_pipe中:

    这时由于左边的窗口写了内容,但是右边窗口并没有打开,所以此时处于阻塞状态

    所以此时右边窗口cat从管道中把数据读取出来:

    此时完成了一个进程向另一个进程通过管道的方式写入消息的过程

    如果想删除管道文件,可以rm,也可以unlink


    mkfifo

    man 3 mkfifo查看mkfifo函数

    头文件:

    sys/types.h和sys/stat.h

    参数:

    第一个参数pathname,表示特定的路径

    第二个参数mode,open时也见过,表示需要指定权限(例如0666,6表示rw-)

    返回值:

    mkfifo成功了返回0,小于0表示创建失败 


    下面用样例更清楚的理解命名管道的知识:

    创建两个.cc(.cc、.cxx、.cpp是一样的)文件,分别是client.cc和server.cc,分别表示服务器端和顾客端

    common.hpp表示client.cc和server.cc必须包含的文件

    log.hpp表示每次执行完操作,打印提示信息

    makefile当然也是有的,方便操作

    下面看具体演示,首先复制一个ssh渠道,让两个毫不相干的进程都在一个路径下(具体代码在演示结果的下面)

    左边窗口当做server端,右边窗口当做client端:

    左边先运行server:

    这时打印显示创建管道文件成功,再在右边窗口运行client:

    这时server端会显示打开管道文件成功

    接着在client端输入信息,分别输入你好,再见:

    这时serve端也会显示对应输入的信息,最后client端Ctrl + c 退出进程:

    server端会显示左边红框的信息,整个过程结束


    下面是具体代码:

    Makefile:

    1. .PHONY:all
    2. all:client server
    3. client:client.cc
    4. g++ -o $@ $^ -std=c++11
    5. server:server.cc
    6. g++ -o $@ $^ -std=c++11
    7. .PHONY:clean
    8. clean:
    9. rm -f client server

    common.hpp:

    1. //如果_COMMON_H_不存在就define
    2. #ifndef _COMMON_H_
    3. #define _COMMON_H_
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include "log.hpp"
    13. using namespace std;
    14. #define SIZE 128
    15. //设置MODE权限为0666
    16. #define MODE 0666
    17. //设置路劲path为当前路径的fifo.ipc文件
    18. string path = "./fifo.ipc";
    19. #endif

    log.hpp:

    1. #ifndef _LOG_H_
    2. #define _LOG_H_
    3. #include
    4. #include
    5. std::ostream &log(std::string message)
    6. {
    7. std::cout << " | " << (unsigned)time(nullptr) << " | " << message;
    8. return std::cout;
    9. }
    10. #endif

    client.cc:

    1. #include "common.hpp"
    2. int main()
    3. {
    4. //获取管道文件
    5. int fd = open(path.c_str(), O_WRONLY);
    6. if(fd < 0)
    7. {
    8. perror("open");
    9. exit(1);
    10. }
    11. //通信过程
    12. string buffer;
    13. while(true)
    14. {
    15. cout << "请输入信息:";
    16. getline(cin, buffer);
    17. write(fd,buffer.c_str(),buffer.size());
    18. }
    19. //关闭fd
    20. close(fd);
    21. return 0;
    22. }

    server.cc

    1. #include "common.hpp"
    2. int main()
    3. {
    4. //创建管道文件
    5. if(mkfifo(path.c_str(), MODE) < 0)
    6. {
    7. perror("mkfifo");
    8. exit(1);
    9. }
    10. log("创建管道文件成功!") << endl;
    11. //文件操作
    12. int fd = open(path.c_str(), O_RDONLY);
    13. if(fd < 0)
    14. {
    15. perror("open");
    16. exit(2);
    17. }
    18. log("打开管道文件成功!") << endl;
    19. //编写通信代码
    20. char buffer[SIZE];
    21. while(true)
    22. {
    23. //清空buffer
    24. memset(buffer,'\0',sizeof(buffer));
    25. //从管道文件中读取
    26. //sizeof(buffer)-1是因为系统接口不考虑\0结尾
    27. ssize_t s = read(fd,buffer,sizeof(buffer) - 1);
    28. if(s > 0)
    29. {
    30. //读取成功
    31. cout << "client :" << buffer << endl;
    32. }
    33. else if(s == 0)
    34. {
    35. //读到了文件结尾
    36. cerr << "读到文件结尾,client和server都退出" << endl;
    37. break;
    38. }
    39. else
    40. {
    41. //读取失败
    42. perror("read");
    43. break;
    44. }
    45. }
    46. //关闭文件
    47. close(fd);
    48. log("关闭管道文件成功!") << endl;
    49. //通信完毕就删除文件
    50. unlink(path.c_str());
    51. log("删除管道文件成功!") << endl;
    52. return 0;
    53. }

    三、system v共享内存

    操作系统提供了共享内存,共享内存的本质:

    共享内存 = 共享内存块 + 共享内存对应的内核数据结构

    共享内存映射到各自进程的地址空间后,不用经过系统调用,直接可以访问,意味着双方进程要通信,直接进行内存级的读和写即可

    shmget函数(创建)

    首先共享内存需要用到shmget函数,作用是创建并获取共享内存

    man 2 shmget查看:

    包含了两个头文件:

    sys/ipc.h和sys/shm.h

    函数参数:

    第一个参数key(key_t其实就是32位的整数类型),是用于保证通信的双方进程看到的是同一份共享内存,key在系统是唯一的,通信双方使用同一个key,只要key值相同,通信双方就是看到了同一个共享内存

    第二个参数size,表示要创建的共享内存有多大

    第三个参数shmflg,一般由两种选项(IPC_CREAT、IPC_EXCL)

    IPC_CREAT:单独使用时,如果底层已经存在,就获取并返回它;如果底层不存在,就创建并返回它

    IPC_EXCL:单独使用是无意义的

    IPC_CREAT、IPC_EXCL共同使用:如果底层不存在,创建并返回;如果底层已经存在,出错返回

    创建共享内存要有权限,还需 | 0666

    函数返回值:

    成功则返回一个合法的标识符,失败就返回-1


    下面具体讲解关于第一个参数key的相关知识

    ftok函数(生成key)

    为了能够形成唯一的key值,需要用到函数ftok

    ftok其实是一套算法,只是为了将pathname和proj_id联合起来,形成一个唯一值

    ftok包含头文件sys/types.h和sys/ipc.h

    参数pathname是路径,proj_id是项目id(一般是0~255之间)

    ftok的返回值成功就返回key,失败返回-1

    下面是ftok的使用:

    shmserver.cc:

    shmclient.cc:

    common.hpp:

    此时运行代码,观察shmserver.cc和shmclient.cc中创建出来的key值:

    通过结果发现,key值相同


    共享内存的声明周期是随内核的

    查看共享内存:ipcs -m

    上图的perm就是权限,nattch与该共享内存关联的进程个数,status是状态 

    手动删除共享内存:ipcrm -m [shmid]

    删除后shmid为0的共享内存就被删除了

    这是手动删除共享内存,比较麻烦,也有自动删除共享内存的函数

    shmctl函数(删除)

    man查看shmctl:

    包含头文件sys/ipc.h和sys/shm.h

    函数参数:

    第一个参数shmid,就是创建的共享内存用户管理的id

    第二个参数cmd,有不同的选项,对这个共享内存有不同的操作方案(删除一般用IPC_RMID)

    IPC_RMID即使有进程与当前共享内存的shm挂接,依旧删除共享内存

    第三个参数通常用于获取共享内存的属性,一般只进行删除操作时,设为空即可

    函数返回值:

    失败会返回-1,成功会返回0

    使用如下(shmid是自己创建的):


    shmat/dt函数(挂接/去关联)

    at就是attch的意思

    man shmat:

    包含头文件sys/types.h和sys/shm.h

    shmat函数参数

    第一个参数shmid,就是要挂接的共享内存的用户管理的id

    第二个参数shmaddr,就是要挂接这个共享内存的虚拟地址(由于虚拟地址我们并不清楚,所以一般设为空,让OS帮我们自由挂接)

    第三个参数shmflg,就是挂接共享内存的挂接方式(例如SHM_RDONLY只读,如果是0表示默认)

    shmat函数返回值:

    成功时会返回挂接成功的共享内存的地址(虚拟地址),失败返回-1

    shmdt函数参数:

    shmaddr就是刚刚shmat函数成功后的返回值

    shmdt函数返回值:

    成功返回0,失败返回-1


    共享内存总结的结论:

    只要通信双方使用共享内存,一方直接向共享内存中写入,另一方马上就能看到对方写入的数据,所以共享内存是所以进程间通信中速度最快的,因为不需要过多的拷贝

    与管道不同,共享内存缺乏访问控制,所以会带来并发问题


    由于共享内存缺乏访问控制,而管道是有访问控制的,所以我们可以借助管道使得共享内存拥有访问控制

    下面例子具体演示(代码下演示结果的下方)

    有以下文件:

    首先复制SSH渠道,使得两个窗口都处于同一路径下,一端作为server端,一端作为client端:

    接着make,创建两个可执行文件:

    左边窗口当做server端,右边当做client端:

    左边窗口server端显示没有数据,等待中,等待右边窗口client端输入数据

    右边输入数据后结果如下:

    最后想退出时,client端输入quit即可:


    下面是上述例子的代码部分:

    Makefile:

    1. .PHONY:all
    2. all:shmclient shmserver
    3. shmclient:shmclient.cc
    4. g++ -o $@ $^ -std=c++11
    5. shmserver:shmserver.cc
    6. g++ -o $@ $^ -std=c++11
    7. .PHONY:clean
    8. clean:
    9. rm -f shmclient shmserver

    log.hpp:

    1. #ifndef _LOG_H_
    2. #define _LOG_H_
    3. #include
    4. #include
    5. std::ostream &log(std::string message)
    6. {
    7. std::cout << " | " << (unsigned)time(nullptr) << " | " << message;
    8. return std::cout;
    9. }
    10. #endif

    common.hpp:

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include "log.hpp"
    13. using namespace std;
    14. #define PATH_NAME "/home/fcy"
    15. #define PROJ_ID 0x55
    16. //共享内存的大小最好是页的整数倍(4096)
    17. #define SHM_SIZE 4096
    18. #define FIFO_NAME "./fifo"
    19. //由于共享内存没有访问控制
    20. //所以借助管道,实现访问控制
    21. class Init
    22. {
    23. public:
    24. Init()
    25. {
    26. umask(0);
    27. int n = mkfifo(FIFO_NAME,0666);
    28. assert(n == 0);
    29. (void)n;
    30. log("创建管道文件成功!");
    31. }
    32. ~Init()
    33. {
    34. unlink(FIFO_NAME);
    35. log("删除管道文件成功!");
    36. }
    37. };
    38. #define READ O_RDONLY
    39. #define WRITE O_WRONLY
    40. //使用下面的函数进行读写操作
    41. int OpenFIFO(std::string pathname, int flags)
    42. {
    43. int fd = open(pathname.c_str(),flags);
    44. assert(fd >= 0);
    45. return fd;
    46. }
    47. //读取需要等待别人写入
    48. void Wait(int fd)
    49. {
    50. log("没有数据,等待中") << endl;
    51. uint32_t str = 0;
    52. ssize_t s = read(fd,&str,sizeof(uint32_t));
    53. assert(s == sizeof(uint32_t));
    54. (void)s;
    55. }
    56. //写入信息
    57. int Signal(int fd)
    58. {
    59. uint32_t str = 1;
    60. ssize_t s = write(fd,&str,sizeof(uint32_t));
    61. assert(s == sizeof(uint32_t));
    62. (void)s;
    63. log("正在等待输入数据") << endl;
    64. }
    65. //关闭该文件描述符
    66. void CloseFIFO(int fd)
    67. {
    68. close(fd);
    69. }

    shmserver.cc:

    1. #include "common.hpp"
    2. //程序加载时自动构建全局变量,自动调用该类的构造函数
    3. //程序退出时,全局变量被析构,自动调用析构函数删除管道文件
    4. Init init;
    5. //将10进制数转化为16进制,方便查看共享内存时观察
    6. string Trans(key_t k)
    7. {
    8. char buffer[32];
    9. snprintf(buffer, sizeof(buffer), "0x%x", k);
    10. return buffer;
    11. }
    12. int main()
    13. {
    14. //创建公共的key
    15. key_t k = ftok(PATH_NAME, PROJ_ID);
    16. assert(k != -1);
    17. log("创建key成功") << "server key->" << Trans(k) << endl;
    18. //创建共享内存,建议创建全新的共享内存
    19. int shmid = shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
    20. if(shmid == -1)
    21. {
    22. perror("shmget");
    23. exit(1);
    24. }
    25. log("创建共享内存成功") << "shmid->" << shmid << endl;
    26. //将指定的共享内存,挂接到自己的地址空间
    27. char* shmaddr = (char*)shmat(shmid,nullptr,0);
    28. log("挂接共享内存成功") << "shmid->" << shmid << endl;
    29. int fd = OpenFIFO(FIFO_NAME,READ);
    30. //将共享内存当做字符串使用
    31. for(;;)
    32. {
    33. //刚开始先wait,数据为空,先不读取
    34. Wait(fd);
    35. printf("%s\n",shmaddr);
    36. //当client端不写入了,server端就停止读取共享内存的数据
    37. if(strcmp(shmaddr, "quit") == 0)
    38. break;
    39. }
    40. //将指定的共享内存,从自己的地址空间中去除关联
    41. int ret = shmdt(shmaddr);
    42. assert(ret != -1);
    43. (void)ret;
    44. log("去关联共享内存成功") << "shmid->" << shmid << endl;
    45. //删除共享内存
    46. ret = shmctl(shmid,IPC_RMID,nullptr);
    47. assert(ret != -1);
    48. (void)ret;
    49. log("删除共享内存成功") << "shmid->" << shmid << endl;
    50. //关闭文件描述符
    51. CloseFIFO(fd);
    52. return 0;
    53. }

    shmclient.cc:

    1. #include "common.hpp"
    2. int main()
    3. {
    4. //创建公共的key
    5. key_t k = ftok(PATH_NAME, PROJ_ID);
    6. if(k < 0)
    7. {
    8. perror("ftok");
    9. exit(1);
    10. }
    11. log("创建key成功") << "client key->" << k << endl;
    12. //获取共享内存
    13. int shmid = shmget(k,SHM_SIZE,0);
    14. if(shmid < 0)
    15. {
    16. perror("ftok");
    17. exit(2);
    18. }
    19. log("创建共享内存成功") << "shmid->" << shmid << endl;
    20. //挂接共享内存
    21. char* shmaddr = (char*)shmat(shmid,nullptr,0);
    22. if(shmaddr == nullptr)
    23. {
    24. perror("ftok");
    25. exit(3);
    26. }
    27. log("挂接共享内存成功") << "shmid->" << shmid << endl;
    28. int fd = OpenFIFO(FIFO_NAME,WRITE);
    29. //将共享内存当做char类型的buffer
    30. while(true)
    31. {
    32. ssize_t s = read(0,shmaddr,SHM_SIZE-1);
    33. if(s > 0)
    34. {
    35. //将quit后面的回车(\n)变为\0
    36. //否则无法判断是quit这个字符串
    37. shmaddr[s-1] = 0;
    38. Signal(fd);
    39. if(strcmp(shmaddr,"quit") == 0)
    40. break;
    41. }
    42. }
    43. //关闭
    44. CloseFIFO(fd);
    45. //将指定的共享内存,从自己的地址空间中去除关联
    46. int ret = shmdt(shmaddr);
    47. assert(ret != -1);
    48. (void)ret;
    49. log("去关联共享内存成功") << "shmid->" << shmid << endl;
    50. //client不需要删除共享内存
    51. return 0;
    52. }

    四、初识信号量

    我们之前所学的,为了进程间通信,都是要让不同的进程看到同一份资源,而不同的进程的进程看到同一份资源,会带来时序问题,造成数据不一致的问题

    多个执行流互相运行是相互干扰,主要是访问临界资源时不加以保护,而在非临界区是不会互相干扰的

    下面引入几个概念:

    临界资源多个进程(执行流)看到的公共的一份资源称为临界资源

    临界区:自己的进程访问临界资源的代码称为临界区

    互斥为了更好的进行临界区的保护,可以让多个执行流在任意时刻只能有一个进程进入临界区,这种方式称为互斥

    原子性:要么不做,要么做完,没有中间状态,称之为原子性


    每一个进程想要进入临界资源,访问临界资源的一部分,需要先申请信号量,才能使用临界资源

    信号量就像一个计数器一样,申请信号量的本质是让信号量计数器--

    申请信号量成功后,临界资源内部就会预留你想要的资源,是对临界资源的预定机制

    所以申请信号量,信号量计数器--,称之为信号量的P操作,必须是原子的

    释放信号量,信号量计数器会++,称之为信号量的V操作,也必须是原子的

    访问临界资源也就是进程执行自己的临界区代码


    剩下详细的信号量知识在多线程部分会说到

  • 相关阅读:
    python基础知识整理 03-字典、集合
    从零开始,开发一个 Web Office 套件(9):拖动鼠标选中文字 Edge Case
    idea如何设置代理实现管理突破呢
    C++ Tutorials: C++ Language: Compound data types: Pointers
    神经网络用于控制的优越性,神经网络稳定性理论
    【云原生】多网络情况下,Kafka客户端如何选择合适的网络发起请求
    Mybatis-Plus关于MYSQL中JSON字段的操作
    SpringBoot读取json文件
    【SpringMVC篇】详解SpringMVC入门案例
    第11章 Java集合(二)
  • 原文地址:https://blog.csdn.net/m0_64411530/article/details/134518558