linux系统编程 进程间通信

简介: linux系统编程 进程间通信

一、进程间通信

(1)进程间通信的原理
    尽管进程空间是各自独立的,相互之间没有任何可以共享的空间,但是至少还有一样东西是所有
  进程所共享的,那就是OS,因为甭管运行有多少个进程,但是它们共用OS只有一个。
    既然大家共用的是同一个OS,那么显然,所有的进程可以通过大家都共享第三方OS来实现数据的
  转发。
    因此进程间通信的原理就是,OS作为所有进程共享的第三方,会提供相关的机制,以实现进程间数
  据的转发,达到数据共享的目的。
(2)广义上的进程间通信
  其实广义上来说,任何一种能够实现进程间数据交换的方式,都可以被称为进程间通信,比如
    A进程——————文件———————B进程
    A进程—————数据库——————B进程
    不过一般来说,这种广义的进程间通信,并不被算作真正的“进程间通信”。
    只有OS所提供的专门的通信机制,才能算作是真正的“进程间通信”,我们本章所讲的就是狭义上
  的真正的“进程间通信”。
(3)Linux提供的“进程通信”方式有哪些
    Linux的父亲是Unix,所以Linux的进程间通信,其实都是继承于Unix。
  不管继承自谁,Linux所提供的进程间通信机制到底有哪些呢?
    1)信号
        上一章讲的信号其实也是进程间通信的一种,只不过信号是非精确通信,而本章讲的
      IPC是精确通信。所谓精确通信,就是能告诉你详细信息,而信号这种非精确通信,只能
      通知某件事情发生了,但是无法告诉详细信息。
    2)本章的进程间通信
      (a)管道
          · 无名管道
          · 有名管道
          OS在进程之间建立一个“管道”,通过这个管道来实现进程间数据的交换。
      (b)system V IPC
          · 消息队列:通过消息队列来通信
          · 共享内存:通过共享内存来通信
          · 信号量:借助通信来实现资源的保护(一种加锁机制)
    3)域套接字
        讲网络编程时再介绍。

二、无名管道

2.1 无名管道的通信原理

  具体来说就是,内核会开辟一个“管道”,通信的进程通过共享这个管道,从而实现通信。
(1)到底什么是管道
      内核的代码也是运行在物理内存上的,内核创建一个“管道”,其实就是在内核自己所在的物理
    内存空间中开辟出一段缓存空间,比如char buf[1024];
(2)如何操作无名管道
      以文件的方式来读写管道,以文件方式来操作时
      1)有读写用的文件描述符
      2)读写时会用write、read等文件Io函数。
(3)为什么叫无名管道
      既然可以通过“文件描述符”来操作管道,那么它就是一个文件(管道文件),但是无名管道
    文件比较特殊,它没有文件名,正是因为没有文件名,所有被称为无名管道。
      没有文件名,我们怎么操作这个文件呢?
      后面再讲这个问题。

2.2 无名管道的API

2.2.1 函数原型

  #include <unistd.h>
  int pipe(int pipefd[2]);
(1)功能
    创建一个用于亲缘进程(父子进程)之间通信的无名管道(缓存),并将管道与两个读写文件描述符
  关联起来。
    无名管道只能用于亲缘进程之间通信,为什么只能用于亲缘进程之间通信呢?
    后面再详细介绍。
(2)参数:缓存地址,缓存用于存放读写管道的文件描述符。
      从这个参数的样子可以看出,这个缓存就是一个拥有两个元素的int型数组。
    1)元素[0]:里面放的是读管道的读文件描述符
    2)元素[1]:里面放的是写管道的写文件描述符。
      特别需要注意的是,这里的读和写文件描述符,是两个不同的文件描述符。
      从这里大家也可以看出,并不是所有的文件描述符,都是通过open函数打开文件得到的。
    这里无名管道的读、写文件描述符,就是直接在创建管道时得到的,与open没有任何关系。
      而且这里也根本没办法使用open函数,因为open函数需要文件路径名,无名管道连文件名
    都没有,所以说根本就没办法使用open来打开文件,返回文件描述符。
(3)返回值:成功返回0,失败则返回-1,并且errno被设置。

2.2.2 无名管道特点

(1)无名管道只能用于亲缘进程之间通信,为什么?
  由于没有文件名,因此进程没办法使用open打开管道文件,从而得到文件描述符,所以只有一种办法,
那就是父进程先调用pipe创建出管道,并得到读写管道的文件描述符。
  然后再fork出子进程,让子进程通过继承父进程打开的文件描述符,父子进程就能操作同一个管道,
从而实现通信。

图:

  对子进程继承父进程属性这一点不清楚的同学,说明“进程控制”这一章你没有学好,你需要回去
复习一下。
  通过前面的描述,我们自然就能理解,为什么无名管道只能用于亲缘进程之间通信了。
  什么样的进程之间,我们可以称为亲缘进程呢?
  只要是存在继承关系的进程就是亲缘进程,继承关系分为两种。
  (1)直接继承关系
      父进程————>子进程
  (2)间接继承关系
      父进程————>子进程————>子进程————>...
(2)读管道时,如果没有数据的话,读操作会休眠(阻塞)

2.2.3 代码演示

2.2.3.1 父子进程单向通信

实现步骤:

(a)父进程在fork之前先调用pipe创建无名管道,并获取读、写文件描述符
(b)fork创建出子进程,子进程继承无名管道读、写文件描述符
(c)父子进程使用各自管道的读写文件描述符进行读写操作,即可实现通信

代码演示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
void print_err(char *estr)
{
  perror(estr);
  exit(-1);
}
int main(void)
{
  int ret = 0;
  //[0]:读文件描述符
  //[1]:写文件描述符
  int pipefd[2] = {0};//用于存放管道的读写文件描述符
  ret = pipe(pipefd);
  if(ret == -1) print_err("pipe fail");
  ret = fork();
  if(ret > 0)
  { 
    close(pipefd[0]); 
    while(1)
    {
      write(pipefd[1], "hello", 5);       
      sleep(1);
    }
  }
  else if(ret == 0)
  {
    close(pipefd[1]);
    while(1)
    {
      char buf[30] = {0};
      bzero(buf, sizeof(buf));
      read(pipefd[0], buf, sizeof(buf));
      printf("child, recv data:%s\n", buf);
    } 
  }
  return 0;
}
为了避免干扰,通常会把没有使用的文件描述关闭。

SIGPIPE信号:

(a)我们讲信号时介绍过这个信号,这里再说一说这个信号,为什么讲这个信号?
            · 与管道有关
            · 回顾信号的内容,进行知识的综合运用
(b)什么时候会产生在这个信号?
      写管道时,如果管道的读端被close了话,向管道“写”数据的进程会被内核发送一个
    SIGPIPE信号,发这个信号的目的就是想通知你,管道所有的“读”都被关闭了。
      这就好比别人把水管的出口(读)给堵住了,结果你还一直往里面灌水(写),别人肯定
    会警告你,因为你这样可能会对水管造成损害,道理其实是类似的。
      由于这个信号的默认动作是终止,所以收到这个信号的进程会被终止,如果你不想被终止的
    话,你可以忽略、捕获、或者屏蔽这个信号。
      只有当管道所有的读端都被关闭时,才会产生这个信号,只有还有一个读端开着,就不会产生。
2.2.3.2 父子进程双向通信

1)单个无名管道无法实现双向通信,为什么?

因为使用单个无名管道来实现双向通信时,自己发送给对方的数据,就被自己给抢读到。

2)如何实现无名管来实现双向通信

使用两个无名管道,每个管道负责一个方向的通信。

代码演示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
void print_err(char *estr)
{
  perror(estr);
  exit(-1);
}
int main(void)
{
  int ret = 0;
  //[0]:读文件描述符
  //[1]:写文件描述符
  int pipefd1[2] = {0};//用于存放管道的读写文件描述符
  int pipefd2[2] = {0};//用于存放管道的读写文件描述符
  ret = pipe(pipefd1);
  if(ret == -1) print_err("pipe fail");
  ret = pipe(pipefd2);
  if(ret == -1) print_err("pipe fail");
  ret = fork();
  if(ret > 0)
  { 
    close(pipefd1[0]);
    close(pipefd2[1]);
    char buf[30] = {0};
    while(1)
    {
      write(pipefd1[1], "hello", 5);        
      sleep(1);
      bzero(buf, sizeof(buf));
      read(pipefd2[0], buf, sizeof(buf));
      printf("parent, recv data:%s\n", buf);
    }
  }
  else if(ret == 0)
  {
    close(pipefd1[1]);
    close(pipefd2[0]);
    char buf[30] = {0};
    while(1)
    {
      sleep(1); 
      write(pipefd2[1], "world", 5);
      bzero(buf, sizeof(buf));
      read(pipefd1[0], buf, sizeof(buf));
      printf("child, recv data:%s\n", buf);
    } 
  }
  return 0;
}

2.3 无名管道有两个缺点

(1)无法用于非亲缘进程之间
    因为非亲缘进程之间没办法继承管道的文件描述符。
(2)无法实现多进程之间的网状通信
    如果非要使用无名管道实现多进程之间的网状通信的话,文件描述符的继承关系将非常的复杂。
  所以无名管道基本只适合两个进程间的通信。

2.4 什么时候合适使用无名管道呢?

如果通信的进程只有两个,而且还是亲缘进程时,那么可以使用无名管道来通信。
比如:
1)直接继承父子进程之间的通信
  父进程 ————————————————> 子进程
    |                         |
    |—————————无名管道————————|
2)间接继承关系的两进程之间的通信     
  父进程 ——————> 子进程 ——————> 子进程 ———————> 子进程 
    |           |
    |————————————————————无名管道———————————————————|

三、有名管道

3.1 为什么叫“有名管道”

  无名管道因为没有文件名,被称为了无名管道,同样的道理,有名管道之所以叫“有名管道”,是因为
