进程间通信

进程间通信目的

进程间通信分类

匿名管道

system V共享内存

信号量(semaphore)

进程间通信(Inter-Process Communication,IPC)是指不同的进程之间进行数据交换和通信的机制。在操作系统中,进程是独立运行的程序实例,通过IPC机制可以实现进程之间的数据共享、同步和通信,以实现协作和协调。

进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变

进程间通信分类

  1. 管道
  • 匿名管道pipe :管道是一种半双工的通信机制,可以在具有亲缘关系的进程之间进行通信。它有两种类型:匿名管道(使用pipe函数)和命名管道(使用mkfifo函数),用于在进程之间传递字节流数据。
  • 命名管道:FIFO(命名管道):FIFO是一种有名字的管道,可以在不具有亲缘关系的进程之间进行通信。通过使用mkfifo函数创建一个命名管道文件,多个进程可以通过打开该文件并进行读写操作来实现通信。
  1. System V IPC
  • System V 消息队列
  • System V 共享内存
  • System V 信号量
  1. POSIX IPC
  • 消息队列(Message Queue):消息队列是一种通过在进程之间传递消息实现通信的机制。进程可以将消息放入队列中,其他进程可以从队列中读取消息。消息队列提供了一种异步通信方式。
  • 共享内存(Message Queue):共享内存是一种高效的进程间通信方式。它允许多个进程共享同一块物理内存区域,进程可以直接读写共享内存,避免了数据的复制和传输开销。
  • 信号量(Semaphore):信号量是一种用于进程间同步和互斥的机制。它可以用于控制对临界资源的访问,并确保不同进程之间的顺序执行。
  • 互斥量
  • 条件变量
  • 读写锁

匿名管道

匿名管道(Anonymous Pipe)是一种无名的、半双工的进程间通信机制,用于在具有亲缘关系的父子进程之间传递字节流数据。它是Unix和类Unix系统中常见的IPC方式之一。

匿名管道特点:

  1. 半双工通信:匿名管道是单向的,一般用于父进程向子进程传递数据。它只支持单向的数据传输,需要在父进程和子进程之间建立两个管道,分别用于父进程向子进程传递数据和子进程向父进程传递数据。

  2. 无名管道:匿名管道是无名的,没有文件系统中的路径和文件名。它是在创建进程时自动创建的,不需要显式地在文件系统中创建管道文件。

  3. 文件描述符:匿名管道通过文件描述符进行引用。在创建管道时,使用pipe()系统调用会返回两个文件描述符,一个用于读取数据,另一个用于写入数据。

  4. 内存缓冲区:匿名管道的数据传输是在内存中进行的,通过内核内存缓冲区进行数据的临时存储。数据从写入端写入缓冲区,然后从读取端读取。

  5. 阻塞式通信:匿名管道是阻塞式的通信机制。如果没有数据可读,则读取操作会阻塞进程,直到有数据到达。类似地,如果管道已满,则写入操作会阻塞进程,直到有空间可用。

匿名管道是一种简单而有效的进程间通信方式,适用于具有亲缘关系的父子进程之间的通信。它的数据传输是在内存中进行的,速度较快。但由于是单向的,只能支持父进程向子进程或子进程向父进程的数据传输。

我们先来见识一下管道
在这里插入图片描述

who查看当前服务器有哪些用户在使用,wc统计文本行有多少行,|是管道,管道前后的两个命令是两个进程,前面的进程通过管道将数据给后面的进程
在这里插入图片描述

sleep 30000后面的&是让他们后台运行
可以看到三个sleep确实是三个进程,父进程相同都是bash

匿名管道的原理

我通过who | wc -1来讲一下管道的原理

因为Linux下一切皆文件,管道也一样是文件,创建一个管道,等同于打开一个文件,who进程以写的方式打开管道并将自己的标准输出重定向到管道,然后wc -1进程以读的方式打开管道并将自己的标准输入重定向到管道,这样就可以让这两个进程进行通信

在这里插入图片描述

这个管道是匿名管道,管道文件的缓冲区是OS提供的内存级的文件,是一个临时文件,本来一个文件是要在磁盘创建然后在打开的,但是这个文件在磁盘中不存在,是OS直接在内存中创建给管道用的,r是以读的方式打开管道文件,w以写的方式打开管道文件

fork后创建子进程,拷贝task_struct,文件描述符表,但是被打开的文件不会被拷贝,文件描述符表里面放的都是这些文件的地址,所以父子进程共同使用这些被打开的文件,这就相当于浅拷贝,这下就满足了通信的一个前提,看到同一份资源

