Linux进程间通信详解-程序员宅基地

技术标签: Linux  运维  进程间通信  linux  共享内存  管道  开发语言  

进程间通信

进程间通信介绍

  1. 进程间通信目的
  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止
    时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另
    一个进程的所有陷入和异常,并能够及时知道它的状态改变。
  1. 进程间通信发展
  • 管道
  • System V进程间通信
  • POSIX进程间通信
  1. 进程间通信分类
  • 管道
    • 匿名管道pipe
    • 命名管道
  • System V IPC
    • System V 消息队列
    • System V 共享内存
    • System V 信号量
  • POSIX IPC
    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

一. 管道

1. 管道的基本概念

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

管道是一种最基本的IPC机制,作用于有血缘关系的进程之间(匿名管道),完成数据传递。调用pipe系统函数即可创建一个匿名管道。

有如下特质:

  1. 其本质是一个伪文件(实为内核缓冲区)
  2. 由两个文件描述符引用,一个表示读端,一个表示写端。
  3. 规定数据从管道的写端流入管道,从读端流出。

ps: 管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。

管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

  • 管道提供流式服务

  • 一般而言,进程退出,管道释放,所以管道的生命周期随着进程

  • 一般而言,内核会对管道操作进行同步互斥 (互斥:同一时间要么写要么读)

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

管道的局限性

  • 数据自己读不能自己写。
  • 数据一旦被读走,便不在管道中存在,不可反复读取。
  • 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
  • 只能在有公共祖先的进程间使用管道。
  • 常见的通信方式有,单工通信、半双工通信、全双工通信。

管道读写规则

  1. 当没有数据可读时
    • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
  2. 当管道满的时候
    • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  3. 管道对应的文件描述符被关闭
    • 写端被关闭,则从管道中读取完所有数据后,继续read会返回0,不再阻塞
    • 读端被关闭,write 操作会产生信号SIGPIPE,进而可能导致write进程退出
  4. 原子性
    • 写入的数据量 不大于 PIPE_BUF时,linux将保证写入的原子性。
    • 写入的数据量 大于 PIPE_BUF时,linux将不再保证写入的原子性。
  5. 其他注意事项
    • 管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除管道文件后,之前已经打开管道的进程依然可以通信

总而言之:

  • 管道是最简单,效率最差的一种通信方式。

  • 管道本质上就是内核中的一个缓存,当进程创建一个管道后,Linux会返回两个文件描述符,一个是写入端的描述符,一个是输出端的描述符,可以通过这两个描述符往管道写入或者读取数据。

  • 如果想要实现两个进程通过管道来通信,则需要让创建管道的进程fork子进程,这样子进程们就拥有了- 父进程的文件描述符,这样子进程之间也就有了对同一管道的操作。

  • 缺点

    • 半双工通信,一条管道只能一个进程写,一个进程读。
    • 一个进程写完后,另一个进程才能读,反之同理。

2. 管道的创建

①. 匿名管道

pipe

头文件: unistd.h

int pipefd[2]={
    0};
int pipe(int pipefd[2]);
  1. 功能: 创建一无名管道
  2. 参数:pipefd:文件描述符数组
    • pipefd[ 0 ]:读端文件描述符
    • pipefd[ 1 ]:写端文件描述符
  3. 返回值:
    • 成功返回 0 ;
    • 失败返回 -1 且 设置错误码 error

pipe函数的应用

#include<iostream>
#include<cassert>
#include<unistd.h>
#include<cstdio>  //等价于 stdio.h
#include<cstring>  //等价于 string.h  以c++的风格将c语言的string头文件改写
int main()
{
    
    int pipefd[2]={
    0};  
    int n = pipe(pipefd);  //创建管道
    assert(n == 0);   //在debug下,assert会存在 realease 下,该行代码不起作用
    (void)n;   //防止编译器在realease下告警 n被定义未使用
    
    cout<<"pipefd[0]:" <<pipefd[0]<<", pipefd[1]:"<<pipefd[1]<<endl;
    
    return 0;
}

创建匿名管道图文详解:

  1. 父进程调用pipe函数
    在这里插入图片描述

  2. fork创建子进程
    在这里插入图片描述

  3. 子进程关闭读端,父进程关闭写端
    在这里插入图片描述

  • 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
  • 从管道写端写入的数据会存在内核缓冲,直到数据从管道中被读取
  • 父进程读写端都打开本质是为了让子进程继承下来,最后确保管道只能进行单向通信
  • 父子进程通信可不可以创建全局缓冲区来完成通信呢? 不可以 ! 进程运行具有独立性,写时拷贝