它有文件名。
  也就是说当我们调用相应的API创建好“有名管道”后,会在相应的路径下面看到一个叫某某名字的
“有名管道文件”。
  不管是有名管道,还是无名管道,它们的本质其实都是一样的,它们都是内核所开辟的一段缓存空间。
进程间通过管道通信时,本质上就是通过共享操作这段缓存来实现,只不过操作这段缓存的方式,是以
读写文件的形式来操作的。

3.2 有名管道特点

1)能够用于非亲缘进程之间的通信
  因为有文件名,所以进程可以直接调用open函数打开文件,从而得到文件描述符,不需要像无名管道
一样,必须在通过继承的方式才能获取到文件描述符。
  所以任何两个进程之间,如果想要通过“有名管道”来通信的话,不管它们是亲缘的还是非亲缘的,只要
调用open函数打开同一个“有名管道”文件,然后对同一个“有名管道文件”进行读写操作,即可实现通信。
  A进程 —————————> 有名管道 ————————> B进程
  总之,不管是亲缘进程还是非亲缘进程,都可以使用有名管道来通信。
2)读管道时,如果管道没有数据的话,读操作同样会阻塞(休眠)
3)当进程写一个所有读端都被关闭了的管道时,进程会被内核返回SIGPIPE信号
    如果不想被该信号终止的话,我们需要忽略、捕获、屏蔽该信号。
    不过一般情况下,不需要对这个信号进行处理,除非你有必须要处理的理由。

3.3 有名管道的使用步骤

(1)进程调用mkfifo创建有名管道
(2)open打开有名管道
(3)read/write读写管道进行通信
  对于通信的两个进程来说,创建管道时,只需要一个人创建,另一个直接使用即可。
  为了保证管道一定被创建,最好是两个进程都包含创建管道的代码,谁先运行就谁先创建,后运行的
发现管道已经创建好了,那就直接open打开使用。

3.4 有名管道API

3.4.1 函数原型

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
(1)功能
    创建有名管道文件,创建好后便可使用open打开。
    如果是创建普通文件的话,我们可以使用open的O_CREAT选项来创建,比如:
      open("./file", O_RDWR|O_CREAT, 0664);
    但是对于“有名管道”这种特殊文件,这里只能使用mkfifo函数来创建。
(2)参数
  1)pathname:被创建管道文件的文件路径名。
  2)mode:指定被创建时原始权限,一般为0664(110110100),必须包含读写权限。
        使用open函数创建普通文件时,指定原始权限是一样的。
          open("./file", O_RDWR|O_CREAT, 0664);
          不过我们学习第2章时讲过,创建新文件时,文件被创建时的真实权限=mode & 
        (~umask)umask是文件权限掩码,一般默认为002或者022,对umask不清楚的同学
        ,请会看第2章。
      mkfifo(“./fifo”, 0664);
(3)返回值:成功返回0,失败则返回-1,并且errno被设置。  

3.4.2 代码演示

3.4.2.1 单向通信

mkfifo1.c代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define FIFONAME1 "./fifo1"
#define FIFONAME2 "./fifo2"
void print_err(char *estr)
{ 
  perror(estr);
  exit(-1);
} 
int creat_open_fifo(char *fifoname, int open_mode)
{ 
  int ret = -1;
  int fd = -1;  
  ret = mkfifo(fifoname, 0664);
  //如果mkfifo函数出错了,但是这个错误是EEXIST,不报这个错误(忽略错误)
  if(ret == -1 && errno!=EEXIST) print_err("mkfifo fail");  
  fd = open(fifoname, open_mode);
  if(fd == -1) print_err("open fail");
  return fd;
}
void signal_fun(int signo)
{
  //unlink();
  remove(FIFONAME1);
  exit(-1);
}
int main(void)
{
  char buf[100] = {0};
  int ret = -1;
  int fd1 = -1;
  fd1 = creat_open_fifo(FIFONAME1, O_RDONLY);
  signal(SIGINT, signal_fun);
  while(1)
  {
    bzero(buf, sizeof(buf));
    read(fd1, buf, sizeof(buf));
    printf("%s\n", buf);
  }
  return 0;
} 

mkfifo2.c代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define FIFONAME1 "./fifo1"
#define FIFONAME2 "./fifo2"
void print_err(char *estr)
{ 
  perror(estr);
  exit(-1);
} 
int creat_open_fifo(char *fifoname, int open_mode)
{ 
  int ret = -1;
  int fd = -1;  
  ret = mkfifo(fifoname, 0664);
  //如果mkfifo函数出错了,但是这个错误是EEXIST,不报这个错误(忽略错误)
  if(ret == -1 && errno!=EEXIST) print_err("mkfifo fail");  
  fd = open(fifoname, open_mode);
  if(fd == -1) print_err("open fail");
  return fd;
}
void signal_fun(int signo)
{
  //unlink();
  remove(FIFONAME1);
  exit(-1);
}
int main(void)
{
  char buf[100] = {0};
  int ret = -1;
  int fd1 = -1;
  fd1 = creat_open_fifo(FIFONAME1, O_RDONLY);
  signal(SIGINT, signal_fun);
  while(1)
  {
    bzero(buf, sizeof(buf));
    read(fd1, buf, sizeof(buf));
    printf("%s\n", buf);
  }
  return 0;
} 
3.4.2.2 双向通信
  同样的,使用一个“有名管道”是无法实现双向通信的,因为也涉及到抢数据的问题。
所以双向通信时需要两个管道。

3.5 什么时候使用有名管道

(1)实现网状通信
      面对众多进程网状通信,有名管道依然实现起来很吃力,所以基本也只适合于两个进程之间
    的通信。你自己可以尝试下,看看能不能使用有名管道来实现多进程的网状通信,在实现过程
    中,你自己就会发现,实现起来很困难。
(2)什么时候合适使用有名管道
    当两个进程需要通信时,不管是亲缘的还是非亲缘的,我们都可以使用有名管道来通信。
    至于亲缘进程,你也可以选择前面讲的无名管道来通信。

四、多进程与多线程

(1)回顾有名管道双向通信
  在使用有名管道实现双向通信时,由于读管道是阻塞读的,为了不让“读操作”阻塞“写操作”,使用了
父子进程来多线操作,
  1)父进程这条线:读管道1
  2)子进程这条线:写管道2
  实际上我们后面学习了线程以后,凡是涉及到多线操作的,基本都使用多线程来实现,比如
  1)主线程:读管道1
  2)次线程:写管道2
(2)对比多进程和多线程各自使用的场合   
    我们前面讲了进程,虽然线程还没有讲,但是大家大致也能理解线程是一个什么样的东西,事实上
  线程和进程都是并发运行的,但是线程和进程各自的使用的场合有所不同。
  1)线程
      凡是涉及多线时,我们使用线程来并发实现,比如我们讲的“有名管道”双向通信的例子,这
    个多线操作理论上就应该使用多线程来实现,只不过我们还没讲多线程而已。
      因为多线使用线程更省计算机cpu和内存的开销。
      也就是说创建出并发运行次线程的目的,是为了多线操作。
  2)进程
    一般情况下,我们的程序并不会涉及到多进程,当涉及多线操作时,我们会直接使用线程来并发
  实现。
  (a)那什么时候我们的程序才会涉及到多进程呢?
      一个简单的判断标准就是,如果你发现你的程序必须要去运行一个新程序时,此时必须
    涉及到多进程,因为此时如果你不创建一个子进程,你是没有办法来执行新程序的。
      新创建的子进程和父进程肯定是并发运行的,只不过这里并发运行的主要目的并不是
    为了多线操作,而是为了单独的去执行新程序,执行新程序时,我们只能使用多进程来操作
    ,你是没有办法使用多线程来操作的,因为线程是不可能去执行一个新程序的。
  (b)一般开发的应用程序不涉及执行新程序
      除非你开发的是比较大型框架,或者拥有众多功能套件的大型应用软件,在你的程序中必
    须开辟新的子进程去执行具有独立功能的新程序,否则们自己写的程序一般都是单进程,根本
    不涉及开辟一个并发运行的子进程,然后在子进程里面去执行新程序。
      也就是说创建一个并发执行的子进程的目的,是为了执行一个全新的程序。

五、System V IPC

5.1 有关System V IPC

(1)什么是System V IPC
  前面讲的无名管道和有名管道,都是UNIX系统早期提供的比较原始的一种进程间通信(IPC)方式,
早到Unix系统设计之初就有了。
  后来Unix系统升级到第5版本时,又提供了三种新的IPC通信方式,分别是:
  · 消息队列
  · 信号量
  · 共享内存
  System V就是系统第5版本的意思,后来的Linux也继承了unix的这三个通信方式,Unix是非常
早期的而且非常优秀OS,所以其它os也借鉴了这三种的System V IPC。
(2)System V IPC的特点
  1)管道(原始IPC)
      管道的本质就是一段缓存,不过Linux OS内核是以文件的形式来管理的,所以我们操作管
    道时,不管是无名管道,还是有名管道,我们都是使用文件描述符以文件的形式来操作的。
      所以我们操作管道时,除了pipe和mkfifo这两个函数外,其它的像read、write、open
    都是我们第1章所学的文件io函数,所以大家在学习管道时,会觉得比较容易。
  2)System V IPC
      System V IPC与管道有所不同,它完全使用了不同的实现机制,与文件没任何的关系,
    也就是说内核不再以文件的形式来管理System V IPC,所以不能再使用文件的方式来操作。
      对于System V IPC,OS内核提供了全新的API,对于这些API来说,我们的要求是理解而
    不是记忆,因为在以后的开发中,确实用的不多。就算真的用到了,只要你理解了,你自然
    能够很快的用起来。
  3)使用System V IPC时,不存在亲缘进程一说
      任何进程之间通信时,都可以使用System V IPC来通信。
