C语言-进程通信

简介

线程与进程对比

      多线程模型主要优势为线程间切换代价较小,因此适用于I/O密集型的工作场景(如上传下载),因为I/O密集型的工作场景经常会由于I/O阻塞导致频繁的切换线程。
      多进程模型的优势是CPU,多进程模型适用于需要频繁的计算场景,比如多机分布式,其实网络编程也可以说是一种跨主机进程通信。

多进程场景

一.多进程场景,进程间需要通信
1.数据传输
   一个进程需要将它的数据发送给另一个进程;
2.资源共享
   多个进程之间共享同样的资源;
3.通知事件
   一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件;
4.进程控制
   有些进程希望完全控制另一个进程的执行(如Debug进程),
   该控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。

基于以上几个原因,特定的环境下我们需要进程间能通信。怎么才能通信?

二.进程间通信的基本原理
      每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信机制(IPC)。
在这里插入图片描述

三.进程间通信的几种常用方式概述

  1. 无名管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,
    而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  2. 有名管道 (named pipe): 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  3. 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。
    消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  4. 信号量( semophore ): 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。
    它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
    因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  5. 共享内存( shared memory ):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
    共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。
    它往往与其他通信机制,如信号,配合使用,来实现进程间的同步和通信。

进程通信方式的详细讲解

1.无名管道(pipe),也可直接叫管道

      这是一种最基本的IPC(Inter-Process Communication,进程间通信)机制,由pipe函数创建:

#include <unistd.h>
int pipe(int pipefd[2]);

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

   调用pipe函数时在内核中开辟一块缓冲区用于通信,它有一个读端,一个写端:

pipefd[0]指向管道的读端,
pipefd[1]指向管道的写端。

   所以管道在用户程序看起来就像一个打开的文件,通过read(pipefd[0])或者write(pipefd[1])向这个文件读写数据,其实是在读写内核缓冲区。

使用管道的通信过程:
1)父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
在这里插入图片描述

2)父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
在这里插入图片描述

3)父进程关闭管道读端,子进程关闭管道写端。或者反过来,父进程关闭管道写端,子进程关闭管道读端。
      父进程可以往管道里写,子进程可以从管道里读,父进程传信息给子进程,反之亦然。
在这里插入图片描述

      上面是让父子进程通信,也可以fork两次,让两个子进程,以类似的方式通信。

这里为什么只能是单向,不能双向呢?
      管道是用环形队列实现的,数据从写端流入从读端流出,假设所有端口开启,父、子进程同时读写数据,那么管道中的信息就会参杂在一块,读出的数据必定不是乱的,无法区分,通信就没有意义了。

管道可能出现的四种特殊情况:

  1. 写端关闭,读端不关闭;
    那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
  2. 写端不关闭,但是也不写数据,读端不关闭;
    此时管道中剩余的数据都被读取之后再次read会被阻塞,直到管道中有数据可读了才重新读取数据并返回;
  3. 读端关闭,写端不关闭;
    此时该进程会收到信号SIGPIPE,通常会导致进程异常终止。
  4. 读端不关闭,但是也不读取数据,写端不关闭;
    此时当写端被写满之后再次write会阻塞,直到管道中有空位置了才会写入数据并重新返回。

使用管道的缺点:

  1. 两个进程通过一个管道只能实现单向通信。
  2. 只能用于具有亲缘关系的进程间通信,例如父子,兄弟进程。

无名管道例子:父进程向子程进发送信息,子进程接收信息

#include <stdlib.h>
#include <stdio.h> 
#include <unistd.h> 
#include <string.h> 
int main(){
    //创建管道
    int _pipe[2];   //0,读端,1,写端
    int ret = pipe(_pipe);
    if(ret!=0){
        printf("管道创建有错!\n");
        return 1;
    }

    pid_t pid = fork();   //创建子进程
    if(pid < 0){
        printf("子进程创建异常!\n");
        return 2;
    }
    if(pid == 0){   //等于0,创建子进程成功
      close(_pipe[1]);   //关闭写端,保留读端
      char _mesg[100];
      int j =0;
      while(j<10){
        memset(_mesg,'\0',sizeof(_mesg));
        read(_pipe[0],_mesg,sizeof(_mesg));
        printf("读到的父进程数据:%s------pid:%d\n",_mesg,pid);
        j++;
      }

    }
    if(pid > 0){
        //父进程
        close(_pipe[0]);   //关闭读端,保留写端,达到向子进程写信息
        //写入数据
        int i=0;
        char * _mesg = NULL;
        while (i<100)
        {
            _mesg = "I am father!";
            write(_pipe[1],_mesg,strlen(_mesg)+1);
            sleep(1);
            i++;
        }
        
    }
    return 0;
}
//编译执行
root@ubuntu:/mnt/hgfs/workspaceC/process/ipc# gcc -g pipe.c -o pipe
root@ubuntu:/mnt/hgfs/workspaceC/process/ipc# ./pipe
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0