接下来确定数据的流向,比如父进程的信息通过管道传给子进程,就要关闭父进程读( r)打开的管道文件,关闭子进程写(w)打开的管道文件,让父进程只能写进管道,子进程只能读取管道

这样的管道只能单向通信,因为管道的缓冲区只有一个,想要双向,就要再打开一个管道
为什么父进程要同时以写(w)和读( r)的方式打开管道文件? -> 因为子进程要继承读和写这两种打开的管道文件,如果父进程只以读的方式打开文件,那么子进程就只能跟着父进程一起以读的方式打开文件,这样就不可以让父子进程进行通信了

使用匿名管道通信

首先我们要认识一个函数int pipe(pipefd[2])这个函数是OS提供的用来打开管道文件的函数

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

参数中pipe[2]是一个输出型参数,在pipe函数内部会以读和写的方式分别打开管道文件,然后将读的文件描述符放在数组的第一个元素,写的文件描述符放在数组第二个元素

创建管道代码

  int pipefd[2] = {0};
  //1.创建管道
  if(n < 0)
  {
    cout << "pipe errno," << errno << strerror(errno) << endl;
    return -1;
  }

创建子进程和子进程代码

    //创建子进程
    pid_t id = fork();
    assert(id != -1);

    if(id == 0)
    {
      //子进程
      //让子进程写入,就要关闭子进程的读端,0下标对应的文件描述符就是读端
      close(pipefd[0]);

      //通信
      const string str = "我是子进程";
      int cnt = 1;
      char buffer[1024];
      while(1)
      {
        snprintf(buffer, sizeof(buffer), "%s,计数器:%d,我的PID:%d\n", str.c_str(), cnt++, getpid());
        write(pipefd[1], buffer, strlen(buffer));
        sleep(1);
      }

      close(pipefd[1]);
      exit(0);
    }

父进程代码

//父进程
    //父进程
    //让父进程读取
    close(pipefd[1]);
   
    //通信
    char buffer[1024];
    while(1)
    {
      int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
      if(n > 0)
      {
        buffer[n] = '\0';
        cout << "我是父进程,子进程说:" << buffer << endl;
      }
    }

    close(pipefd[0]);

总代码

#include <iostream>
#include <cerrno>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <cassert>
#include <string>

using namespace std;

int main()
{
    int pipefd[2] = {0};
    //1.创建管道
    int n = pipe(pipefd);
    if (n < 0)
    {
        cout << "pipe errno," << errno  << ";" << strerror(errno) << endl;
        return -1;
    }
    //创建子进程
    pid_t id = fork();
    assert(id != -1);

    if(id == 0)
    {
      //子进程
      //让子进程写入,就要关闭子进程的读端
      close(pipefd[0]);

      //通信
      const string str = "我是子进程";
      int cnt = 1;
      char buffer[1024];
      while(1)
      {
        snprintf(buffer, sizeof(buffer), "%s,计数器:%d,我的PID:%d\n", str.c_str(), cnt++, getpid());
        write(pipefd[1], buffer, strlen(buffer));
        sleep(1);
      }

      close(pipefd[1]);
      exit(0);
    }

    //父进程
    //让父进程读取
    close(pipefd[1]);
   
    //通信
    char buffer[1024];
    while(1)
    {
      int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
      if(n > 0)
      {
        buffer[n] = '\0';
        cout << "我是父进程,子进程说:" << buffer << endl;
      }
    }

    close(pipefd[0]);
    return 0;
}

在这里插入图片描述

根据代码,子进程将信息写入管道,父进程从管道读取并打印,只有父进程会打印,看到结果,可以知道通信成功了