(3)System V IPC标识符
    我们前面说过,System V IPC不再以文件的形式存在,因此没有文件描述符这个东西,但是它
  有类似的“标识符”。
    你完全可以认为这个“标识符”就是文件描述符的替代者,但是它是专门给System V IPC使用的,
  所以我们不能使用文件IO函数来操作“标识符”,只能使用System V IPC的特有API才能操作。
  1)怎么才能得到这个“标识符”
      调用某API创建好某个“通信结构”以后,API就会返回一个唯一的“标识符”。
      比如创建好了一个“消息队列”后,创建的API就会返回一个唯一标识消息队列的“标识符”。
  2)System V IPC标识符的作用?
      比如,如果创建的是消息队列的话,进程通过消息队列唯一的标识符,就能找到创建好的
    “消息队列”,使用这个消息队列,进程就能读写数据,然后实现进程间通信。

5.2 System V IPC 之 消息队列

5.2.1 消息队列的原理

(1)消息队列的本质
    消息队列的本质就是由内核创建的用于存放消息的链表,由于是存放消息的,所以我们就把这个链表
  称为了消息队列。通信的进程通过共享操作同一个消息队列,就能实现进程间通信。
(2)消息是如何存放在消息队列中的呢?
    消息队列这个链表有很多的节点,链表上的每一个节点就是一个消息。

图:

    从图中可以看出,每个消息由两部分组成,分别是消息编号(消息类型)和消息正文。
    1)消息编号:识别消息用
    2)消息正文:真正的信息内容
(3)收发数据的过程
  1)发送消息
  (a)进程先封装一个消息包
        这个消息包其实就是如下类型的一个结构体变量,封包时将消息编号和消息正文
      写到结构体的成员中。
        struct msgbuf
        {
              long mtype;         /* 放消息编号,必须> 0 */
              char mtext[msgsz];  /* 消息内容(消息正文) */
        };  
  (b)调用相应的API发送消息
        调用API时通过“消息队列的标识符”找到对应的消息队列,然后将消息包发送给消息队列
      ,消息包(存放消息的结构体变量)会被作为一个链表节点插入链表。
  2)接收消息
    调用API接收消息时,必须传递两个重要的信息,
    (a)消息队列标识符
    (b)你要接收消息的编号
        有了这两个信息,API就可以找到对应的消息队列,然后从消息队列中取出你所要编号
      的消息,如此就收到了别人所发送的信息。
  “消息队列”有点像信息公告牌,发送信息的人把某编号的消息挂到公告牌上,接收消息的人自己到公
告牌上去取对应编号的消息,如此,发送者和接受者之间就实现了通信。
(4)使用消息队列实现网状交叉通信
      对于前面讲的管道来说,很难实现网状交叉通信,但是使用消息队列确非常容易实现。

5.2.2 消息队列的使用步骤

(1)使用msgget函数创建新的消息队列、或者获取已存在的某个消息队列,并返回唯一标识消息队列的
        标识符(msqID),后续收发消息就是使用这个标识符来实现的。
(2)收发消息
    · 发送消息:使用msgsnd函数,利用消息队列标识符发送某编号的消息
    · 接收消息:使用msgrcv函数,利用消息队列标识符接收某编号的消息
(3)使用msgctl函数,利用消息队列标识符删除消息队列
  对于使用消息队列来通信的多个进程来说,只需要一个进程来创建消息队列就可以了,对于其它要参与
通信的进程来说,直接使用这个创建好的消息队列即可。
  为了保证消息队列的创建,最好是让每一个进程都包含创建消息队列的代码,谁先运行就由谁创建,
后运行的进程如果发现它想用的那个消息队列已经创建好了,就直接使用,当众多进程共享操作同一个
消息队列时,即可实现进程间的通信。

5.2.3 消息队列的函数

  所有system V ipc的API都是相似的,如果你能把消息队列的API搞清楚,后面的共享内存和信号量
API,理解起来很容易。
5.2.3.1 msgget函数
5.2.3.1.1 函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
(a)功能:利用key值创建、或者获取一个消息队列。
  · 如果key没有对应任何消息队列,那就创建一个新的消息队列
  · 如果key已经对应了某个消息队列,说明你要的消息队列已经存在了,那就获取这个消息队
    列来使用
    估计你也感觉到了,key值也能够唯一的标识消息队列,那key值到底是个啥?
    后面再介绍。
(b)返回值
  · 成功:返回消息队列标识符(消息队列的ID)
      对于每一个创建好的消息队列来说,ID是固定的。
  · 失败:失败返回-1,并设置errno。
(c)参数
  int msgget(key_t key, int msgflg);
  ·key值
  用于为消息队列生成(计算出)唯一的消息队列ID。
  我们可以指定三种形式的key值:
  - 第一种:指定为IPC_PRIVATE宏,指定这个宏后,每次调用msgget时都会创建一个新的消息队列。
      如果你每次使用的必须是新消息队列的话,就可以指定这个,不过这个用的很少。
      因为一般来说,只要有一个消息队列可以用来通信就可以了,并不需要每次都创建一个全新
    的消息队列。
  - 第二种:可以自己指定一个整形数,但是容易重复指定
      本来我想创建一个新的消息队列,结果我所指定的这个整形数,之前就已经被用于创建
    某个消息队列了,当我的指定重复时,msgget就不会创建新消息队列,而是使用的是别人
    之前就创建好的消息队列。
      所以我们也不会使用这种方式来指定key值。
  - 第三种:使用ftok函数来生成key
     #include <sys/types.h>
     #include <sys/ipc.h>
     key_t ftok(const char *pathname, int proj_id);
      ftok通过指定路径名和一个整形数,就可以计算并返回一个唯一对应的key值,
    只要路径名和整形数不变,所对应的key值就唯一不变的。
      不过由于ftok只会使用整形数(proj_id)的低8位,因此我们往往会指定为一个ASCII码值,
    因为ASCII码值刚好是8位的整形数。
  int msgget(key_t key, int msgflg);
  · msgflg
    指定创建时的原始权限,比如0664
    创建一个新的消息队列时,除了原始权限,还需要指定IPC_CREAT选项。
    msgid = msgget(key, 0664|IPC_CREAT);
      如果key值没有对应任何消息队列,就会创建一个新的消息队列,此时就会用到msgflg参数,
    但是如果key已经对应了某个早已存在消息队列,就直接返回这个已存在消息队列的ID(标识符),
    此时不会用到msgflg参数。
(d)多个进程是如何共享到同一个消息队列的 
  1)创建进程
    如果创建者使用"./file", 'a'生成一个key值,然后调用msgget创建了一个消息队列,比如:
    key = ftok("./file", 'a');
    msgid = msgget(key, 0664|IPC_CREAT);
    当创建者得到msgid后,即可操作消息队列。
  2)其它共享操作消息队列的进程
      共享的方法很简单,只要你能拿到别人创建好的消息队列的ID,即可共享操作同一个消
    息队列,实现进程间通信。
      获取别人创建好的消息队列的ID,有两个方法:
    (a)创建者把ID保存到某文件,共享进程读出ID即可
        这种情况下,共享进程根本不需要调用msgget函数来返回ID。
    (b)调用msgget获取已在消息队列的ID
        · 使用ftok函数,利用与创建者相同的“路径名”和8位整形数,生成相同的key值
        · 调用msgget函数,利用key找到别人创建好的消息队列,返回ID
          key = ftok("./file", 'a');
          msgid = msgget(key, 0664|IPC_CREAT);
          拿到了消息队列的ID后就能共享操作了。
        这种方法是最常用的方法,因为ftok所用到的“路径名”和“8位的整形数”比较好记忆,
      所以,你只要记住别人生成key值时所用的“路径名”和“8位的整形数”,你就一定能共享
      操作别人创建好的消息队列。
5.2.3.1.2 代码演示
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <signal.h>
#define MSG_FILE "./msgfile"
#define MSG_SIZE 1024
struct msgbuf
{
  long mtype;         /* 放消息编号,必须 > 0 */
  char mtext[MSG_SIZE];  /* 消息内容(消息正文) */
};                  
void print_err(char *estr)
{
  perror(estr);
  exit(-1);
}
int creat_or_get_msgque(void)
{
  int msgid = -1;
  key_t key = -1;
  int fd = 0;
  /* 创建一个消息队列的专用文件,ftok会用到这个文件的路径名 */
  fd = open(MSG_FILE, O_RDWR|O_CREAT, 0664);
  if(fd == -1) print_err("open fail");
  /* 利用存在的文件路径名和8位整形数,计算出key */
  key = ftok(MSG_FILE, 'a');
  if(key == -1) print_err("ftok fail");
  /* 利用key创建、或者获取消息队列 */
  msgid = msgget(key, 0664|IPC_CREAT);
  if(msgid == -1) print_err("msgget fail");
  return msgid;
} 
int msgid = -1;
//用于退出后删除消息队列
void signal_fun(int signo)
{
  msgctl(msgid, IPC_RMID, NULL);
  remove(MSG_FILE); 
  exit(-1);
}
int main(int argc, char **argv)
{ 
  int ret = -1;
  long recv_msgtype = 0;
  if(argc !=  2)
  {
    printf("./a.out recv_msgtype\n");
    exit(-1);
  }
  recv_msgtype = atol(argv[1]);
  msgid = creat_or_get_msgque();
  ret = fork();
  if(ret > 0) //发送消息
  {
    signal(SIGINT, signal_fun);
    struct msgbuf msg_buf = {0};
    while(1)
    {
      bzero(&msg_buf, sizeof(msg_buf));
      /* 封装消息包 */
      scanf("%s", msg_buf.mtext);
      printf("input snd_msgtype:\n");
      scanf("%ld", &msg_buf.mtype);
      /* 发送消息包 */
      msgsnd(msgid, &msg_buf, MSG_SIZE, 0); 
    }
  }
  else if(ret == 0)//接收消息
  {
    struct msgbuf msg_buf = {0};
    int ret = 0;
    while(1)
    {
      bzero(&msg_buf, sizeof(msg_buf));
      ret = msgrcv(msgid, &msg_buf, MSG_SIZE, recv_msgtype, 0);
      if(ret > 0) 
      {
        printf("%s\n", msg_buf.mtext);
      } 
    }
  }
  return 0;
}
(a)如何验证消息队列是否被创建成功?
    使用ipcs命令即可查看,可跟接的选项有:
      - a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
      - m:只显示共享内存的信息    
      - q:只显示消息队列的信息
      - s:只显示信号量的信息