2.命名管道(FIFO)

概念
      管道的一个不足之处是没有名字,因此,只能用于具有亲缘关系的进程间通信,在命名管道(named pipe 或FIFO)提出后,该限制得到了克服。
      命名管道不同于管道之处在于它提供一个路径名与之关联,以命名管道的文件形式存储于文件系统中。
      命名管道是一个设备文件,因此,即使某进程与创建命名管道的进程不存在亲缘关系,只要可以访问该路径,就能够通过命名管道相互通信。
   值得注意的是,命名管道总是按照先进先出的原则操作(保留匿名管道的特性),第一个被写入的数据将先从管道中读出。

命名管道的创建与读写
Linux 下有两种方式创建命名管道。
   一是在Shell下交互地建一个命名管道,二是在程序中使用系统函数建命名管道。
   Shell 方式下可使用mknod 或mkfifo命令,下面命令使 mknod 创建了一个命名管道:

mknod namedpipe 

创建命名管道的系统函数有两个:mknod 和mkfifo。
两个函数均定义在头件sys/stat.h ,
函数原型如下:

#include <sys/types.h> 
#include <sys/stat.h> 
int mknod(const char *path,mode_t mod,dev_t dev); 
int mkfifo(const char *path,mode_t mode);  

函数mknod 参数:
      path 为创建的命名管道的全路径名;
      mod 为创建的命名管道的模式和明其存取权限,一般用      S_IFIFO表示要创建一个FIFO文件,0666就可以了;
      dev为设备值,它只在创建设备时才会用到,一般文件用0就可以了。
函数mkfifo两个参数的含义和mknod前两个相同。

mknod 是较老的函数,使用mkfifo函数更加简单和规范,所以建议尽量使用mkfifo而不是mknod 。
这两个函数调用成功都返回0,失败都返回-1。

下面使用 mknod 函数创建了个命名管道:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
//创建一个命名管道‘文件’,它是一个设备文件,linux中该文件有p标志
int main(){
    umask(0);
    if(mknod("/tmp/fifo",S_IFIFO | 0666,0) == -1){
        perror("mkfifo error!\n");
        exit(1);
    }
    umask(0002);
    return 0;

}

//编译与执行后
root@ubuntu:/mnt/hgfs/workspaceC/process/ipc# cd /tmp
root@ubuntu:/tmp# ll

total 84
drwxrwxrwt 18 root     root     4096 May 21 18:42 ./
drwxr-xr-x 20 root     root     4096 Apr  6 10:39 ../
prw-rw-rw-  1 root     root        0 May 21 18:41 fifo|

下面是使用 mkfifo的代例代码:是我们推荐用的

umask(0); 
if  (mkfifo("/tmp/fifo",S_IFIFO | 0666) == -1) { 
    perror("mkfifo error!"); 
    exit(1); 
}
umask(0002); 

   使用命名管道时,必须先调open()将其打开。
因为命名管道是一个存在于硬盘上的文件,匿名管道是存在于内存中的特殊文件。
   需要注意的是,调用open()打开命名管道的进程可能会被阻塞。
但如果同时用读、写方式(O_RDWR)打开,则一定不会导致阻塞;
如果以只读方式(O_RDONLY)打开,则调open()函数的进程将会被阻塞,直到有写方式打开管道;
同样只以写方(O_WRONLY)打开也会阻塞直到有读方式打开管道。

程序实例:
server写端,并创建命名管道文件:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define _PATH_ "/tmp/file.tmp"
#define _SIZE_ 100
int main(){
    int ret = mkfifo(_PATH_,S_IFIFO|0666);
    if(ret !=0){
        printf("mkfifo error!\n");
        exit(1);
    }
    printf("写端(服务端)打开文件阻塞!\n");
    int fd = open(_PATH_,O_WRONLY);   //只写模式,会阻塞其它进程,直到另一个进程以只读方式打开之
    

    if(fd < 0){
        printf("open error!\n");
        exit(1);

    }
    char buf[_SIZE_];
    memset(buf,'\0',sizeof(buf));    //初始化缓冲区为0
    while(1){
        printf("下面是scanf等待输入阻塞:\n");
        scanf("%s",buf);
        int ret = write(fd,buf,strlen(buf)+1);
        if(ret < 0){
            printf("写入文件错误 !\n");
            break;
        }
        if(strncmp(buf,"quit",4)==0){  //比较buf和quit是否相同,相同返回0,如果输入quit即退出循环
            break;
        }
    }
    close(fd);
    return 0;
}

client读端:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define _PATH_ "/tmp/file.tmp"
#define _SIZE_ 100
int main(){
    printf("下面是读端打开文件阻塞:\n");
   int fd = open(_PATH_,O_RDONLY);
   if(fd <0){
    printf("open file error!\n");
    exit(1);
   }
   char buf[_SIZE_];
   memset(buf,'\0',sizeof(buf));
   while(1){
      int ret = read(fd,buf,sizeof(buf));
      if(ret <0){
        printf("读错或读完了buf!\n");
      }
      printf("%s\n",buf);
      if(strncmp(buf,"quit",4)==0){
        break;
      }
   }
   close(fd);
    return 0;
}