②. 命名管道

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道
命名管道是一种特殊类型的文件

注意:管道文件是以 p 开头的,如图:

在这里插入图片描述

创建命名管道

  1. 命令行上创建

    #include <sys/stat.h>  //头文件
    mkfifo filename  //创建一个FIFO命名管道文件
    

    命名管道可以从命令行上创建

  2. 程序内创建

    //命名管道也可以从程序里创建
    int mkfifo(const char *filename,mode_t mode);
    //             文件名            文件权限
    int main(int argc, char *argv[])
    {
          
    	mkfifo("p2", 0644);
     	return 0;
    }
    
  3. 功能: 创建一命名管道

  4. 参数

    • filename:指定文件名
    • mode: 指定文件权限
  5. 返回值:

    • 成功返回0
    • 失败返回-1。
    • 如果返回EEXIST则表示管道的标识符文件已经存在了不需要重新创建。

命名管道的特性

  • 如果管道文件被只 打开,会阻塞,直到管道文件也被 方式打开
  • 如果管道文件被只 打开,会阻塞,直到管道文件也被 方式打开


匿名与命名管道的区别
  • 匿名管道pipe 函数创建并打开。
  • 命名管道mkfifo 函数创建,打开用 open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义。

3. 删除管道

删除管道文件可以用如下命令

unlink fifo  //删除管道文件
rm -f fifo

4. 管道的4种特殊情况

  • 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被阻塞挂起,直到管道里面有数据后,读端进程才会被唤醒。
  • 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被阻塞挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
  • 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
  • 读端进程将读端关闭,而写端进程还在一直向管道写入数据,没有进程读取,那么写入的数据就没有意义,那么操作系统会将写端进程杀掉。

二、system V

  1. 管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份资源。

  2. system V IPC提供的通信方式有以下三种:

    • system V共享内存
    • system V消息队列
    • system V信号量
  3. 其中,system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。

​   共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到系统内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据,减少了拷贝的次数,加快的程序的运行。

共享内存示意图
在这里插入图片描述

共享内存数据结构

truct shmid_ds {
    struct ipc_perm     shm_perm;   /* operation perms */
    int         shm_segsz;  /* size of segment (bytes) */
    __kernel_time_t     shm_atime;  /* last attach time */
    __kernel_time_t     shm_dtime;  /* last detach time */
    __kernel_time_t     shm_ctime;  /* last change time */
    __kernel_ipc_pid_t  shm_cpid;   /* pid of creator */
    __kernel_ipc_pid_t  shm_lpid;   /* pid of last operator */
    unsigned short      shm_nattch; /* no. of current attaches */
    unsigned short      shm_unused; /* compatibility */
    void            *shm_unused2;   /* ditto - used by DIPC */
    void            *shm_unused3;   /* unused */
};

1. 共享内存( shm )

shm基本概念

​ 共享内存本质上就是内存中的一块区域,用于进程间通信使用。该内存空间由操作系统分配与管理。与文件系统类似的是,操作系统在管理共享内存时,不仅仅有内存数据块,同时还会创建相应结构体来记录该共享内存属性,以便于管理。

因此,共享内存不只有一份,可以根据需求申请多个。

进程之间进行通信的时候,会获取 到共享内存的地址,写端进程写入数据,读端进程通过直接访问内存完成数据读取。

  • 共享内存优点
    相比于管道而言,共享内存不仅能够用于非父子进程之间的通信,而且访问数据的速度也比管道要快。这得益于通信直接访问内存,而管道则需要先通过操作系统访问文件再获得内存数据。

  • 共享内存缺点
    用于进程间通信时,共享内存本身不支持阻塞等待操作。这是因为当读端读取数据后,数据并不会在内存中清空。因此读端和写端可以同时访问内存空间,即全双工。因为共享内存本质是进程直接访问内存,无法主动停止读取,如果读端不加以限制,那么将持续读取数据。同理,写端也会持续写入数据。换句话说,共享内存本身没有访问控制。

    ​      共享内存**没有提供同步的机制**,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。
    

其他注意事项

  • 共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除
  • 共享内存生命周期随内核,只要不删除,就一直存在于内核中,除非重启系统(当然这里指的是非手动操作,可以手动删除)
shm函数

头文件:

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

ftok

函数 ftok 把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键值(也称IPC key键值)。该 IPC 为后续创建共享内存做准备。