(b)system v ipc的缺点
      进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到。
      如何删除? 
      · 方法1:重启OS,很麻烦
      · 方法2:进程结束时,调用相应的API来删除,后面再讲
      · 方法3:使用ipcrm命令删除
        - 删除共享内存
          + M:按照key值删除
            ipcrm -M key
          + m:按照标识符删除
            ipcrm -m msgid
        - 删除消息队列
          + Q:按照key值删除
          + q:按照标识符删除
        - 删除信号量
          + S:按照key值删除
          + s:按照标识符删除
5.2.3.2 msgsnd函数
函数原型
  #include <sys/types.h>
  #include <sys/ipc.h>
  #include <sys/msg.h>
  int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
(a)功能:发送消息到消息队列上。
      说白了就是将消息挂到消息队列上。
(b)返回值
    · 成功:返回0,
    · 失败:返回-1,errno被设置
(c)参数
    · msqid:消息队列的标识符。
    · msgp:存放消息的缓存的地址,类型struct msgbuf类型
        这个缓存就是一个消息包(存放消息的结构体变量)。
      struct msgbuf
      {
            long mtype;         /* 放消息编号,必须 > 0 */
            char mtext[msgsz];  /* 消息内容(消息正文) */
      };        
    · msgsz:消息正文大大小。
    · msgflg:
      - 0:阻塞发送消息
        也就是说,如果没有发送成功的话,该函数会一直阻塞等,直到发送成功为止。
      - IPC_NOWAIT:非阻塞方式发送消息,不管发送成功与否,函数都将返回
          也就是说,发送不成功的的话,函数不会阻塞。
5.2.3.3 msgrcv函数
函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);           
(a)功能:接收消息
      说白了就是从消息队列中取出别人所放的某个编号的消息。
(b)返回值
    成功:返回消息正文的字节数
    失败:返回-1,errno被设置
(c)参数 
    · msqid:消息队列的标识符。
    · msgp:缓存地址,缓存用于存放所接收的消息
      类型还是struct msgbuf:
      struct msgbuf
      {
            long mtype;         /* 存放消息编号*/
            char mtext[msgsz];  /*存放 消息正文内容 */
      };          
    · msgsz:消息正文的大小
    · msgtyp:你要接收消息的编号
    · int msgflg:
      - 0:阻塞接收消息
        也就是说如果没有消息时,接收回阻塞(休眠)。
      - IPC_NOWAIT:非阻塞接收消息
        也就是说没有消息时,该函数不阻塞
5.2.3.4 进程结束时,自动删除消息队列
我们需要调用msgctl函数来实现。
msgctl函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
(a)功能
    ctl就是控制contrl的意思,从这个名字我们就能猜出,这个函数的功能是根据cmd指定的要求,
  去控制消息队列,比如进行哪些控制呢?
  · 获取消息队列的属性信息
  · 修改消息队列的属性信息
  · 删除消息队列
  · 等等
    我们调用msgctl函数的最常见目的就是删除消息队列,事实上,删除消息队列只是各种消息队列
  控制中的一种。
(b)参数
  int msgctl(int msqid, int cmd, struct msqid_ds *buf);
· msqid:消息队列标识符
· cmd:控制选项,其实cmd有很多选项,我这里只简单介绍三个
    - IPC_STAT:将msqid消息队列的属性信息,读到第三个参数所指定的缓存。
    - IPC_SET:使用第三个参数中的新设置去修改消息队列的属性
        + 定一个struct msqid_ds buf。
        + 将新的属性信息设置到buf中
        + cmd指定为IPC_SET后,msgctl函数就会使用buf中的新属性去修改消息队列原有的属性。
    - IPC_RMID:删除消息队列
        删除消息队列时,用不到第三个参数,用不到时设置为NULL。
    - ... :略
· buf:存放属性信息
  有的时候需要给第三个参数,有时不需要,取决于cmd的设置。
  buf的类型为struct msqid_ds,有关这个结构体类型,这里这里只进行简单了解。
  结构体中的成员都是用来存放消息队列的属性信息的。
struct msqid_ds 
{
    struct ipc_perm  msg_perm; /* 消息队列的读写权限和所有者 */
    time_t  msg_stime;    /* 最后一次向队列发送消息的时间*/
    time_t  msg_rtime;    /* 最后一次从消息队列接收消息的时间 */
    time_t  msg_ctime;    /* 消息队列属性最后一次被修改的时间 */
    unsigned  long __msg_cbytes; /* 队列中当前所有消息总的字节数 */
    msgqnum_t  msg_qnum;     /* 队列中当前消息的条数*/
    msglen_t msg_qbytes;  /* 队列中允许的最大的总的字节数 */
    pid_t  msg_lspid;     /* 最后一次向队列发送消息的进程PID */
    pid_t  msg_lrpid;     /* 最后一次从队列接受消息的进程PID */
};
struct ipc_perm 
{
  key_t          __key;       /* Key supplied to msgget(2):消息队列的key值 */
  uid_t          uid;         /* UID of owner :当前这一刻正在使用消息队列的用户 */
  gid_t          gid;         /* GID of owner :正在使用的用户所在用户组 */
  uid_t          cuid;        /* UID of creator :创建消息队列的用户 */
  gid_t          cgid;        /* GID of creator :创建消息队列的用户所在用户组*/
  unsigned short mode;        /* Permissions:读写权限(比如0664) */
  unsigned short __seq;       /* Sequence number :序列号,保障消息队列ID不被立即
                                  重复使用 */
};

5.2.4 什么时候合适使用消息队列

实际上消息队列这种通信方式,使用起来还是蛮方便的,因为不管是两个进程之间的通信,还是n多个进
程的网状交叉通信,消息队列都能搞定,完全可以替代前面讲的管道,
  特别是当你的程序必须涉及到多进程网状交叉通信时,消息队列是上上之选。

5.2.5 消息队列的缺点

与管道一样,不能实现大规模数据的通信,大规模数据的通信,必须使用后面讲的“共享内存”来实现。

5.3 共享内存

共享内存的API与消息队列的API非常相似,应该System V IPC的API都是差不多的,所以只要大家把
前面的消息队列拎清楚了,大家学习本小节的共享内存和之后的信号量时,你会觉非常的容易。
  共享内存就是OS在物理内存中开辟一大段缓存空间,不过与管道、消息队列调用read、write、
msgsnd、msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的。
  当然不管使用那种方式,只要能够共享操作同一段缓存,就都可以实现进程间的通信。
  不过如果直接使用地址来读写缓存时,效率会更高,但是如果是调用API来读写的话,中间必须经过重重
的OS函数调用之后,直到调用到最后一个函数时,该函数才会通过地址去读写共享的缓存,中间的调用过程
会降低效率。
  对于小数据量的通信来说,使用管道和消息队列这种使用API读写的通信方式很合适,但是如果进程涉及到
超大量的数据通信时,必须使用“共享内存”这种直接使用地址操作的通信方式,如果使用API来读写的话,
效率会非常的低。

5.3.1 共享内存的原理

共享内存的实现原理很简单,进程空间不是没有交集吗,让他们的空间有交集不就行了吗。
  以两个进程使用共享内存来通信为例,实现的方法就是:
(1)调用API,让OS在物理内存上开辟出一大段缓存空间。
(2)让各自进程空间与开辟出的缓存空间建立映射关系
  就让虚拟地址和物理内存的实际物理地址建立一对一的对应关系,使用虚拟地址读写缓存时,虚拟
地址最终是要转为物理地址的,转换时就必须参考这个映射关系。
  总之建立映射关系后,每个进程都可以通过映射后的虚拟地址来共享操作实现通信了。

图:

多个进程能不能映射到同一片空间,然后数据共享呢?
  答:当然是可以的
  不过当多个进程映射并共享同一个空间时,在写数据的时候可能会出现相互干扰,
    比如A进程的数据刚写了一半没写完,结果切换到B进程后,B进程又开始写,A的数据就被中间B的
  数据给岔开了这时往往需要加保护措施,让每个进程在没有操作时不要被别人干扰,等操作完以后,
  别的进程才能写数据。比如可以使用信号或者信号量来解决这种同步的问题。

5.3.2 共享内存的使用步骤

(1)进程调用shmget函数创建新的或获取已有共享内存
      shm是share memory的缩写。
(2)进程调用shmat函数,将物理内存映射到自己的进程空间
      说白了就是让虚拟地址和真实物理地址建议一一对应的映射关系。
      建立映射后,就可以直接使用虚拟地址来读写共享的内存空间了。
(3)shmdt函数,取消映射
(4)调用shmctl函数释放开辟的那片物理内存空间
      和消息队列的msgctl的功能是一样的,只不过这个是共享内存的。
    多个进程使用共享内存通信时,创建者只需要一个,同样的,一般都是谁先运行谁创建,其它后运行的
  进程发现已经被创建好了,就直接获取共享使用,大家共享操作同一个内存,即可实现通信。

5.3.2 共享内存的函数

5.3.2.1 shmget函数
5.3.2.1.1 函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
1)功能:创建新的,或者获取已有的共享内        
  · 如果key值没有对应任何共享内存
      创建一个新的共享内存,创建的过程其实就是os在物理内存上划出(开辟出)一段物理内存
    空间出来。
  · 如果key值有对应某一个共享内存
      说明之前有进程调用msgget函数,使用该key去创建了某个共享内存,既然别人之前就创
    建好了,那就直接获取key所对应的共享内存。