编译与执行
      打开两个终端(linux),先在一个终端中编译server.c,然后在另一个终端中编译client.c
在这里插入图片描述
在这里插入图片描述

3.消息队列

      消息队列是Linux IPC中很常用的一种通信方式,它通常用来在不同进程间发送特定格式的消息数据。

区别管道通信的主要特点:
可以进行进程间的双向通信
消息队列是基于消息的,管道是基于字节流的
消息队列不一定是先入先出

POSIX消息队列相关函数:
1、mq_open

#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);

      用于打开或创建一个消息队列,成功返回消息队列描述符,失败返回-1。

  1. name:表示消息队列的名字,它符合POSIX IPC的名字规则。以/开头,并且不能再有/。
  2. oflag:表示打开的方式,和open函数的类似。
    有必须的选项:O_RDONLY,O_WRONLY,O_RDWR,
    还有可选的选项:O_NONBLOCK,O_CREAT,O_EXCL。
  3. mode:是一个可选参数,在oflag中含有O_CREAT标志且消息队列不存在时,才需要提供该参数。
    表示默认访问权限,0666。
  4. attr:也是一个可选参数,在oflag中含有O_CREAT标志且消息队列不存在时才需要。该参数用于给新队列设定某些属性,如果是空指针,那么就采用默认属性。

  mq_open返回值是mqd_t类型的值,被称为消息队列描述符。

2、mq_close

#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
mqd_t mq_close(mqd_t mqdes);

      用于关闭一个消息队列,关闭后,消息队列并不从系统中删除。
一个进程结束,会自动关闭打开着的消息队列。

3、mq_unlink

#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
mqd_t mq_unlink(const char *name);

   用于删除一个消息队列。消息队列创建后可以通过调用该函数进行删除。

4、mq_getattr和mq_setattr
POSIX消息队列的属性
POSIX标准规定消息队列属性mq_attr必须要含有以下四个内容:
   long mq_flags //消息队列的标志:0阻塞,O_NONBLOCK不阻塞
   long mq_maxmsg //消息队列的最大消息数
   long mq_msgsize //消息队列中每个消息的最大字节数
   long mq_curmsgs //消息队列中当前的消息数目,用户不可修改
函数原型:

#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
mqd_t mq_getattr(mqd_t mqdes, struct mq_attr *attr);
mqd_t mq_setattr(mqd_t mqdes, struct mq_attr *newattr, struct mq_attr *oldattr);
  1. mq_setattr可以设置的属性只有mq_flags,用来设置消息队列的非阻塞和阻塞,newattr结构的其他属性被忽略。
  2. mq_maxmsg和mq_msgsize属性只能在创建消息队列时通过mq_open来设置,
  3. mq_open只会设置该两个属性,忽略另外两个属性。
  4. mq_curmsgs属性只能被获取而不能被设置。

下面是测试代码,注意:编译依赖动态库:librt.so:

#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    mqd_t mqID;
    mqID = mq_open("/anonymQueue", O_RDWR | O_CREAT, 0666, NULL);
    if (mqID < 0) {
        printf("mq_opene error\n");
        exit(1);
    }

    struct mq_attr mqAttr;
    if (mq_getattr(mqID, &mqAttr) < 0) {
        printf("omq_getattr error\n");
        exit(1);
    }

    printf("mq_flags:%ld \n",mqAttr.mq_flags);
    printf("mq_maxmsg:%ld \n",mqAttr.mq_maxmsg);
    printf("mq_msgsize:%ld \n",mqAttr.mq_msgsize);
    printf("mq_curmsgs:%ld \n",mqAttr.mq_curmsgs);

    mqAttr.mq_flags = O_NONBLOCK;
    if (mq_setattr(mqID, &mqAttr,NULL) < 0) {
        printf("mq_setattr error\n");
        exit(1);
    }

    printf("mq_flags:%ld \n",mqAttr.mq_flags);
    printf("mq_maxmsg:%ld \n",mqAttr.mq_maxmsg);
    printf("mq_msgsize:%ld \n",mqAttr.mq_msgsize);
    printf("mq_curmsgs:%ld \n",mqAttr.mq_curmsgs);
}
//编译执行,依赖系统的动态库librt.so
# gcc xiaoxitest.c -o xiaoxitest -lrt
#./xiaoxitest
mq_flags:0     //默认是阻塞的
mq_maxmsg:10 
mq_msgsize:8192 
mq_curmsgs:0 
mq_flags:2048    //修改为非阻塞
mq_maxmsg:10 
mq_msgsize:8192 
mq_curmsgs:0 