该函数需要包含以下两个头文件:

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

fork函数原型

//把从pathname导出的信息与id的低序8位组合成一个整数IPC键
key_t ftok(const char *pathname, int proj_id);

//pathname:指定的文件,此文件必须存在且可存取
//proj_id:计划代号(project ID)

返回值(key_t 一般为32位的 int 型的重定义):

  • 成功:返回 key_t (即IPC 键值)
  • 出错:返回 -1,错误原因存于error中

shmget

int shmget(key_t key, size_t size, int shmflg);

//示例
int main()
{
    
    key_t n = ftok("/Linux/code",0x11223344);
    int id = shmget(n , 4096 , IPC_CREAT | IPC_EXCL | 0664);
}
  1. 功能:用来创建共享内存

  2. 参数

    • key: 这个共享内存段名字(IPC 值,即key_t )

    • size: 共享内存大小,一般设置为 4096 的倍数

    • shmflg: 创建共享内存的方式以及设置权限,由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

      //IPC_CREAT  可以单独使用
      //如果共享内存不存在,则开辟内存,函数返回值是新开辟的共享内存的ID
      //如果已经存在,则沿用已有的共享内存,函数返回值是已有的共享内存的
      
      //IPC_EXCL  无法单独使用
      //需要配合 IPC_CREAT 使用,即 IPC_CREAT | IPC_EXCL               
      
      //IPC_CREAT | IPC_EXCL 
      //如果共享内存不存在,则重新开辟,函数返回值是新开辟的共享内存的ID
      //如果已经存在,则报错
      
      //IPC_CREAT | IPC_EXCL | 0664
      //开辟共享内存的同时,设置共享内存的访问权限
      
  3. 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

shmat

void *shmat(int shmid, const void *shmaddr, int shmflg);
//                            一般为nullptr     一般为0

//示例
char* = (char*)shmat(shmid,nullptr,0);
  1. 功能:挂接共享内存至进程

  2. 参数:

    • shmid: 通过 shmget 创建的共享内存的返回值

    • shmaddr: 指定挂接地址, 通常传入 nullptr 即可, 让OS自动选择挂接地址

    • shmflg: 通常为0即可

  3. 返回值

    • 成功:返回一个void* 指针, 这里类似于malloc, 返回值需要根据使用来强制类型转换, 返回的地址是共享内存的起始地址,
    • 失败:如果失败则返回 (void*) -1, 并且错误码 errno 被设置

shmdt

int shmdt(const void *shmaddr);
  1. 功能:将共享内存段与当前进程脱离
  2. 参数
    • shmaddr: 由 shmat 所返回的指针
  3. 返回值
    • 成功:返回 0
    • 失败:返回 -1

注意:将共享内存段与当前进程脱离不等于删除共享内存段

shmctl

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//                     IPC_RMID           nullptr
  1. 功能:⽤于控制共享内存

  2. 参数

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

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

      // IPC_ STAT:把shmid_ ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值
      //IPC_ SET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
      //IPC_RMID:删除共享内存段
      
    • buf: 指向⼀个保存着共享内存的模式状态和访问权限的数据结构。

  3. 返回值

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

2. 消息队列( msg )

msg基本概念

消息队列是消息的链表,存放在内核中并由消息队列标识符表示。
  消息队列提供了一个从一个进程向另一个进程发送数据块的方法,每个数据块都可以被认为是有一个类型,接受者接受的数据块可以有不同的类型。
  但是同管道类似,它有一个不足就是每个消息的最大长度是有上限的(MSGMAX),每个消息队列的总的字节数(MSGMNB),系统上消息队列的总数上限(MSGMNI)。可以用cat /proc/sys/kernel/msgmax查看具体的数据。
  内核为每个IPC对象维护了一个数据结构struct ipc_perm,用于标识消息队列,让进程知道当前操作的是哪个消息队列。每一个msqid_ds表示一个消息队列,并通过msqid_ds.msg_first、msg_last维护一个先进先出的msg链表队列,当发送一个消息到该消息队列时,把发送的消息构造成一个msg的结构对象,并添加到msqid_ds.msg_first、msg_last维护的链表队列。在内核中的表示如下:

msg函数

头文件:

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

msgget

int msgget(key_t key, int msgflag);