2)返回值
  (a)成功:返回共享内存的标识符,以后续操作
  (b)失败:返回-1,并且errno被设置。
    int shmget(key_t key, size_t size, int shmflg);
3)参数
  (a)key:用于生成共享内存的标识符
      可以有三种设置:
      · IPC_PRIVATE:指定这个后,每次调用shmget时都会创建一个新共享内存。
      · 自己指定一个长整型数
      · 使用ftok函数,通过路径名和一个8位的整形数来生成key值
  (b)size:指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍
      一般来说虚拟页大小是4k(4096字节),如果你指定的大小不是虚拟页的整数倍,也会自动
    帮你补成整数倍。
  (c)semflg:与消息队列一样
        指定原始权限和IPC_CREAT,比如0664|IPC_CREAT。
        只有在创建一个新的共享内存时才会用到,否者不会用到。
5.3.2.1.2 代码演示

共享内存写数据代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void *shmaddr = NULL; 
void print_err(char *estr)
{
  perror(estr); 
  exit(-1);
}
void create_or_get_shm(void)
{
  int fd = 0;
  key_t key = -1; 
  fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
  if(fd == -1) print_err("open fail");
  key = ftok(SHM_FILE, 'b');
  if(key == -1) print_err("ftok fail");
  shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
  if(shmid == -1) print_err("shmget fail");
  //write(fd, &shmid, sizeof(shmid));
}
char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};
void signal_fun(int signo)
{
  shmdt(shmaddr);
  shmctl(shmid, IPC_RMID, NULL);
  remove("./fifo");
  remove(SHM_FILE);
  exit(-1); 
}
int get_peer_PID(void)
{
  int ret = -1;
  int fifofd = -1;
  /* 创建有名管道文件 */
        ret = mkfifo("./fifo", 0664); 
        if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
  /* 以只读方式打开管道 */
  fifofd = open("./fifo", O_RDONLY);
        if(fifofd == -1) print_err("open fifo fail");
  /* 读管道,获取“读共享内存进程”的PID */
        int peer_pid;
        ret = read(fifofd, &peer_pid, sizeof(peer_pid));
        if(ret == -1) print_err("read fifo fail");
  return peer_pid; 
}
int main(void)
{
  int peer_pid = -1;
  /* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
  signal(SIGINT, signal_fun);
  /* 使用有名管道获取读共享内存进程的PID */
  peer_pid = get_peer_PID();
  /* 创建、或者获取共享内存 */
  create_or_get_shm();
  //建立映射
  shmaddr = shmat(shmid, NULL, 0);
  if(shmaddr == (void *)-1) print_err("shmat fail");  
  while(1)
  {
    memcpy(shmaddr, buf, sizeof(buf));
    //保证写完后再读数据,当共享内存没有数据时,读进程休眠,当写进程把数据写完后,
    //将读进程唤醒。
    kill(peer_pid, SIGUSR1);
    sleep(1);
  }
  return 0;
}

共享内存读数据代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void *shmaddr = NULL; 
void print_err(char *estr)
{
  perror(estr); 
  exit(-1);
}
void create_or_get_shm(void)
{
  int fd = 0;
  key_t key = -1; 
  fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
  if(fd == -1) print_err("open fail");
  key = ftok(SHM_FILE, 'b');
  if(key == -1) print_err("ftok fail");
  shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
  if(shmid == -1) print_err("shmget fail");
  //read(fd, &shmid, sizeof(shmid));
}
void signal_fun(int signo)
{
  if(SIGINT == signo)
  {
    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, NULL);
    remove("./fifo");
    remove(SHM_FILE);
    exit(-1);
  }
  else if(SIGUSR1 == signo)
  {
  }
}
void snd_self_PID(void)
{
  int ret = -1;
  int fifofd = -1;
  /* 创建有名管道文件 */
  mkfifo("./fifo", 0664); 
  if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
  /* 以只写方式打开文件 */
  fifofd = open("./fifo", O_WRONLY);
  if(fifofd == -1) print_err("open fifo fail");
  /* 获取当前进程的PID, 使用有名管道发送给写共享内存的进程 */
  int pid = getpid();
  ret = write(fifofd, &pid, sizeof(pid));//发送PID
  if(ret == -1) print_err("write fifo fail");
}
int main(void)
{
  /*给SIGUSR1注册一个空捕获函数,用于唤醒pause()函数 */
  signal(SIGUSR1, signal_fun);
  signal(SIGINT, signal_fun);
  /* 使用有名管道,讲当前进程的PID发送给写共享内存的进程 */
  snd_self_PID(); 
  /* 创建、或者获取共享内存 */
  create_or_get_shm();
  //建立映射
  shmaddr = shmat(shmid, NULL, 0);
  if(shmaddr == (void *)-1) print_err("shmat fail");  
  while(1)
  {
    //无数据休眠,有数据唤醒
    pause();
    printf("%s\n", (char *)shmaddr);
    bzero(shmaddr, SHM_SIZE);
  }
  return 0;
}

说明:

(a)使用ipcs命令即可查看创建的共享内存:
  - a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
  - m:只显示共享内存的信息    
  - q:只显示消息队列的信息
  - s:只显示信号量的信息
(b)共享内存的删除
  进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到。
  如何删除? 
  · 方法1:重启OS,很麻烦
  · 方法2:进程结束时,调用相应的API来删除,后面再讲
  · 方法3:使用ipcrm命令删除
    - 删除共享内存
      + M:按照key值删除
        ipcrm -M key
      + m:按照标识符删除
        ipcrm -m msgid
    - 删除消息队列
      + Q:按照key值删除
      + q:按照标识符删除
    - 删除信号量
      + S:按照key值删除
      + s:按照标识符删除
5.3.2.2 shmat
(1)函数原型
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
1)功能
    将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回影射后的起始
  地址(虚拟地址)。
    有了这个地址后,就可以通过这个地址对共享内存进行读写操作。
2)参数
  (a)shmid:共享内存标识符。
  (b)shmaddr:指定映射的起始地址
        有两种设置方式
      · 自己指定映射的起始地址(虚拟地址)。
        我们一般不会这么做,因为我们自己都搞不清哪些虚拟地址被用了,哪些没被用。
      · NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。
          这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址
        可用,哪些不可用。
    void *shmat(int shmid, const void *shmaddr, int shmflg);
  (c)shmflg:指定映射条件。
      · 0:以可读可写的方式映射共享内存
          也就是说映射后,可以读、也可以写共享内存。
      · SHM_RDONLY:以只读方式映射共享内存
          也就是说映射后,只能读共享内存,不能写。
3)返回值
  (a)成功:则返回映射地址
  (b)失败:返回(void *)-1,并且errno被设置。
5.3.2.3 shmdt函数
(1)函数原型
  #include <sys/types.h>
  #include <sys/shm.h>
  int shmdt(const void *shmaddr); 
1)功能:取消建立的映射。
2)返回值:调用成功返回0,失败返回-1,且errno被设置。
3)参数
  shmaddr:映射的起始地址(虚拟地址)。
5.3.2.4 shmctl函数
(1)函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
(a)功能:根据cmd的要求,对共享内存进行相应控制。
        比如:
      · 获取共享内存的属性信息
      · 修改共享内存的属性信息
      · 删除共享内存
      · 等等
        删除共享内存是最常见的控制。
(b)参数
· shmid:标识符。
· cmd:控制选项
  - IPC_STAT:从内核获取共享内存属性信息到第三个参数(应用缓存)。
  - IPC_SET:修改共享内存的属性。
      修改方法与消息队列相同。
  - IPC_RMID:删除共享内存,不过前提是只有当所有的映射取消后,才能删除共享内存。
      删除时,用不着第三个参数,所以设置为NULL
· buf
  buf的类型为struct shmid_ds。
- cmd为IPC_STAT时
  buf用于存储原有的共享内存属性,以供查看。
- cmd为IPC_SET时
  buf中放的是新的属性设置,用于修改共享内存的属性。
- struct shmid_ds结构体
struct shmid_ds 
{
  struct ipc_perm shm_perm;    /* Ownership and permissions:权限 */
  size_t shm_segsz;   /* Size of segment (bytes):共享内存大小 */
  time_t shm_atime;   /* Last attach time:最后一次映射的时间 */
  time_t shm_dtime;   /* Last detach time:最后一次取消映射的时间 */
  time_t shm_ctime;   /* Last change time:最后一次修改属性信息的时间 */
  pid_t shm_cpid;    /* PID of creator:创建进程的PID */
  pid_t shm_lpid;    /* PID of last shmat(2)/shmdt(2) :当前正在使用进程的PID*/
  shmatt_t shm_nattch;  /* No. of current attaches:映射数量,
                         * 标记有多少个进程空间映射到了共享内存上
                         * 每增加一个映射就+1,每取消一个映射就-1 */ 
  ...
};
struct ipc_perm,这个结构体我们在讲消息队列时已经讲过,这里不再重复讲。
struct ipc_perm 
{
   key_t          __key;    /* Key supplied to shmget(2) */
   uid_t          uid;      /* UID of owner */
   gid_t          gid;      /* GID of owner */
   uid_t          cuid;     /* UID of creator */
   gid_t          cgid;     /* GID of creator */
   unsigned short mode;     /* Permissions + SHM_DEST andSHM_LOCKED flags */
   unsigned short __seq;    /* Sequence number */
};
  4)返回值
      调用成功0,失败则返回-1,并且errno被设置。

5.4 system V IPC 之 信号量(或信号灯)semaphore

5.4.1 信号量的作用

当多个进程/线程进行共享操作时,用于资源保护,以防止出现相互干扰的情况。
再间简洁一点,信号量用于“资源的保护“。
(1)进程信号量
    实现的是进程所操作资源的保护。
(2)线程信号量
    实现的是线程所操作资源的保护。