5、mq_send和mq_receive两函数发送和接收消息:

#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>

mqd_t mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);

成功返回0,出错返回-1

mqd_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio);

成功返回接收到消息的字节数,出错返回-1
      mq_send向消息队列中写入一条消息,mq_receive从消息队列中读取一条消息。

  1. mqdes:消息队列描述符;
  2. msg_ptr:指向消息体缓冲区的指针;
  3. msg_len:消息体的长度。
  4. msg_prio:消息的优先级;它是一个小于MQ_PRIO_MAX的数,数值越大,优先级越高。
    POSIX消息队列在调用mq_receive时总是返回队列中最高优先级的最早消息。
    如果消息不需要设定优先级,那么可以在mq_send是置msg_prio为0,
    mq_receive的msg_prio置为NULL。

下面是消息队列使用的测试代码:
send.c,先发后收

#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#define MQ_FILE "/hello"

int main(){
    char input[100]={0};
    struct mq_attr attr;    //消息队列属性
    mqd_t mqID;
    int get_num =0;   //接收消息的字节数
    int prio = 0;     //消息队列中的消息优先级,数越大,优先级越高
    //创建消息队列,后执行程序,先启动发送端
    mqID = mq_open(MQ_FILE,O_RDWR | O_CREAT |O_EXCL,0666,NULL);  //NULL表示默认消息属性
    if(mqID <0){
        if(mqID == -1){
            mq_unlink(MQ_FILE);   //删除消息队列
            mqID = mq_open(MQ_FILE,O_RDWR | O_CREAT,0666,NULL);
        }else{
            printf("mq_open error!\n");
            exit(1);
        }
    }
   while(1){    //要一直发消息,接收消息
    //先发
    printf("请输入要发送的消息!\n");
    scanf("%s",input);
    if(strncmp(input,"quit",4)==0){  //如果发送的信息头四个是quit,则退出发送阻塞
        mq_send(mqID,input,strlen(input),0);  
        mq_unlink(MQ_FILE);                     //不玩了,删除消息队列
        break;                                  //退出循环
    }
    mq_send(mqID,input,strlen(input),0);  //发送除 quit的消息

    //后收回复的消息
    mq_getattr(mqID,&attr);
    get_num = mq_receive(mqID,input,attr.mq_msgsize,&prio);
    if(get_num <0){
        printf("mq_receive error!\n");
        sleep(1);           //不能退出(exit),要不然的话会结束会话
     }
     if(strncmp(input,"quit",4)==0){
        break;                         //即然回复的是quit,说明发送方退出了,我也要退出了
     }
     printf("收到的消息内容: %s,字节数:%d,优先级: %d\n",input,get_num,prio);
    }
   
    return 0;
}

rec.c先收再发,打开消息队列,而不创建之

#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#define MQ_FILE "/hello"

int main(){
    char buf[100]={0};
    struct mq_attr attr;    //消息队列属性
    mqd_t mqID;
    int get_num =0;   //接收消息的字节数
    int prio = 0;     //消息队列中的消息优先级,数越大,优先级越高
    mqID = mq_open(MQ_FILE,O_RDWR);  //只打开消息队列
    if(mqID <0){
        printf("open error!\n");
    }
    while(1){
    //先收发送来的消息
     mq_getattr(mqID,&attr);
     get_num = mq_receive(mqID,buf,attr.mq_msgsize,&prio);
     if(get_num <0){
        printf("mq_receive error!\n");
        sleep(1);           //不能退出(exit),要不然的话会结束会话
      }
      if(strncmp(buf,"quit",4)==0){
         break;                         //即然回复的是quit,说明发送方退出了,我也要退出了
      }
       printf("收到的消息内容: %s,字节数:%d,优先级: %d\n",buf,get_num,prio);
    
    //后发
    printf("请输入要发送的消息!\n");
    scanf("%s",buf);
    if(strncmp(buf,"quit",4)==0){  //如果发送的信息头四个是quit,则退出发送阻塞
        mq_send(mqID,buf,strlen(buf),0);  
        mq_unlink(MQ_FILE);                     //不玩了,删除消息队列
        break;                                  //退出循环
     }
     mq_send(mqID,buf,strlen(buf),0);  //发送除 quit的消息
   }
    

    return 0;
}

编译行执,打开两个linux终端,分别编译执行之

# gcc -g send.c -o send -lrt           #gcc -g recive.c -o rec -lrt
#./hello                               #./rec                             
请输入要发送的消息!                   收到的消息内容: hello,字节数:5,优先级: 0
hello                                 请输入要发送的消息!
                                      k l 
收到的消息内容: k l,字节数:3,优先级: 0

注意:POSIX消息队列的限制
      POSIX消息队列本身的限制就是mq_attr中的mq_maxmsg和mq_msgsize,
这两个参数可以在调用mq_open创建一个消息队列的时候设定。
但这个设定是受到系统内核限制的。