匿名管道的特点

  • 五种特点
  1. 单向通信
  2. 管道的本质是文件,生命周期和进程是一样的
  3. 管道通信,可以在具有“血缘”关系的进程间通信,如父子进程,爷孙进程,兄弟进程的等,常在父子进程间通信
  4. 管道通信中,读可以一次全读下来,可以读一定量子节,写也是,读和写可以随便定,读写的次数的多少没有强相关(子节流)
  5. 具有一定的协同能力,让读和写按照一定的步骤通信(自带同步机制)
  • 四种场景
  1. 如果读的很快,写的很慢,那么读的进程就要等待写的进程去写入,就是一个进程读的时候管道里没有信息,就要等待另一个进程去写入
  2. 管道是有大小的,不能无限制写,写满了就不能再写了,要等另一个进程把信息读完才可以写入
  3. 写的进程写完关闭,读的进程读完之后信息后再读,read会返回0,表示读到了文件结尾
  4. 写的进程一直写,读的进程读了几次之后不读了,直接关闭,OS会直接发送13信号SIGPIPE杀死一直在写的进程,因为OS不会维护无意义,低效率,浪费资源的事,读的进程关闭了,写的进程却仍然在写,又没有进程可以读到这些信息了,所以这样写入是无意义的,会被OS杀死
    在这里插入图片描述
    在这里插入图片描述
    子进程代码不变,修改一下父进程的代码,让父进程先退出然后等待子进程退出,得到子进程退出收到的13信号
  • 原子性:就是保证你写入的数据的完整,比如说我写入hello world,就是保证写完hello world之后再读,而不是写一个hello就读了
    当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
    当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。

特点:

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

    半双工:比如说两个说话,就是一个人说一个人听
    全双工:就是两个人吵架,不会等着别人说完再说,两个人对着喷

命名管道

命名管道(Named Pipe),也称为FIFO(First-In-First-Out),是一种有名字的管道用于进程间通信。与匿名管道不同,命名管道可以在文件系统中创建,并且可以被多个进程同时访问。

命名管道具有以下特点:

  1. 有名字的管道:命名管道在文件系统中有一个路径和文件名,可以通过文件系统中的路径来引用。它与普通文件类似,但其数据传输方式是按照先进先出的顺序进行。

  2. 文件系统中的创建:命名管道使用mkfifo命令或mkfifo()函数在文件系统中创建。创建后,可以像操作普通文件一样对其进行读写操作。

  3. 多进程访问:命名管道可以被多个进程同时访问,不限于具有亲缘关系的进程。不同进程可以通过打开同一个命名管道文件来进行读写操作。

  4. 阻塞式通信:命名管道是阻塞式的通信机制。如果没有数据可读,则读取操作会阻塞进程,直到有数据到达。类似地,如果管道已满,则写入操作会阻塞进程,直到有空间可用。

命名管道是一种灵活且功能强大的进程间通信方式,适用于不具有亲缘关系的进程之间的通信。它可以在文件系统中创建,支持多进程访问,并且具有阻塞式的通信特性。

命令行创建命名管道

命令mkfifo (文件名)即可创建一个命名管道,man手册中查到
在这里插入图片描述
创建的文件类型以p开头,表示管道文件
在这里插入图片描述
我们向文件fifo中写入一些东西发现卡在这里了
在这里插入图片描述
创建一个会话,发现fifo的内容还是0,而管道文件里明明有内容,这是因为管道文件有种特殊的特性,虽然在磁盘中创建了fifo,但是管道文件是内存级文件,所以并没有写到磁盘中,只是写到了管道文件里

为什么是内存级的文件,因为管道文件就是给进程通信用的,如果把文件内容刷到磁盘中,就会浪费时间,降低通信速度,没有必要

在这里插入图片描述

系统调用创建命名管道

在这里插入图片描述
系统调用接口也是mkfifo,在man手册,3号可以查到

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • pathname:要创建的命名管道的路径和文件名。
  • mode:权限模式,用于指定创建的命名管道的权限。通常使用八进制表示的权限模式,例如0666表示读写权限。

函数返回值为0表示创建命名管道成功,-1表示创建失败。


我们来使用系统调用创建命名管道

makefile文件内容

.PHONY:all
all:server client

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

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

.PHONY:clean
clean:
	rm -f server client

server.cc文件内容

#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>

using namespace std;

const string fifoname = "./fifo";
mode_t mode =0666;

int main()
{
    //1.创建管道文件
    int n = mkfifo(fifoname.c_str(), mode);
    if(n != 0)
    {
        cout << errno << ":" << strerror(errno) << endl;
        return -1;
    }

    return 0;
}

client.cc内容
在这里插入图片描述

在这里插入图片描述

可以看到成功创建出来了命名管道,但是命名管道文件的权限对不上
在这里插入图片描述
在这里插入图片描述
因为系统的umask文件掩码影响了文件权限
在这里插入图片描述
在这里插入图片描述
可以通过系统调用来在进程中设置umask掩码,并不会影响到系统默认的umask掩码,只会改变进程中的umask掩码

client.cc完整代码

#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdio>
#include <cassert>
#include <string>

using namespace std;

const string fifoname = "./fifo";