5.4.1.1 什么是进程资源保护
我们讲的是进程的资源保护,实际上线程的资源保护也是类似的原理。
为了更直观的讲解,我们直接通过例子来介绍什么是“进程资源保护”。
(1)例子1:多进程操作共享内存
      比如,多个进程同时向共享内存里面写数据时,可能会出现数据相互干扰的情况。
      比如,某个进程写数据操作还没有写完时,进程的时间片就到了,然后被切换到另一个写
    “共享内存”的进程上运行,这个进程会接着往共享内存里面写数据,此时显然就把第一个进程写
    的数据给隔断,这就形成了数据相互干扰。
      如果只是普通数据的话无所谓,但是如果是很重要的数据的话,这种干扰是无法接受。
(2)例子2:多进程操作文件
    比如当多个进程同时共享向文件里面写数据时,同样会出现和共享写“共享内存”相同的情况
为了避免出现以上所说的相互干扰的问题,就需要加入资源保护的措施,保护的目的就是,保证每个
进程在没有把数据读、写完整之前,其它进程不能进行读、写操作,以防止干扰别人。
疑问:资源保护,这个“资源”到底指的是谁?
答:这个资源指的就是你操作的数据,保护的目的就是不要出现相互干扰,导致紊乱和错误数据的产生。
5.4.1.2 资源保护操作的种类
资源保护的操作分两种,一种叫互斥,另一个种叫同步。
(1)互斥
    对于互斥操作来说,多进程共享操作时,多个进程间不关心谁先操作、谁后操作的先后顺序问题,
  它们只关心一件事,那就是我在操作时别人不能操作。
    就算当前正在操作的进程它的时间片到了,切换到了其它进程上,但是当该进程检测到上一个进程
  还没有操作完时,该进程在当前的时间片内会休眠,直到再次切换会上一个进程,将操作完成后再切
  换回来,此时才能进行操作。
    这跟上厕所时把门关起来是一样的,我在蹲坑时你不能蹲,你在蹲坑时我不能蹲,这就是互斥,至于
  蹲坑先后顺序并没有要求。
(2)同步
    同步其实本身就包含了互斥,不过同步不仅仅只互斥,同步对于谁先操作、谁后操作的先后顺序有
  要求,比如规定A进程先写,然后是B进程写,然后是C进程写,绝对不能出现这操作顺序以外的顺序。
    所以所谓同步就是,多个共享操作时,进程必须要有统一操作的步调,按照一定的顺序来操作。  
  疑问:同步有意义吗?
  答:我们讲共享内存时,例子代码不就需要同步吗。
(3)实现同步、互斥,其实就是加锁
  这个很形象,我要操作我就上把锁,我上锁的过程中你就不能操作,直到我把锁打开了,你才能操作,
你操作时也会加锁,加锁后我就不能操作了。
  所以说信号量就是一个加锁机制,通过加锁来实现同步和互斥。
  说到加锁,我们讲到后面“高级IO”时,我们还会讲到“文件锁”这个东西,顾名思义,文件锁就是专门用
来给文件时上锁的,讲到时在详细介绍。
  其实,不管是进程还是线程,都存在同步和互斥的问题,同步和互斥的目的其实就是为了实现“资源”的
保护,不要让数据(资源)出现紊乱。
(4)疑问:信号量既然是一种加锁机制,为什么进程信号量会被归到了进程间通信里面呢?
    资源保护时,某个进程的操作没有完全完成之前,别人是不能操作的,那么进程间必须相互知道
  对方的操作状态,必须会涉及到通信过程。
  所以信号量实现资源保护的本质就是,通过通信让各个进程了解到操作状态,然后查看自己能不能操作。

5.4.2 使用信号量实现互斥

进程信号量既能实现进程的互斥,也能实现进程的同步,不过有些“资源保护机制”就只能实现互斥,
而不能实现同步。
  虽然我们这里主要是讲互斥,但是也会捎带的提到同步,为后面讲同步打基础。
5.4.2.1 需要互斥实现“资源保护”的例子

semaphore.h:

#ifndef H_SEM_H
#define H_SEM_H
extern void print_err(char *estr);
extern int creat_or_get_sem(int nsems);
extern void init_sem(int semid, int semnum, int val);
extern void del_sem(int semid, int nsems);
extern void p_sem(int semid, int semnum_buf[], int nsops);
extern void v_sem(int semid, int semnum_buf[], int nsops);
#endif

semaphore.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun 
{
  int val;   
  struct semid_ds *buf;    
  unsigned short  *array;  /* 不做要求 */
  struct seminfo  *__buf;  /* 不做要求 */
};
#define SEM_FILE "./semfile"
void print_err(char *estr)
{
        perror(estr);
        //exit(-1);
}
int creat_or_get_sem(int nsems)
{
  int semid;
  int fd = -1;
  key_t key = -1;
  fd = open(SEM_FILE, O_RDWR|O_CREAT, 0664);
  if(fd == -1) print_err("open ./semfile fail");
  key = ftok(SEM_FILE, 'a');  
  if(key == -1) print_err("ftok fail");
  semid = semget(key, nsems, 0664|IPC_CREAT);
  if(semid == -1) print_err("semget fail");
  return semid; 
}
void init_sem(int semid, int semnum, int val)
{
  int ret = -1;
  union semun sem_un;
  /* semnum:信号量编号
   * SETVAL:设置信号量初始值cmd
   * sem_un:初始值
   */ 
  sem_un.val = val;
  ret = semctl(semid, semnum, SETVAL, sem_un);
  if(ret == -1) print_err("semctl fail");
}
void del_sem(int semid, int nsems)
{
  int ret = 0;
  int i = 0;
  for(i=0; i<nsems; i++)
  { 
    ret = semctl(semid, i, IPC_RMID);
    if(ret == -1) print_err("semctl del sem fail");
  }
  remove(SEM_FILE);
}
void p_sem(int semid, int semnum_buf[], int nsops)
{
  int i = 0;
  int ret = -1;
  struct sembuf sops[nsops];
  for(i=0; i<nsops; i++)
  {
    sops[i].sem_num = semnum_buf[i];//信号量编号
    sops[i].sem_op  = -1;//-1 p操作
    sops[i].sem_flg = SEM_UNDO;//防止死锁 
  }
  ret = semop(semid, sops, nsops);
  if(ret == -1) print_err("semop p fail");
}
void v_sem(int semid, int semnum_buf[], int nsops)
{
  int i = 0;
  int ret = -1;
  struct sembuf sops[nsops];
  for(i=0; i<nsops; i++)
  {
    sops[i].sem_num = semnum_buf[i];//信号量编号
    sops[i].sem_op  = 1;//+1 v操作
    sops[i].sem_flg = SEM_UNDO;//防止死锁 
  }
  ret = semop(semid, sops, nsops);
  if(ret == -1) print_err("semop p fail");
}

share_write_file.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include "semaphore.h"
#define NSEMS 1
int semid;
void signal_fun(int signo)
{
  del_sem(semid, NSEMS);
  exit(-1);
}
int main(void)
{
  int i = 0;
  int ret = 0;
  int fd = -1;
  int semnum_buf[1] = {0};
  fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
  if(fd == -1) print_err("open file fail");
  semid = creat_or_get_sem(NSEMS);
  for(i=0; i<NSEMS; i++)
  {
    init_sem(semid, i, 1);
  }
  ret = fork();
  if(ret > 0)
  {
    signal(SIGINT, signal_fun);
    while(1)
    {
      semnum_buf[0] = 0;//设置要操作的信号量的编号
      p_sem(semid, semnum_buf, 1); //P操作
      write(fd, "hello ", 6);
      write(fd, "world\n", 6);
      semnum_buf[0] = 0; //设置要操作的信号量的编号
      v_sem(semid, semnum_buf, 1);//v操作
    }   
  }
  else if(ret == 0)
  {
    while(1)
    {
      semnum_buf[0] = 0;//设置要操作的信号量的编号
      p_sem(semid, semnum_buf, 1); //P操作
      write(fd, "hhhhh ", 6);
      write(fd, "wwwww\n", 6);
      semnum_buf[0] = 0; //设置要操作的信号量的编号
      v_sem(semid, semnum_buf, 1);//v操作
    }   
  }
  return 0;
}
5.4.2.2 进程信号量实现互斥的原理
(1)什么是进程信号量
  简单理解的话,信号量其实是OS创建的一个共享变量,进程在进行操作之前,会先检查这个变量的值,
这变量的值就是一个标记,通过这个标记就可以知道可不可以操作,以实现互斥。
(2)多值信号量和二值信号量
  1)二值信号量
      同步和互斥时使用的都是二值信号量。
      二值信号量的值就两个,0和1,0表示不可以操作,1表示可以操作。
    通过对变量进行0、1标记,就可以防止出现相互干扰情况。
  2)多值信号量
      信号量的最大值>1,比如为3的话,信号量允许的值为0、1、2、3。
      多值信号量用的不是很多,所这里只简单的提一下。
(3)信号量集合
      我们说信号量其实是一个OS创建的,供相关进程共享的int变量,只不过我们在调用相关API
    创建信号量时,我们创建的都是一个信号量集合,所谓集合就是可能会包含好多个信号量。
      用于互斥时,集合中只包含一个信号量。
      用于同步时,集合中会包含多个信号量,至于多少个,需要看情况。
(4)信号量的使用步骤
    1)进程调用semget函数创建新的信号量集合,或者获取已有的信号量集合。
    2)调用semctl函数给集合中的每个信号量设置初始值
    3)调用semop函数,对集合中的信号量进行pv操作
        什么是pv操作?
          pv操作其实说白了就是加锁、解锁操作。
        (a)P操作(加锁):对信号量的值进行-1,如果信号量的值为0,p操作就会阻塞
        (b)V操作(解锁):对信号量的值进行+1,V操作不存在阻塞的问题
        总之通过pv操作(加锁、解锁),就能够实现互斥,以防止出现干扰。
        正如我们前面总结的,加锁、解锁就跟上厕所蹲坑把门栓起来,完事了再把门打开是
      一样的,上厕所时通过门栓的加锁和解锁,就实现了上厕所蹲坑的互斥,防止上厕所的相互
      干扰。
    4)调用semctl删除信号量集合