# ulimit -a |grep message

      限制大小为800KB,该大小是整个消息队列的大小,不仅仅是最大消息数*消息的最大大小;还包括消息队列的额外开销。
      MQ_OPEN_MAX:一个进程能同时打开的消息队列的最大数目,POSIX要求至少为8
      MQ_PRIO_MAX:消息的最大优先级,POSIX要求至少为32

4.信号量

      信号量的本质是一种数据操作锁,只是一种外部资源的标识,信号量本身不具备通信能力。

简单说一下信号量的工作机制:
      信号量只能进行两种操作等待和发送信号,即P(sv) V(sv)他们的行为是这样的:
P(sv) :如果sv 的值大于零,就给它减掉一个值;值减少到零,就阻塞进程。
V(sv) :恢复进程运行,给sv值增加。

Linux下信号量相关函数
   信号量函数由semget、semop、semctl三个函数组成,这个三个函数来自System V标准体系。

(1)semget,得到一个信号量集标识符或创建一个信号量集对象

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
  1. key:0(IPC_PRIVATE):会建立新信号量集对象
    大于0的32位整数:通常要求此值来源于ftok返回的IPC键值,这个key和semflg有关联

  2. nsems:创建信号量集中信号量的个数,该参数只在创建信号量集时有效

  3. semflg:0:取信号量集标识符,若不存在则函数会报错
       IPC_CREAT:当semflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的信号量集,则新建一个信号量集;如果存在这样的信号量集,返回此信号量集的标识符
        IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的信号量集,则新建一个信号量集;如果存在这样的信号量集则报错

(2)semop,对信号量集标识符为semid中的一个或多个信号量进行P操作或V操作

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops)
  1. semid:信号量集标识符

  2. sops:指向进行操作的信号量集结构体数组的首地址,此结构的具体说明如下:
    struct sembuf {
         short semnum; 信号量集合中的信号量编号,0代表第1个信号量
         short val;
           若val>0进行V操作信号量值加val,表示进程释放控制的资源
           若val<0进行P操作信号量值减val,若(semval-val)<0,则调用进程阻塞,直到资源可用;
           //若设置IPC_NOWAIT不会睡眠,进程直接返回EAGAIN错误*/
           若val==0时,调用进程进入睡眠状态,直到信号值为0;若设置IPC_NOWAIT,进程不会睡眠,直接返回EAGAIN错误
        short flag;
           0 设置信号量的默认操作
           PC_NOWAIT设置信号量操作不等待
           SEM_UNDO 选项会让内核记录一个与调用进程相关的UNDO记录,如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值
    };

  3. nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。
    最常见设置此值等于1,只完成对一个信号量的操作

成功:返回信号量集的标识符,失败返回-1

   sops为指向sembuf数组,定义所要进行的操作序列。
下面是信号量操作举例:

struct sembuf sem_get={0,-1,IPC_NOWAIT}; /*将信号量对象中序号为0的信号量减1*/
struct sembuf sem_get={0,1,IPC_NOWAIT};  /*将信号量对象中序号为0的信号量加1*/
struct sembuf sem_get={0,0,0};           /*进程被阻塞,直到对应的信号量值为0*/

      flag一般为0,若flag包含IPC_NOWAIT,则该操作为非阻塞操作。
      若flag包含SEM_UNDO,则当进程退出的时候会还原该进程的信号量操作,这个标志在某些情况下是很有用的,比如某进程做了P操作得到资源,但还没来得及做V操作时就异常退出了,此时,其他进程就只能都阻塞在P操作上,于是造成了死锁。若采取SEM_UNDO标志,就可以避免因为进程异常退出而造成的死锁。

(3)semctl,可以对信号量集中的单个信号量进行很多操作

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, union semun arg)
  1. semid:信号量集标识符
  2. semnum:信号量集数组上的下标,表示某一个信号量
  3. cmd:
    IPC_RMID:从内核中删除信号量集合
    SETVAL:用联合体中val成员的值设置信号量集合中单个信号量的值
    IPC_STAT:从信号量集上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中semid_ds内核为每个信号量集合维护着一个结构体
    IPC_SET:设置一个信号量集合的semid_ds里的值,并从semun的buf中取出值
    GETALL和SETALL:GETALL从信号量集合中获得所有信号量的值,并把其整数值存到semun联合体, SETALL反过来设置
    arg:
union semun {
   short val;          /*SETVAL用的值*/
   struct semid_ds* buf; /*IPC_STAT、IPC_SET用的semid_ds结构*/
   unsigned short* array; /*SETALL、GETALL用的数组值*/
   struct seminfo *buf;   /*为控制IPC_INFO提供的缓存*/
} arg;

信号量本身不具备通信的能力,例子我们放到下面共享内存,联合演示。

5.共享内存(最高效的通信方式)

(1)什么是共享内存
在这里插入图片描述

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