int main()
{
    //1.不用创建管道文件,只需要打开同一个管道文件
    int wfd = open(fifoname.c_str(), O_WRONLY);
    if(wfd < 0)
    {
        cout << errno << ":" << strerror(errno) << endl;
        return -1;
    }

    //2.通信
    char buffer[1024] = {0};
    while(1)
    {
        cout << "输入你的消息:";
        char* msg = fgets(buffer, sizeof(buffer), stdin);
        assert(msg);
        (void)msg;
        
        //去除msg字符串中最后带着的'\n'
        buffer[strlen(buffer) - 1] = 0;

        ssize_t n = write(wfd, buffer, sizeof(buffer));
        assert(n >= 0);
        (void)n;
    }

    close(wfd);

    return 0;
}

server.cc完整代码

#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string>

using namespace std;

const string fifoname = "./fifo";
mode_t mode =0666;

int main()
{
    //1.创建管道文件
    umask(0);
    int n = mkfifo(fifoname.c_str(), mode);
    if(n != 0)
    {
        cout << errno << ":" << strerror(errno) << endl;
        return -1;
    }

    //2.服务端打开管道文件
    int rfd = open(fifoname.c_str(), O_RDONLY);
    if(rfd < 0)
    {
        cout << errno << ":" << strerror(errno) << endl;
        return -2;
    }

    //3.通信
    char buffer[1024] = {0};
    while(1)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            cout << "client say:" << buffer << endl;
        }
        else if(n == 0)
        {
            cout << "client exit" << endl;
        }
        else
        {
            cout << errno << ":" << strerror(errno) << endl;
            break;
        }
    }

    //关闭管道文件
    close(rfd);

    return 0;
}

system V共享内存

共享内存就是在物理空间上开辟以一个内存块,然后通过页表映射到两个进程的地址空间的共享区中,然后两个进程就可以看到同一份资源,这样两个进程就可以通信了

共享内存是所有通信中最快的,管道通信是两进程通过看到相同的文件通信,需要使用系统接口,还要多次拷贝,而共享内存直接映射在两进程的地址空间中,两进程都可以直接通信,不需要拷贝

管道有同步机制,如果一方没有写,另一方就要等写了才能读,而共享内存没有这样的机制,共享内存的两个进程可以随时访问,就算数据为空或者乱码,也可以读
在这里插入图片描述

shmget创建共享内存

我们先直接来看看共享内存的使用

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  • key是一个可以标定唯一性的数,可以随便设置,但是我们一般用key_t ftok(const char *pathname, int proj_id)根据用户设定的路径和idftok函数会结合传入的路径和id,用算法生成一个冲突概率很低的数,并返回给用户

  • size就是设置申请的共享内存块的大小,可以任意设置

    大小是以PAGE页(4KB)为单位的,会向上对齐如果设置4097字节,那么操作系统会申请8KB,但是并不代表你能使用8KB,只能使用4097字节,并且ipcs查询后也只显示4097字节

  • shmflg就是传选项

    • IPC_CREAT:单独传这个宏,就是创建一个共享内存,如果共享内存不存在,就创建,如果已经存在,就获取已经存在的共享内存并返回
    • IPC_EXEL:不能单独使用,要IPC_CREAT一起使用,如果共享内存不存在,就创建,如果存在就立马出错并返回
  • 返回值
    创建成功返回共享内存的标识符,创建失败就返回-1,并设置错误码

使用方法:

server.cc的代码

#include <iostream>
#include <string>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>

using namespace std;

const string PATHNAME = ".";
const int PROJID = 0x2345;
const int shmsize = 4096;

key_t getkey()
{
    key_t k = ftok(PATHNAME.c_str(), PROJID);
    if(k == -1)
    {
        cout << errno << ":" << strerror(errno) << endl;
        exit(1);
    }

    return k;
}

int createshm(key_t k, int size)
{
    //创建一个新的共享内存
    int shmid = shmget(k, size, IPC_CREAT | IPC_EXCL);
    if(shmid == -1)
    {
        cout << errno << ":" << strerror(errno) << endl;
        exit(2);
    }

    return shmid;
}

int main()
{
    //1.创建key
    key_t k = getkey();
    printf("server key:0x%x\n", k);
    //2.创建共享内存
    int shmid = createshm(k, shmsize);
    printf("server shmid:%d\n", shmid);

    return 0;
}

client.cc的代码

#include <iostream>
#include <string>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>

using namespace std;

const string PATHNAME = ".";
const int PROJID = 0x2345;
const int shmsize = 4096;