5.4.3 信号量相关的API

5.4.3.1 semget函数
1)函数原型
  #include <sys/types.h>
  #include <sys/ipc.h>
  #include <sys/sem.h>
  int semget(key_t key, int nsems, int semflg);
  sem就是semaphore的缩写。
(a)功能:根据key值创建新的、或者获取已有的信号量集合,并返回其标识符。
    · 实现互斥时:集合中只需要一个信号量
    · 实现同步时:集合中需要多个信号量  
(b)参数
    ·  key:设置同消息队列和共享内存。
      一般都使用ftok获取key值。
    · nsems:指定集合中信号量的个数。
      用于互斥时,数量都指定为1,因为只需要一个信号量。
      如果是同步的话就需要至多为多个,至于到底是多少个,讲到同步时再说。
    · semflg:设置同消息队列和共享内存。
      一般都设置为0664|IPC_CREAT。
(c)返回值:调用成功则返回信号量集合的标识符,失败则返回-1,并且errno被设置。
5.4.3.2 semctl函数
1)函数原型
  #include <sys/types.h>
  #include <sys/ipc.h>
  #include <sys/sem.h>
  int semctl(int semid, int semnum, int cmd, ...);
(a)功能
      根据cmd的要求对集合中的各个信号量进行控制,...表示它是一个变参函数,如果第四
    个参数用不到的话,可以省略不写。
(b)返回值:调用成功返回非-1值,失败则返回-1,errno被设置。
(c)参数说明
· semid:信号量标识符。
  通过标识符就能找到信号量集合。
· semnum:集合中某个信号量的编号。
  信号量的编号为非负整数,而且是自动从0开始编号的。
  通过信号量编号就能找到集合中对应信号量,然后对这个具体的信号量进行控制操作。
int semctl(int semid, int semnum, int cmd, ...);
· cmd:控制选项。
  - IPC_STAT:将信号量的属性信息从内核读到第四个参数所以指定的
            struct semid_ds缓存中。
  - IPC_SET:修改属性信息,此时也会用到struct semid_ds结构体变量
      具体的修改方法同消息队列和共享内存。
  - IPC_RMID:删除信号量集合时,并不需要把所有的信号量都删除掉后才能删除,只需要指定semid
      和IPC_RMID就可以不把整个信号量集合删除,其中第二个参数semnum没有被用到,所以
      semnum的值可以随便写,不过我们一般都是把它写为0。
      比如:semctl(semid, 0, IPC_RMID); 
  int semctl(int semid, int semnum, int cmd, ...);
  - SETVAL:通过第四个参数,给集合中semnu编号的信号量设置一个int初始值。
      在前面就说过,如果是二值信号量的话,设置初始值要么是0,要么是1,如果信号量的
    目的是互斥的话,基本都是设置为1。
      当设置为1后,多几个进程互斥操作时,那就是谁先运行就谁先操作。
      如果是同步的话,初值是1还是0,这要就要看具体的情况了。
  - 其它选项:省略
    其中信号量的IPC_STAT、IPC_SET、IPC_RMID与消息队列和共享内存的
  IPC_STAT、IPC_SET、IPC_RMID是一样的。
    但是SETVAL确属于进程信号量所独有的选项。
  对于信号量来说,IPC_RMID、SETVAL是最常用的两个选项。
int semctl(int semid, int semnum, int cmd, ...);
· ...
  ...表示,如果用不到时可以省略不写。
  通过前面cmd的介绍我们可以看出,第四个参数具体设置为什么其实是不一定的,比如
  - cmd为IPC_STAT:第四个参数应为struct semid_ds类型的缓存。
          有关struct semid_ds结构体我们不再介绍,因为与共享内存的
          struct shmid_ds,以及消息队列的struct msqid_ds结构体是类似的。
  - cmd为SETVAL:第四个参数应该设置为一个int的值,用于初始化信号量。
    从以上可以看出,第四个参数对应内容是变着的,为了应对这种变化就用到了一个联合体。
    union semun {
       int              val;    
       struct semid_ds *buf;    
       unsigned short  *array;  /* 不做要求 */
       struct seminfo  *__buf;  /* 不做要求 */
    };
      这个联合体类型并没有被定义在信号量相关的系统头文件中,我们使用这个联合体时,
    我们需要自己定义这个类型,至于联合体类型名可以自己定,不过一般都是直接沿用
    semun这个名字。
      成员:
      val:存放用于初始化信号量的值
      buf:存放struct semid_ds结构体变量的地址
      有关联合体的详细讲解,请看《C深度解析课》这门课,联合体的介绍属于基础
    课程的内容,所以我们这里不再赘述。
    疑问:这个联合怎么用?
    + 例1:当需要指定struct semid_ds缓存时
      union semun sem_un; //定义一个联合体变量
      struct semid_ds buff; //定义一个struct semid_ds缓存
      sem_un.buf = &buff;  //现在整个联合体的值就是buf中缩放的buff的地址  
      semctl(semid, 0, IPC_STAT, sem_un); //这里将联合体传递给semctl函数,
            //其实就是将buff的地址传递给了semctl函数
    + 例2:当需要指定信号量的int初始值时
      union semun sem_un; 
      sem_un.val = 1;  //现在整个联合体的值就是1 
      semctl(semid, 0, IPC_STAT, sem_un); 
5.4.3.3 semop函数
1)函数原型
  #include <sys/types.h>
  #include <sys/ipc.h>
  #include <sys/sem.h>
  int semop(int semid, struct sembuf *sops, unsigned nsops);
  op是operate操作的意思。
(a)功能:对指定的信号量进行p操作、或者是v操作。
  · p操作:将信号量的值-1
    当信号量的值为0时,p操作默认是阻塞的。
  · v操作:将信号量的值+1
    v操作不存在阻塞的问题。
      对于二值信号量来说,v操作后,值就从0变为了1,这就表示我操作完了,其它进程运行时
    就可以进行p操作了。
(b)返回值:调用成功返回0,失败则返回-1,errno被设置。
  int semop(int semid, struct sembuf *sops, unsigned nsops);
(c)参数
· semid:信号量集合的标识符。
· sops:这个参数更好理解的写法是struct sembuf sops[],
    第三个参数nsops就是用于指定数组元素个数的。
    每一个数组成员对应一个信号量,每一个元素都是一个struct sembuf结构体变量,内部成员的
  决定着:
    - 你要对集合中哪一个信号量进行操作
    - 要进行的是p操作呢,还是v操作
  - 结构体成员
    struct sembuf
    {
      unsigned short sem_num;  
      short          sem_op;
      short          sem_flg;  
    }
      这个结构体不需要我们自己定义,因为在semop的头文件中已经定义了。
      如果你无法判断这个结构体是否需要我们自己定义,那你就不要定义,如果编译提示这
    个结构体类型不存在,就说明需要自己定义,编译通过就说明在系统头文件中早就定义好了。
    + sem_num:信号量编号,决定对集合中哪一个信号量进行pv操作
    + sem_op:设置为-1,表示想-1进行p操作,设置1表示想+1进行v操作
    + sem_flg:
      · IPC_NOWAIT: 
          一般情况下,当信号量的值为0时进行p操作的话,semop的p操作会阻塞。
          如果你不想阻塞的话,可以指定这个选项,NOWAIT就是不阻塞的意思。
            不过除非某些特殊情况,否则我们不需要设置为非阻塞。
      · SEM_UNDO:防止死锁
          还是以二值信号量为例,当进程在v操作之前就结束时,信号量的值就会一直保持
        为0,那么其它进程将永远无法p操作成功,会使得进程永远休眠下去,这造成就是死锁。
          但是设置了SEM_UNDO选项后,如果进程在结束时没有V操作的话,OS会自动帮忙
        V操作,防止死锁。

5.4.4 使用信号量实现同步

5.4.4.1 什么是同步
让多个进程按照固定的步调做事,我们前面就说过,同步本身就是互斥的。
  实现同步时,同步的进程可以是亲缘进程,也可以是非亲缘进程。
5.4.4.2 同步举例1
通过同步让三个亲缘进程按照顺序打印出111111、222222、333333。

代码演示:

semaphore.h:

#ifndef H_SEM_H
#define H_SEM_H
extern void print_err(char *estr);
extern int creat_or_get_sem(int nsems);
extern void init_sem(int semid, int semnum, int val);
extern void del_sem(int semid, int nsems);
extern void p_sem(int semid, int semnum_buf[], int nsops);
extern void v_sem(int semid, int semnum_buf[], int nsops);
#endif

semaphore.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun 
{
  int val;   
  struct semid_ds *buf;    
  unsigned short  *array;  /* 不做要求 */
  struct seminfo  *__buf;  /* 不做要求 */
};
#define SEM_FILE "./semfile"
void print_err(char *estr)
{
        perror(estr);
        //exit(-1);
}
int creat_or_get_sem(int nsems)
{
  int semid;
  int fd = -1;
  key_t key = -1;
  fd = open(SEM_FILE, O_RDWR|O_CREAT, 0664);
  if(fd == -1) print_err("open ./semfile fail");
  key = ftok(SEM_FILE, 'a');  
  if(key == -1) print_err("ftok fail");
  semid = semget(key, nsems, 0664|IPC_CREAT);
  if(semid == -1) print_err("semget fail");
  return semid; 
}
void init_sem(int semid, int semnum, int val)
{
  int ret = -1;
  union semun sem_un;
  /* semnum:信号量编号
   * SETVAL:设置信号量初始值cmd
   * sem_un:初始值
   */ 
  sem_un.val = val;
  ret = semctl(semid, semnum, SETVAL, sem_un);
  if(ret == -1) print_err("semctl fail");
}
void del_sem(int semid, int nsems)
{
  int ret = 0;
  ret = semctl(semid, 0, IPC_RMID);
  if(ret == -1) print_err("semctl del sem fail");
  remove(SEM_FILE);
}
void p_sem(int semid, int semnum_buf[], int nsops)
{
  int i = 0;
  int ret = -1;
  struct sembuf sops[nsops];
  for(i=0; i<nsops; i++)
  {
    sops[i].sem_num = semnum_buf[i];//信号量编号
    sops[i].sem_op  = -1;//-1 p操作
    sops[i].sem_flg = SEM_UNDO;//防止死锁 
  }
  ret = semop(semid, sops, nsops);
  if(ret == -1) print_err("semop p fail");
}
void v_sem(int semid, int semnum_buf[], int nsops)
{
  int i = 0;
  int ret = -1;
  struct sembuf sops[nsops];
  for(i=0; i<nsops; i++)
  {
    sops[i].sem_num = semnum_buf[i];//信号量编号
    sops[i].sem_op  = 1;//+1 v操作
    sops[i].sem_flg = SEM_UNDO;//防止死锁 
  }
  ret = semop(semid, sops, nsops);
  if(ret == -1) print_err("semop p fail");
}