(2)共享内存的使用涉及到的函数
      与信号量一样,在Linux中也提供了一组函数接口用于使用共享内存,而且使用共享共存的接口还与信号量的非常相似,而且比使用信号量的接口来得简单。
1)shmget函数,创建共享内存
原型为:

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);  

第一个参数,与信号量的semget函数一样,程序需要提供一个参key(非0 整数),
它有效地为共享内存段命名。

第二个参数,size以字节为单位指定需要共享的内存容量

第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,
如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。
共享内存的权限标志与文件的读写权限一样,0644表示允许一个进程创建的共享内存被
内存创建者所拥有的进程向共享内存读取和写入数据,
同时其他用户创建的进程只能读取共享内存。

      shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。失败返回-1

2)shmat函数,启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
原型:

void *shmat(int shm_id, const void *shm_addr, int shmflg);  

第一个参数,shm_id是由shmget函数返回的共享内存标识。
第二个参数,shm_addr指定共享内存连接到当前进程中的地址,通常为空,让系统来选择共享内存的地址。
第三个参数,shm_flg是一组标志位,通常为0,详情到linux中,用man shmat查看。

调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.

3)shmdt函数,将共享内存从当前进程中分离。
      注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。
原型如下:

int shmdt(const void *shmaddr);  

参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.

4)shmctl函数,与信号量的semctl函数一样,用来控制共享内存,
原型如下:

int shmctl(int shm_id, int command, struct shmid_ds *buf);  
  1. 第一个参数,shm_id是shmget函数返回的共享内存标识符。
  2. 第二个参数,command是要采取的操作,它可以取下面的三个值 :
    IPC_STAT:把shmid_ds结构中的数据取出来,放到后面的buf
    IPC_SET:如果进程有足够的权限,把后面的buf的值设置到内核shmid_ds结构去。
    IPC_RMID:删除共享内存段
  3. 第三个参数,buf是一个结构指针,它指向结构体是内核中用来维护共享内存的内部结构体。
使用共享内存进行进程间通信

      大概思路:假如两个进程,一个往共享内中写,一个从共享内存中读,为了两个进程同步,我们设置两个信号量,当信号量值为1,0时,进程只可写,另一个进程看到1,0,就不读了,当写完操作做好了,再把信号量减去为0,1了,释放共享内存,让另一个进程读。
      文件shmread.c创建共享内存,并读取其中的信息,另一个文件shmwrite.c向共享内存中写入数据。
      为了方便操作和数据结构的统一,为这两个文件定义了相同的数据结构,定义在文shmdata.h中。

shmdata.h的源代码如下:定义了一些宏和共用体

# ifndef _SHM_SEM_H
# define _SHM_SEM_H

# define SHM_KEY 38111
# define SEM_KEY 38222
# define SHM_SIZE 2048
# define SEM_NUM  2

union semun
{
    int val;                 /*for SETVAL*/
    struct semid_ds *buf;    /*for IPC_STAT  ans  IPC_SET*/
    unsigned short *array;   /*for GETALL  ans  SET_ALL*/
};



# endif

源文件shmread.c的源代码如下:

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>

#include "shmdata.h"

/*sem P*/
int sem_p(int semid, int semnum) {
    /*int semop(int semid, struct sembuf semoparray[], size_t cnts); */
    struct sembuf semOpr;

    semOpr.sem_num = semnum;
    semOpr.sem_op = -1;
    semOpr.sem_flg = SEM_UNDO;

    return semop(semid, &semOpr, 1);
}

/*sem V*/
int sem_v(int semid, int semnum) {
    /*int semop(int semid, struct sembuf semoparray[], size_t cnts); */
    struct sembuf semOpr;

    semOpr.sem_num = semnum;
    semOpr.sem_op = 1;
    semOpr.sem_flg = SEM_UNDO;

    return semop(semid, &semOpr, 1);
}

/*get semval*/
int get_semval(int semid, int semnum) { return semctl(semid, semnum, GETVAL); }