//key =ftok()       IPC_CREAT|IPC_EXCL|0666
//例:  int msqid = msgget(key, IPC_CREAT | 0666);
  1. 功能:创建和访问一个消息队列

  2. 参数

    • key:某个消息队列的名字,用ftok()产生

    • msgflag:有两个选项 IPC_CREAT和IPC_EXCL,单独使用IPC_CREAT,如果消息队列不存在则创建之,如果存在则打开返回;单独使用IPC_EXCL是没有意义的;两个同时使用,如果消息队列不存在则创建之,如果存在则出错返回。

  3. 返回值:成功返回一个非负整数,即消息队列的标识码,失败返回-1

msgsnd

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

struct msgbuf {
    
    long mtype;       /* 标志,代表接收特定标记的数据块 */
    char mtext[1];    /* message data */
};

//例:
int send_msg(int msqid, int m_type, char *in)
{
                       //#define cilent_type 1
    struct my_msgbuf buf;
    buf.mtype = m_type;
    strncpy(buf.mtext, in, sizeof in);
    int n = msgsnd(msqid, (void *)&buf, sizeof buf.mtext, 0);
    if (n < 0)
    {
    
        cerr << "msg_send error! eoorr:" << errno << " strerror:" << strerror(errno) << endl;
        exit(1);
    }
    return 1;
}
  1. 功能:把一条消息添加到消息队列中

  2. 参数

    • msgid:由msgget函数返回的消息队列标识码
    • msgp:指针指向准备发送的消息
    • msgze:msgp指向的消息的长度(不包括消息类型的long int长整型)
    • msgflg:默认为0
  3. 返回值:成功返回0,失败返回-1

msgrcv

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  1. 功能:是从一个消息队列接受消息
  2. 参数
    • msgid:由msgget函数返回的消息队列标识码
    • msgp:指针指向准备发送的消息
    • msgz:msgp指向的消息的长度(不包括消息类型的long int长整型)
    • msgtyp接收指定标记的数据块
    • msgflg:默认为0
  3. 返回值:成功返回实际放到接收缓冲区里去的字符个数,失败返回-1

msgctl

int msgctl(int msqid, int cmd, struct msqid_ds *buf);: int n = msgctl(msgid, IPC_RMID, nullptr);
  1. 功能:消息队列的控制函数

  2. 参数

    • msqid:由msgget函数返回的消息队列标识码

    • cmd:有三个可选的值,在此我们使用IPC_RMID

    • buf:一般为 nullptr

      //IPC_STAT 把msqid_ds结构中的数据设置为消息队列的当前关联值
      //IPC_SET 在进程有足够权限的前提下,把消息队列的当前关联值设置为msqid_ds数据结构中给出的值
      //IPC_RMID 删除消息队列
      
  3. 返回值:成功返回0,失败返回-1

3. 信号量

sem函数

semget

int semget(key_t key, int num_sems, int sem_flags);
  1. 功能:它的作用是创建一个新信号量或取得一个已有信号量。

  2. 参数

    • key:整数值,可以理解为文件目录索引值、不同于id为该文件id名。

    • num_sems: 指定需要的信号量数目,它的值几乎总是1。

    • sem_flags:一组标志,当想要当信号量不存在时创建一个新的信号量 IPC_CREAT|0666

  3. 返回值:成功返回一个相应信号量集ID,失败返回-1.

semop

int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
  1. 功能:它的作用是改变信号量的值,进行PV操作的函数。原型为:

  2. 参数

    • sem_id: 是由semget返回的信号量标识符,

    • sem_opa:信号量结构体struct sembuf指针,该指针改变后的信号量

    • num_sem_ops: struct sembuf变量成员数量

//sembuf结构的定义如下:

struct sembuf{
    
    short sem_num;//除非使用一组信号量,否则它为0
    short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                 //一个是+1,即V(发送信号)操作。
    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
                  //并在进程没有释放该信号量而终止时,操作系统释放信号量
};

​ 由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv)

     P操作:sem_op= -1 <0,使得共享临界资源上锁,其它进程不得访问

     V操作:sem_op= 1 >0,使得共享临界资源解锁,其它进程可以访问

    semctl在初始化信号量的时候,union semum结构体中val就是信号量的初始化值,一般==1;

进行PV操作时,semop函数把 struct sembuf结构体中设置好的参数储存,此时val+1或-1,从而达到加锁解锁的目的,控制进程访问。

    举个例子,就是两个进程共享信号量sv=val=1,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,sem_op使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

semctl函数

int semctl(int sem_id, int sem_num, int cmd, ...);
  1. 功能:该函数用来直接控制信号量信息,初始化和删除信号量
  2. 参数:
    • sem_id:信号量标示符
    • sem_num:信号量集中有多个信号量,表示第几个信号量
    • cmd:SETVAL 用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
      IPC_RMID 用于删除一个已经无需继续使用的信号量标识符。