sync.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include "semaphore.h"
#define NSEMS 3
int semid;
void signal_fun(int signo)
{
  del_sem(semid, NSEMS);
  exit(-1);
}
int main(void)
{
  int i = 0;
  int ret = 0;
  int fd = -1;
  int semnum_buf[1] = {0};
  //创建信号量集合
  semid = creat_or_get_sem(NSEMS);
  //初始化信号量集合中的每个信号量
  for(i=0; i<NSEMS; i++)
  {
    if(i == 0) init_sem(semid, i, 1);
    else init_sem(semid, i, 0);
  }
  ret = fork();
  if(ret > 0)
  {
    ret = fork();
    if(ret > 0) //父进程
    {
      while(1)
      { 
        semnum_buf[0] = 2;
        p_sem(semid, semnum_buf, 1);
        printf("333333\n");
        sleep(1);
        semnum_buf[0] = 0;
        v_sem(semid, semnum_buf, 1);
      }   
    }
    else if(ret == 0) //子进程2
      {
      while(1)
      {
        semnum_buf[0] = 1;
        p_sem(semid, semnum_buf, 1);
        printf("222222\n");
        sleep(1);
        semnum_buf[0] = 2;
        v_sem(semid, semnum_buf, 1);
      }   
    }
  }
  else if(ret == 0)//子进程1
  {
    signal(SIGINT, signal_fun);
    while(1)
    {
      semnum_buf[0] = 0;
      p_sem(semid, semnum_buf, 1);
      printf("111111\n");
      sleep(1);
      semnum_buf[0] = 1;
      v_sem(semid, semnum_buf, 1);
    }   
  }
  return 0;
}
5.4.4.3 同步例子2
使用信号量来解决共享内存的同步问题

semaphore.h:

#ifndef H_SEM_H
#define H_SEM_H
extern int creat_or_get_sem(int nsems);
extern void init_sem(int semid, int semnum, int val);
extern void del_sem(int semid);
extern void p_sem(int semid, int semnum_buf[], int nsops);
extern void v_sem(int semid, int semnum_buf[], int nsops);
#endif

semaphore.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun 
{
  int val;   
  struct semid_ds *buf;    
  unsigned short  *array;  /* 不做要求 */
  struct seminfo  *__buf;  /* 不做要求 */
};
#define SEM_FILE "./semfile"
static void print_err(char *estr)
{
        perror(estr);
        //exit(-1);
}
int creat_or_get_sem(int nsems)
{
  int semid;
  int fd = -1;
  key_t key = -1;
  fd = open(SEM_FILE, O_RDWR|O_CREAT, 0664);
  if(fd == -1) print_err("open ./semfile fail");
  key = ftok(SEM_FILE, 'a');  
  if(key == -1) print_err("ftok fail");
  semid = semget(key, nsems, 0664|IPC_CREAT);
  if(semid == -1) print_err("semget fail");
  return semid; 
}
void init_sem(int semid, int semnum, int val)
{
  int ret = -1;
  union semun sem_un;
  /* semnum:信号量编号
   * SETVAL:设置信号量初始值cmd
   * sem_un:初始值
   */ 
  sem_un.val = val;
  ret = semctl(semid, semnum, SETVAL, sem_un);
  if(ret == -1) print_err("semctl fail");
}
void del_sem(int semid)
{
  int ret = 0;
  ret = semctl(semid, 0, IPC_RMID);
  if(ret == -1) print_err("semctl del sem fail");
  remove(SEM_FILE);
}
void p_sem(int semid, int semnum_buf[], int nsops)
{
  int i = 0;
  int ret = -1;
  struct sembuf sops[nsops];
  for(i=0; i<nsops; i++)
  {
    sops[i].sem_num = semnum_buf[i];//信号量编号
    sops[i].sem_op  = -1;//-1 p操作
    sops[i].sem_flg = SEM_UNDO;//防止死锁 
  }
  ret = semop(semid, sops, nsops);
  if(ret == -1) print_err("semop p fail");
}
void v_sem(int semid, int semnum_buf[], int nsops)
{
  int i = 0;
  int ret = -1;
  struct sembuf sops[nsops];
  for(i=0; i<nsops; i++)
  {
    sops[i].sem_num = semnum_buf[i];//信号量编号
    sops[i].sem_op  = 1;//+1 v操作
    sops[i].sem_flg = SEM_UNDO;//防止死锁 
  }
  ret = semop(semid, sops, nsops);
  if(ret == -1) print_err("semop p fail");
}

shm1.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include "semaphore.h"
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
int semid = -1;
void *shmaddr = NULL; 
static void print_err(char *estr)
{
  perror(estr); 
  exit(-1);
}
void create_or_get_shm(void)
{
  int fd = 0;
  key_t key = -1; 
  fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
  if(fd == -1) print_err("open fail");
  key = ftok(SHM_FILE, 'b');
  if(key == -1) print_err("ftok fail");
  shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
  if(shmid == -1) print_err("shmget fail");
  //write(fd, &shmid, sizeof(shmid));
}
char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};
void signal_fun(int signo)
{
  shmdt(shmaddr);
  shmctl(shmid, IPC_RMID, NULL);
  del_sem(semid);//删除信号量集合  
  remove("./fifo");
  remove(SHM_FILE);
  exit(-1); 
}
int main(void)
{
  int peer_pid = -1;
  /* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
  signal(SIGINT, signal_fun);
  /* 创建、或者获取共享内存 */
  create_or_get_shm();
  //创建信号量集合 
  semid = creat_or_get_sem(2);
  /* 初始化信号量集合 */
  int i = 0;
  for(i=0; i<2; i++)
  {   
    //将编号0的信号量初始化为1,其它初始化为0
    if(i == 0) init_sem(semid, i, 1);
    else init_sem(semid, i, 0);
  }
  //建立映射
  shmaddr = shmat(shmid, NULL, 0);
  if(shmaddr == (void *)-1) print_err("shmat fail");  
  int semnum_buf[1] = {0};//存放信号量的编号
  while(1)
  { 
    //p sem 0
    semnum_buf[0] = 0;
    p_sem(semid, semnum_buf, 1);
    /* 向共享内存写数据 */
    memcpy(shmaddr, buf, sizeof(buf));
    sleep(1);
    //v sem 1
    semnum_buf[0] = 1;
    v_sem(semid, semnum_buf, 1);
  }
  return 0;
}

shm2.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include "semaphore.h"
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
int semid = -1;
void *shmaddr = NULL; 
void print_err(char *estr)
{
  perror(estr); 
  exit(-1);
}
void create_or_get_shm(void)
{
  int fd = 0;
  key_t key = -1; 
  fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
  if(fd == -1) print_err("open fail");
  key = ftok(SHM_FILE, 'b');
  if(key == -1) print_err("ftok fail");
  shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
  if(shmid == -1) print_err("shmget fail");
  //read(fd, &shmid, sizeof(shmid));
}
void signal_fun(int signo)
{
  if(SIGINT == signo)
  {
    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, NULL);
    remove("./fifo");
    remove(SHM_FILE);
    exit(-1);
  }
  else if(SIGUSR1 == signo)
  {
  }
}
int main(void)
{
  signal(SIGINT, signal_fun);
  /* 创建、或者获取共享内存 */
  create_or_get_shm();
  //获取别人创建号的信号量
  semid = creat_or_get_sem(2);
  //建立映射
  shmaddr = shmat(shmid, NULL, 0);
  if(shmaddr == (void *)-1) print_err("shmat fail");  
  int semnum_buf[1] = {0};//存放信号量编号 
  while(1)
  {
    //p sem 1
    semnum_buf[0] = 1;
                p_sem(semid, semnum_buf, 1);
    //从共享内存去除数据并打印显示
    printf("%s\n", (char *)shmaddr);
    bzero(shmaddr, SHM_SIZE);//清空共享内存
    //v sem 0
                semnum_buf[0] = 0;
                v_sem(semid, semnum_buf, 1);
  }
  return 0;
}


目录
相关文章
|
12天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
35 1
|
7天前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
15天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
1月前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
133 4
linux进程管理万字详解!!!
|
20天前
|
缓存 算法 Linux
Linux内核的心脏:深入理解进程调度器
本文探讨了Linux操作系统中至关重要的组成部分——进程调度器。通过分析其工作原理、调度算法以及在不同场景下的表现,揭示它是如何高效管理CPU资源,确保系统响应性和公平性的。本文旨在为读者提供一个清晰的视图,了解在多任务环境下,Linux是如何智能地分配处理器时间给各个进程的。
|
1月前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
71 8
|
27天前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
56 1
|
27天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
1月前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
70 4
|
7月前
|
缓存 Linux 测试技术
安装【银河麒麟V10】linux系统--并挂载镜像
安装【银河麒麟V10】linux系统--并挂载镜像
2039 0