int main(void) {
    int ret;
    int shmid;
    int semid;
    void *ptrShm = NULL;
    struct shmid_ds shm_ds;
    struct semid_ds sem_ds;
    char buf[1024] = {0};
    union semun arg;
    unsigned short semvalArr[SEM_NUM];

    memset(&shm_ds, 0x00, sizeof(shm_ds));
    memset(&sem_ds, 0x00, sizeof(sem_ds));

    /*semget*/
    semid = semget((key_t)SEM_KEY, SEM_NUM, 0660 | IPC_CREAT | IPC_EXCL); 
    if (semid == -1) {
        if (errno == EEXIST) /*no need to init sem*/
        {
            printf("semget() warning: %s\n", strerror(errno));
            semid = semget((key_t)SEM_KEY, 0, 0660 | IPC_CREAT); 
            if (semid == -1) {
                printf("semget() error: %s\n", strerror(errno));
                return -1;
            }
            printf("semget() success. semid=[%d]\n", semid);
        } else {
            printf("semget() error: %s\n", strerror(errno));
            return -1;
        }
    } else /*need to init sem*/
    {
        printf("semget() success. semid=[%d]\n", semid);

        /*semctl(): init sem, set semvalArr[0]=1 semvalArr[1]=0*/
        semvalArr[0] = (unsigned short)1;
        semvalArr[1] = (unsigned short)0;
        arg.array = semvalArr;
        ret = semctl(semid, 0, SETALL, arg); 
        if (ret == -1) {
            printf("semctl() SETALL error: %s\n", strerror(errno));
            printf("init sem error.\n");
            ret = semctl(semid, 0, IPC_RMID);
            if (ret == -1) {
                printf("semctl() error: %s\n", strerror(errno));
            }
            printf("semctl() success. Sem is deleted.\n");
            return -1;
        }
        printf("semget() success.\n");
    }
    printf("semval[0]=[%d] semval[1]=[%d].\n", get_semval(semid, 0), get_semval(semid, 1));

    /*shmget*/
    shmid = shmget((key_t)SHM_KEY, SHM_SIZE, 0660 | IPC_CREAT | IPC_EXCL); 
    if (shmid == -1) {
        if (errno == EEXIST) {
            printf("shmget() warning: %s\n", strerror(errno));
            shmid = shmget((key_t)SHM_KEY, SHM_SIZE, 0660 | IPC_CREAT);
            if (shmid == -1) {
                printf("shmget() error: %s\n", strerror(errno));
                return -1;
            }
        } else {
            printf("shmget() error: %s\n", strerror(errno));
            return -1;
        }
    }
    printf("shmget() success. shmid=[%d]\n", shmid);

    /*shmat*/
    ptrShm = shmat(shmid, NULL, 0); 
    if (ptrShm == NULL) {
        printf("shmat() error: %s\n", strerror(errno));
        goto FAILED;
    }
    printf("shmat() success.\n");
    printf("Begin to read shared memory.\n");

    while (1) {
        /*test semvalArr[0]*/
        ret = get_semval(semid, 1);
        if (ret < 0) /**/
        {
            printf("get_semval() error.\n");
            goto FAILED;
        } else if (ret == 0) /*It suggests that the server hasn't start yet*/
            continue;
        else
            break;
    }
    /*##########################################*/

    /*Begin to read*/
    while (1) {
        /*P*/
        ret = sem_p(semid, 1);
        if (ret == -1) {
            printf("sem_p() error\n");
            goto FAILED;
        }

        /*read shm*/
        memset(&buf, 0x00, sizeof(buf));
        memcpy(buf, (char *)ptrShm, sizeof(buf));

        /*V*/
        ret = sem_v(semid, 0);
        if (ret == -1) {
            printf("sem_v() error\n");
            goto FAILED;
        }

        printf("client: %s", buf);
        if (buf[0] == '\0') printf("\n");

        if (memcmp(buf, "exit", 4) == 0) {
            break;
        }
    }

    /*shmdt*/
    ret = shmdt(ptrShm);
    if (ret == -1) {
        printf("shmdt() error: %s\n", strerror(errno));
    }
    printf("shmdt() success.\n");

    /*shmctl IPC_RMID*/
    ret = shmctl(shmid, IPC_RMID, &shm_ds);
    if (ret == -1) {
        printf("shmctl() error: %s\n", strerror(errno));
    }
    printf("shmctl() success. Shm is deleted.\n");

    /*semctl IPC_RMID*/
    ret = semctl(semid, 0, IPC_RMID);
    if (ret == -1) {
        printf("semctl() error: %s\n", strerror(errno));
    }
    printf("semctl() success. Sem is deleted.\n");

    return 0;

FAILED:
    /*shmctl IPC_RMID*/
    ret = shmctl(shmid, IPC_RMID, &shm_ds);
    if (ret == -1) {
        printf("shmctl() error: %s\n", strerror(errno));
    }
    printf("shmctl() success. Shm is deleted.\n");

    /*semctl IPC_RMID*/
    ret = semctl(semid, 0, IPC_RMID);
    if (ret == -1) {
        printf("semctl() error: %s\n", strerror(errno));
    }
    printf("semctl() success. Sem is deleted.\n");

    return -1;
}

源文件shmwrite.c的源代码如下:

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>

#include "shmdata.h"


/*sem P*/
int sem_p(int semid, int semnum) {
    struct sembuf semOpr;
    semOpr.sem_num = semnum;
    semOpr.sem_op = -1;
    semOpr.sem_flg = SEM_UNDO;

    return semop(semid, &semOpr, 1);
}

/*sem V*/
int sem_v(int semid, int semnum) {
    struct sembuf semOpr;
    semOpr.sem_num = semnum;
    semOpr.sem_op = 1;
    semOpr.sem_flg = SEM_UNDO;
    return semop(semid, &semOpr, 1);
}