三、指令操作

  • 查看相关通信方式

    ipcs -m  //查看共享内存
    ipcs -q  //查看消息队列
    ipcs -s  //查看信号量
    

    ① 查看共享内存
    在这里插入图片描述

    ② 查看消息队列
    在这里插入图片描述
    ③ 查看信号量
    在这里插入图片描述

    nattch: 当前有多少进程挂接到该共享内存

  • 删除相关通信方式

    ipcrm -m [shmid]
    ipcrm -q [msgid]
    ipcrm -sv  [semid]
        
    ipcrm -a
    //使用ipcrm -a选项可以删除所有进程间通信资源
    

四、内核IPC结构

结构代码

struct ipc_ids{
    
		int size;   /*entries数组的大小*/
		int in_use;	/*entries数组已使用的元素个数*/
		int max_id;
		unsigned short  seq;
		unsigned short seq_max;
		struct semaphore sem; /*控制对ipc_ids结构的访问*/
		spinlock_t ary; 		/*自旋锁控制对数组entries的访问*/
		
    	struct ipc_id* entries;
     	//柔性数组,可以一直扩大大小
    	//ipc_id_ary [size,*p[0/1/2/3];
      	// *p[0] *p[1] 指向 shmid_kerne  sem_array  msg_queue  这些数据结构
	};

	struct ipc_id{
     struct ipc_perm *p;};

ipc_perm

 struct ipc_perm {
    
               key_t          __key;       /* Key supplied to msgget(2) */
               uid_t          uid;         /* Effective UID of owner */
               gid_t          gid;         /* Effective GID of owner */
               uid_t          cuid;        /* Effective UID of creator */
               gid_t          cgid;        /* Effective GID of creator */
               unsigned short mode;        /* Permissions */
               unsigned short __seq;       /* Sequence number */
           };

shmid_kerne

 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_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
               shmatt_t        shm_nattch;  /* No. of current attaches */
               ...
           };

msg_queue

struct msqid_ds {
    
               struct ipc_perm msg_perm;     /* Ownership and permissions */
               time_t          msg_stime;    /* Time of last msgsnd(2) */
               time_t          msg_rtime;    /* Time of last msgrcv(2) */
               time_t          msg_ctime;    /* Time of last change */
               unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
               msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
               msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
               pid_t           msg_lspid;    /* PID of last msgsnd(2) */
               pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
           };


struct msqid_ds {
     
	struct ipc_perm msg_perm; 
	struct msg *msg_first; /* 队列上第一条消息,即链表头*/ 
	struct msg *msg_last; /* 队列中的最后一条消息,即链表尾 */ 
	time_t msg_stime; /* 发送给队列的最后一条消息的时间 */ 
	time_t msg_rtime; /* 从消息队列接收到的最后一条消息的时间 */ 
	time_t msg_ctime; /* 最后修改队列的时间*/ 
	ushort msg_cbytes; /*队列上所有消息总的字节数 */ 
	ushort msg_qnum; /*在当前队列上消息的个数 */ 
	ushort msg_qbytes; /* 队列最大的字节数 */ 
	ushort msg_lspid; /* 发送最后一条消息的进程的pid */ 
	ushort msg_lrpid; /* 接收最后一条消息的进程的pid */ 
	};

sem_array

struct semid_ds {
    
	struct ipc_perm sem_perm;	/*IPC权限 */
	long	sem_otime; 		/* 最后一次对信号量操作(semop)的时间 */
	long	sem_ctime;		/* 对这个结构最后一次修改的时间 */
	struct sem		/*sem_base;		/* 在信号量数组中指向第一个信号量的指针 */
	struct sem_queue *sem_pending;		/* 待处理的挂起操作*/
	struct sem_queue **sem_pending_last;	 /	* 最后一个挂起操作 */
	struct sem_undo *undo; 		 /* 在这个数组上的undo 请求 */
	ushort	sem_nsems;	 	/* 在信号量数组上的信号量号 */
};

union semun {
    
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO                                     
    (Linux-specific) */
};

struct semid_ds {
    
	struct ipc_perm sem_perm;  /* Ownership and permissions */
	time_t          sem_otime; /* Last semop time */
	time_t          sem_ctime; /* Last change time */
	unsigned long   sem_nsems; /* No. of semaphores in set */
};
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_72982923/article/details/136282761

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法