key_t getkey()
{
    key_t k = ftok(PATHNAME.c_str(), PROJID);
    if(k == -1)
    {
        cout << errno << ":" << strerror(errno) << endl;
        exit(1);
    }

    return k;
}

int getshm(int k, int size)
{
    //获取共享内存
    int shmid = shmget(k, size, IPC_CREAT);
    if(shmid == -1)
    {
        cout << errno << ":" << strerror(errno) << endl;
        exit(2);
    }

    return shmid;
}

int main()
{
    //1.获取key
    key_t k = getkey();
    printf("client key:0x%x\n", k);
    //2.获取共享内存
    int shmid = getshm(k, shmsize);
    printf("client shmid:%d\n", shmid);

    return 0;
}


makefile代码

.PHONY:all
all:server client

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

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

.PHONY:clean
clean:
	rm -f client server

共享内存生命周期随操作系统

  • 指令删共享内存
    第一次运行如上代码的时候,可以创建成功共享内存,但是删除程序,重新运行的时候,刚刚创建的共享内存并不会被删掉,共享内存的生命周期不随进程,随操作系统,所以之后运行的时候都会报文件已存在这个错误
    在这里插入图片描述
    我们可以证明共享内存被创建了之后不会自动删除,会一直存在
    在这里插入图片描述
    ipcs(ipc是进程间通信- -Inter-Process Communication)可以查询进程间通信设施的状态,从到下依次为消息队列,共享内存,信号量
    ipcs -m是单独查询共享内存,perms(permission权限),bytes共享内存的大小,nattch共享内存与几个进程有连接,status共享内存状态

    修改权限就是要在创建共享内存的参数flag中按位或上权限,要设置umask掩码
    在这里插入图片描述

    在这里插入图片描述

    指令ipcrm -m shmid删除共享内存

  • shmctl删共享内存
    int shmctl(int shmid, int cmd, struct shmid_ds *buf)第一个参数:想要操作的共享内存的id,第二个参数:操作的选项

    IPC_STAT:获取共享内存段的信息,并将其存储在buf指向的shmid_ds结构中。
    IPC_SET:设置共享内存段的信息,使用buf指向的shmid_ds结构中的值来更新。
    IPC_RMID:删除共享内存段。

    第三个参数:存放共享内存的属性
    在这里插入图片描述
    系统调用删除共享内存
    在这里插入图片描述

shmatshmdt,连接和断连接共享内存和进程

连接上了共享内存之后就可以直接对其解引用访问

shmat

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid:共享内存的标识符,通过shmget()函数获取。
  • shmaddr:指定共享内存连接的地址,通常设为NULL,由系统自动分配地址。
  • shmflg:标志参数,用于指定共享内存的连接方式和权限。

server.cc:

char* attachshm(int shmid)
{
    char* start = (char*)shmat(shmid, nullptr, 0);
    return start;
}

int main()
{
    //1.创建key
    key_t k = getkey();
    printf("server key:0x%x\n", k);

    //2.创建共享内存
    int shmid = createshm(k, shmsize);
    printf("server shmid:%d\n", shmid);

    //3.连接共享内存
    char* start = attachshm(shmid);

    //4.删除共享内存
    delshm(shmid);
    return 0;
}

client.cc

char* attachshm(int shmid)
{
    char* start = (char*)shmat(shmid, nullptr, 0);
    return start;
}

int main()
{
    //1.获取key
    key_t k = getkey();
    printf("client key:0x%x\n", k);
    
    //2.获取共享内存
    int shmid = getshm(k, shmsize);
    printf("client shmid:%d\n", shmid);
	
    //3.连接共享内存
    char* start = attachshm(shmid);

    return 0;
}

shmdt

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

在这里插入图片描述

信号量(semaphore)

信号量(Semaphore)是一种用于进程间同步和互斥的机制。

信号量的主要作用是控制对共享资源的访问,以避免多个进程同时访问共享资源而导致的数据不一致或冲突。它可以用来实现进程间的互斥(Mutex)和同步(Synchronization)。

信号量的基本操作有两个:P 操作和 V 操作。

  • P(Proberen)操作,也称为申请操作或减操作,用于申请资源或减少信号量的值。如果信号量的值大于 0,则将其减 1;如果信号量的值为 0,则阻塞当前进程,直到信号量的值大于 0。
  • V(Verhogen)操作,也称为释放操作或增操作,用于释放资源或增加信号量的值。它将信号量的值加 1,并通知等待该信号量的其他进程。