/*get semval*/
int get_semval(int semid, int semnum) { return semctl(semid, semnum, GETVAL); }

int main(void) {
    int ret;
    int shmid;
    int semid;
    void *ptrShm = NULL;
    struct shmid_ds shm_ds;
    struct semid_ds sem_ds;
    char buf[1024] = {0};
    union semun arg;
    unsigned short semvalArr[SEM_NUM] = {1, 0};

    memset(&shm_ds, 0x00, sizeof(shm_ds));
    memset(&sem_ds, 0x00, sizeof(sem_ds));

    /*semget*/
    semid = semget((key_t)SEM_KEY, SEM_NUM, 0660 | IPC_CREAT | IPC_EXCL); 
    if (semid == -1) {
        if (errno == EEXIST){
            printf("semget() warning: %s\n", strerror(errno));
            semid = semget((key_t)SEM_KEY, 0, 0660 | IPC_CREAT); /*nsems 可以是 0 当semid已存在*/
            if (semid == -1) {
                printf("semget() error: %s\n", strerror(errno));
                return -1;
            }
            printf("semget() success. semid=[%d]\n", semid);
        } else {
            printf("semget() error: %s\n", strerror(errno));
            return -1;
        }
    } else /*need to init sem*/
    {
        printf("semget() success. semid=[%d]\n", semid);
        arg.array = semvalArr;
        ret = semctl(semid, 0, SETALL, arg); 
        if (ret == -1) {
            printf("semctl() SETALL error: %s\n", strerror(errno));
            printf("init sem error.\n");
            /*semctl IPC_RMID*/
            ret = semctl(semid, 0, IPC_RMID);
            if (ret == -1) {
                printf("semctl() error: %s\n", strerror(errno));
            }
            printf("semctl() success. Sem is deleted.\n");
            return -1;
        }
        printf("semget() success.\n");
    }
    printf("init sem success. semval[0]=[%d] semval[1]=[%d].\n", get_semval(semid, 0), get_semval(semid, 1));

    /*shmget*/
    shmid = shmget((key_t)SHM_KEY, SHM_SIZE, 0660 | IPC_CREAT | IPC_EXCL); 
    if (shmid == -1) {
        if (errno == EEXIST) {
            printf("shmget() warning: %s\n", strerror(errno));
            shmid = shmget((key_t)SHM_KEY, SHM_SIZE, 0660 | IPC_CREAT);
            if (shmid == -1) {
                printf("shmget() error: %s\n", strerror(errno));
                return -1;
            }
        } else {
            printf("shmget() error: %s\n", strerror(errno));
            return -1;
        }
    }
    printf("shmget() success. shmid=[%d]\n", shmid);

    /*shmat*/
    ptrShm = shmat(shmid, NULL, !SHM_RDONLY); /*void *shmat(int shmid, const void *shmarr,  int flag);*/
    if (ptrShm == NULL) {
        printf("shmat() error: %s\n", strerror(errno));
        goto FAILED;
    }
    printf("shmat() success.\n");

    /*Begin to write*/
    printf("Begin to write shared memory.\n");
    while (1) {
        /*input*/
        memset(&buf, 0x00, sizeof(buf));

        printf("server: ");
        if (fgets(buf, sizeof(buf) - 1, stdin) < 0) {
            printf("fgets() error\n");
            fflush(stdin);
            continue;
        }
        fflush(stdin);

        /*P*/
        ret = sem_p(semid, 0);
        if (ret == -1) {
            printf("sem_p() error\n");
            goto FAILED;
        }

        /*write into shm*/
        memset(ptrShm, 0x00, SHM_SIZE);
        memcpy((char *)ptrShm, buf, strlen(buf));

        /*V*/
        ret = sem_v(semid, 1);
        if (ret == -1) {
            printf("sem_v() error\n");
            goto FAILED;
        }

        if (memcmp(buf, "exit", 4) == 0) {
            break;
        }
    }

    /*shmdt*/
    ret = shmdt(ptrShm);
    if (ret == -1) {
        printf("shmdt() error: %s\n", strerror(errno));
    }
    printf("shmdt() success.\n");

    return 0;

FAILED:
    /*shmctl IPC_RMID*/
    ret = shmctl(shmid, IPC_RMID, &shm_ds);
    if (ret == -1) {
        printf("shmctl() error: %s\n", strerror(errno));
    }
    printf("shmctl() success. Shm is deleted.\n");

    /*semctl IPC_RMID*/
    ret = semctl(semid, 0, IPC_RMID);
    if (ret == -1) {
        printf("semctl() error: %s\n", strerror(errno));
    }
    printf("semctl() success. Sem is deleted.\n");

    return -1;
}

Makefile

CC = gcc

编译与执行

  1. 先make shmwrite,
  2. 然后make shmread
  3. 先执执行写 ./shmwrite
  4. 然后读 ./shmread
  • 31
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qq_33406021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值