信号量是一种用于进程间同步和互斥的机制,它通过 P 操作和 V 操作来实现资源的申请和释放。信号量可以用来控制对共享资源的访问,避免竞态条件和数据不一致问题。在并发编程中,信号量是一种重要的同步工具,用于实现进程间的互斥和同步。

互斥等4个概念

  1. 互斥:任何一个时刻,都只允许一个执行流在进行共享资源的访问
  2. 我们把任何一个时刻,都只运行一个执行流访问的公共资源叫做临界资源
  3. 临界资源是通过代码访问的,凡是访问临界资源的代码,叫做临界区
  4. 原子性,只有两种确定状态的属性

信号量是资源的预定机制,信号量本质上是一个计数器

任何一个执行流要向访问临界资源的一个子资源的时候,不能直接访问,要先申请信号量

申请成功后,信号量减少(count-=1),申请信号量成功后,一定可以拿到一个子资源。释放信号量资源,就是增加信号量(count+=1)

信号量必须保证自己的增加和减少是原子性的,申请信号量是P操作,释放信号量是V操作

如果信号量计数器为1,那就是二元信号量,实现的互斥功能,互斥本质将临界资源独立使用

查询信号量ipcs -s
在这里插入图片描述
删除信号量量ipcrm -s -semid

semget获取信号量

semget函数可用于创建新的信号量集或获取现有的信号量集

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
  • key
    用于标识一个唯一的信号量集通常使用ftok()函数生成。
  • nsems
    信号量的个数,操作系统允许一次申请多个信号量,创建的是一个信号量集
  • semflg
    • IPC_CREAT:如果指定的键值对应的信号量集合不存在,则创建一个新的信号量集合。如果信号量集合已存在,则该标志被忽略。
    • IPC_EXCL:与IPC_CREAT标志一起使用时,如果指定的键值对应的信号量集合已存在,则返回错误。
    • 0666:指定创建的信号量集合的权限。

semctl

semctl函数可用于获取信号量的信息、修改信号量的属性和删除信号量集

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);

semctl()函数是一个系统调用,用于对信号量集合进行控制操作,如获取或设置信号量集合中的属性、获取或设置信号量的值等。它的函数原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);

semctl()函数接受四个参数:

  • semid:信号量集合的标识符,由semget()函数返回。
  • semnum:要操作的信号量的编号,对于单个信号量集合来说,一般为 0。
  • cmd:指定要执行的操作,可以是以下命令之一:
    • GETVAL:获取信号量的值。
    • SETVAL:设置信号量的值。
    • GETPID:获取最后一次执行semop()操作的进程 ID。
    • GETNCNT:获取等待信号量值增加的进程数量。
    • GETZCNT:获取等待信号量值减少的进程数量。
    • IPC_RMID:删除信号量集合。
  • ...:可选参数,根据不同的命令可能需要传递额外的参数。

semop

semop函数可用于对信号量集执行一系列原子操作。每个操作由sembuf结构中的sem_numsem_opsem_flg字段定义,就是对信号量加或者减(count++或count–)

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

semop()函数是一个系统调用,用于对信号量集合执行操作。它可以用于对一个或多个信号量进行 P(等待)操作或 V(释放)操作,以实现进程间的同步和互斥。semop()函数的函数原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

semop()函数接受三个参数:

  • semid:信号量集合的标识符,由semget()函数返回。
  • sops:指向一个sembuf结构体数组的指针,每个结构体表示一个信号量操作。
  • nsops:待执行的信号量操作的数量。

sops结构体要自己去创建

sembuf结构体定义如下:

struct sembuf {
    unsigned short sem_num;  // 信号量的编号
    short sem_op;            // 信号量操作的值
    short sem_flg;           // 操作的标志
};

sembuf结构体的成员解释如下:

  • sem_num:要执行操作的信号量的编号,对于单个信号量集合来说,一般为 0。
  • sem_op:信号量操作的值,可以是正数、负数或零。
    • 正数:表示执行 V(释放)操作,将信号量的值增加。
    • 负数:表示执行 P(等待)操作,将信号量的值减少。
    • 零:表示执行 Z(阻塞)操作,如果信号量的值为零,则阻塞等待,直到信号量的值大于零。
  • sem_flg:操作的标志,可以是以下标志的按位或运算:
    • SEM_UNDO:在进程退出时自动撤销操作,避免出现死锁。
    • IPC_NOWAIT:非阻塞操作,如果无法进行操作,则立即返回错误。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值