Metainfo

  • 标题: 进程间通信
  • 日期: 2023-05-19 周五
  • 进度:完成
  • 感悟:进程完成隔离后,需要通过其它安全手段进行通信。主要手段有共享内存、信号量和消息队列
  • 标签:
    • #进程 #通信 #共享内存 #信号量 #消息队列
    • #review #☆☆☆☆

  • 进程之间的一大特性就是隔离性,但是进程之间仍然需要信息的交互,这里就用到进程间通信
  • 进程间通信(Inter-Process Communication):可以实现不同进程之间的数据传输和共享,以及进程的同步和互斥等操作。常见的 IPC 方式包括管道、消息队列、共享内存、信号量等。
    111

管道

  • 最简单的信息通信是通过磁盘中的文件,但是这种通信要使用磁盘文件,效率十分低下
  • [[文件操作#5 管道|有名管道]] (named pipe, FIFO)是文件系统中专门用来读写的文件,通过有名管道进行通信时,并没有经过磁盘,而是通过内核的管道缓冲区进行的数据传递
  • 对于有亲缘关系的进程而言,可以使用匿名管道进行通信,不需要在文件系统中创建文件,是在进程执行过程中动态创建和销毁的

测试

popen 和 pclose

  • popen 可以通过 fork 创建一个子进程,然后子进程可以执行一个命令
  • 子进程从管道中读取数据时是占用的[[文件操作#^6cf85f|标准输入]],往管道中写数据时占用的是标准输出
#include <stdio.h>

FILE* popen(const char* command, const char* type);
int pclose(FILE* stream);
  • 示例代码
#include <func.h>
int main()
{
	// 以读的形式通过 popen 创建子进程执行 printf 程序
    FILE* fp = popen("./printf", "r");
    char buf[1024] = {0};
    // 从管道中读数据
    fread(buf, 1, sizeof(buf), fp);
    printf("buf = %s", buf);
    pclose(fp);
}

#include <func.h>
int main()
{
	// printf 直接写到标准输出中,这里把标准输出重定向到管道中了
    printf("I am printf\n");
}

#include <func.h>
int main()
{
	// 以写的形式通过 popen 创建子进程执行 add 程序
    FILE* fp = popen("./add", "w");
    fwrite("3 4", 1, 3, fp);
    pclose(fp);
}

#include <func.h>
int main()
{
    int a, b;
	// scanf 从标准输入读数据,这里把标准输入重定向到管道中了
    scanf("%d%d", &a, &b);
    printf("a = %d, b = %d\n", a, b);
    printf("a + b = %d\n", a + b);
}

pipe

  • 系统调用 pipe 可以创建匿名管道,是半双工管道,可以通过两条管道实现全双工通信
  • pipe 管道只能在亲缘关系的进程中通信
  • 使用 pipe 前需要
#include <unistd.h>

int pipe(int pipefd[2]);
  • 使用 pipe 时,需要先创建一个大小为 2 的整型数组用来存储打开的文件描述符,其中 pipefd[0] 是读端的文件描述符,pipefd[1] 是写端的文件描述符
  • 通过 fork 创建的子进程和父进程共享[[进程#fork 函数|文件描述符]]

![[748B6C6EFC25C0049717899933C513E9.png]]

  • 示例代码
#include <func.h>
int main()
{
	// 新建两个 pipefd[2]
    int pipefd1[2] = {0};
    int pipefd2[2] = {0};
    pipe(pipefd1);
    pipe(pipefd2);
    pid_t pid = fork();
    char buf[1024] = {0};
    if (pid == 0)
    {
	    // 把 fd1 的读端和 fd2 的写端关闭
        close(pipefd1[0]);
        close(pipefd2[1]);
        // 向 fd1 的写端写数据
        write(pipefd1[1], "I am child", 10);
        // 从 fd2 的读端读数据
        read(pipefd2[0], buf, sizeof(buf));
        printf("From parent buf = %s\n", buf);
    }
    else
    {
	    // 把 fd1 的写端和 fd2 的读端关闭
        close(pipefd1[1]);
        close(pipefd2[0]);
        // 从 fd1 的读端读数据
        read(pipefd1[0], buf, sizeof(buf));
        // 向 fd1 的写端写数据
        write(pipefd2[1], "I am parent", 11);
        printf("From parent buf = %s\n", buf);
        wait(NULL);
    }
}

FIFO

  • FIFO 建立的是有名管道,和 shell 命令中的 [[文件操作#5 管道|mkfifo]] 相关的操作一样
  • 相关操作
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);

#include <unistd.h>
int unlink(const char* pathname); // 删除所有文件

#include <stdio.h>
int rename(const char* oldpath, const char* newpath); // 文件重命名、移动

#include <unistd.h>
int link(const char* oldpath, const char* newpath); // 给文件建立硬链接

共享内存

使用原因

  • 管道通信每次都会调用两次 pipe 系统调用,随着进程的增加,pipe 的使用次数会急剧增加
  • 使用管道通信还需要向缓冲区进行读写需要进行两次拷贝
  • 共享内存:对于每个进程中使用的地址都是虚拟地址,共享内存允许两个或多个进程共享一个给定的物理内存,为了实现共享,内核专门维护了一个用来存储共享内存信息的数据结构
  • 使用 [[进程#fork 函数|fork]] 创建子进程时的写时复制,如果只进行读则两个进程访问的物理内存空间是一样的,这里可以使用共享内存保证进程写时访问的物理空间内存也是一样的

system V 版本的共享内存

  • 内核使用一个非负整数键来区分不同的共享内存区域,不同的进程使用同一个键来定位共享内存段来进行通信
  • 键可以手动指定,也可以使用 ftok 生成
#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char* pathname, int proj_id); // 失败会返回-1

创建、获取共享内存

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

#include <sys/types.h>
#include <sys/shm.h>
void* shmat(int shmid, const void* shmaddr, int shmflg); // shmid 即 shmget 的返回值,shmflg 是对应共享内存的权限
/*
* SHM_RDONLY:指定只读方式连接共享内存。如果指定了该标志,则进程只能读取共享内存中的内容,不能修改共享内存中的数据
* SHM_RND:指定连接地址对齐。如果指定了该标志,则系统会将连接地址向下舍入到一个系统页大小的倍数
* SHM_REMAP:指定连接方式。如果指定了该标志,则进程连接共享内存时将使用映射方式,而不是复制方式。该标志通常用于提高共享内存的访问速度,但同时也会增加内存的使用量
* SHM_EXEC:指定连接内存段的权限。如果指定了该标志,则进程连接共享内存时将允许执行共享内存中的代码段。该标志通常用于共享内存中存储可执行代码的场合
* SHM_DEST:指定删除共享内存段。如果指定了该标志,则进程连接共享内存时将自动删除共享内存段。该标志通常用于在进程异常终止时自动清理共享内存段
* SHM_HUGETLB:指定使用大页面方式连接共享内存。如果指定了该标志,则系统会使用大页面(huge page)来分配共享内存,以提高内存访问速度。该标志通常用于需要处理大量数据的应用程序,但需要注意的是,该标志需要操作系统支持大页面功能
*/
  • 使用 shmget 接口来根据键来获取一个共享内存段,内存的大小是页表大小 4096 的整数倍,无论是新建还是引入
  • shmat 根据指定 shmid 来建立连接,shmaddr 一般设置为 NULL,表示在堆中自动分配区域映射共享内存段,shmflg 权限一般为 0
  • 共享内存创建以后,即使进程终止,这些 IPC 也不会释放在内核中的数据结构,可以使用 ipcs 来查看这些信息
$ ipcs
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status   
键         描述符      拥有者     权限       占据空间    连接数      状态
0x01031f46 2          le         600        4096       0(当程序运行时为1)    
$ ipcrm -m shmid # 手动删除共享内存

使用共享内存进行进程间通信

  • 只需要知道共享内存的键就可以进行通信,通过对 shmat 返回的指针进行强转成相应类型即可共享使用内存

解除共享内存映射

  • 使用 shmdt 即可解除进程的用户空间对共享内存的映射
#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void* shmaddr); 

查看、修改和删除共享内存

  • 使用 shmctl 来对共享内存进行查看、修改和删除
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
* cmd : IPC_STAT IPC_SAT IPC_RMID
*       查看     设置     删除:标记删除(无法有新进程指向,当所有共享内存都detach时删除)
*/
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;   /* Creation time/time of last
                                               modification via shmctl() */
	            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 */
}
struct ipc_perm{
			   key_t          __key;    /* Key supplied to shmget(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 + SHM_DEST and
                                           SHM_LOCKED flags */
               unsigned short __seq;    /* Sequence number */
}
  • 使用 IPC_SAT 修改共享内存中参数的流程
    1. 在栈上创建 shmid_ds 结构体并通过 shmctl 中的 IPC_STAT 对 shmid_ds 结构体设置
    2. 直接对 shmid_ds 结构体中的内容修改
    3. 通过 shmctl 中的 IPC_SAT 更新共享内存的参数
  • IPC_SAT 只可以修改 uid gid mode
#include <func.h>
int main()
{
    key_t key = ftok("file", 1);
    int shmid = shmget(key, 4096, IPC_CREAT|0600);
    // 创建 shmid_ds 结构体
    struct shmid_ds buf;
    // 通过 IPC_STAT 获取 shmid 共享内存的参数
    int ret = shmctl(shmid, IPC_STAT, &buf);
    ERROR_CHECK(ret, -1, "shmctl ipc_stat");
    printf("shm size is %lu, shm priority is %o\n", buf.shm_segsz, buf.shm_perm.mode);
    // 直接对参数进行修改
    buf.shm_perm.mode = 0666;
    // 更新 shmid 共享内存的参数
    ret = shmctl(shmid, IPC_SET, &buf);
    printf("shm size is %lu, shm priority is %o\n", buf.shm_segsz, buf.shm_perm.mode);
    // 标记删除共享内存,第三个参数不需要传,故添 NULL
    shmctl(shmid, IPC_RMID, NULL);
}

信号量

信号量介绍

  • 信号量(Semaphore)是一种用于实现进程间同步和互斥的机制
  • 信号量是一个计数器,用于控制多个进程对共享资源的访问。当一个进程需要访问共享资源时,需要首先获取信号量,如果信号量的值大于0,则将信号量的值减1,表示占用了共享资源,其他进程需要等待。如果信号量的值等于0,则表示共享资源已被占用,进程需要等待其他进程释放共享资源后才能访问,当一个进程访问完共享资源后,需要释放信号量,将信号量的值加1,表示共享资源已经被释放,其他进程可以继续访问
  • 信号量可以用于实现进程间同步和互斥。在进程间同步时,可以将信号量的初始值设置为0,表示共享资源当前不可用,需要等待其他进程或线程释放资源后才能访问。在进程间互斥时,可以将信号量的初始值设置为1,表示共享资源当前可用,只能有一个进程或线程访问。使用信号量可以保证多个进程或线程对共享资源的访问安全和正确性,避免了数据竞争和不一致性问题
  • PV 操作是对信号量进行操作的一种机制,用于实现进程间同步和互斥
    • P 操作(也叫做 wait 操作)用于获取或等待信号量
    • V 操作(也叫做 signal 操作)用于释放或增加信号量的值
    • P 操作和 V 操作是原子操作,可以保证多个进程同时对同一信号量进行操作时的正确性和一致性
    • 进行 P 操作时,如果信号量的值大于0,则将其减1,表示某个进程正在使用共享资源,其他进程需要等待。如果信号量的值等于0,则进程需要等待其他进程释放共享资源
    • 在进行 V 操作时,将信号量的值加1,表示某个进程已经释放了共享资源,其他进程可以继续使用共享资源
  • 二元信号量是一种只能取0或1值的信号量,也称为互斥信号量。当二元信号量的值为0时,表示共享资源已被占用,其他进程需要等待;当二元信号量的值为1时,表示共享资源可用,进程可以访问。在使用二元信号量实现进程间互斥时,只有一个进程可以访问共享资源,其他进程需要等待共享资源被释放后才能访问
  • 计数信号量是一种可以取任意整数值的信号量。计数信号量的值表示共享资源的可用数量。当计数信号量的值为0时,表示共享资源已被占用完毕,其他进程需要等待;当计数信号量的值大于0时,表示共享资源可用的数量,进程可以访问。在使用计数信号量实现进程间同步时,可以控制多个进程共享资源的访问顺序和数量,从而保证共享资源访问的正确性和一致性
  • 相关函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg); // 用于创建或获取一个信号量集,nsems表示创建的信号量集合中信号量的数量,key和semflg和shmget参数相同
int semctl(int semid, int semum, int cmd, ...); // 用于对信号量进行控制操作:获取信号量的值、设置信号量的值、删除信号量
                                                  // semum表示对semid中 下标为semum的信号 执行cmd命令并传入与semun相对应的可变参数 semun中是相应的可变参数列表
int semop(int semid, struct sembuf* sops, size_t nsops); // 对信号量进行操作:使用信号量、释放信号量
                                                         // sops结构体数组表示对semid中通过sum_num索引(从0开始)对应的信号量中的val执行sem_op操作,nsops表示操作信号量的数量,sem_flag先定义为SEM_UNDOd 如果要操作多个信号,则sops传入一个sembuf数组的首地址,nsops为对应的信号数量

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; /* Creation time/time of last
                                modification via semctl() */
    unsigned long   sem_nsems; /* No. of semaphores in set */
};

struct sembuf{
	unsigned short sum_num; /* semaphore number */
	short          sem_op;  /* semaphore operation */
	short          sem_flg; /* operation flags */
}

// 支持的 cmd 
// IPC_STAT IPC_SET IPC_RMID GETALL GETVAL SETALL SETVAL
  • 在使用 semget 来创建信号量集合时,如果不是采用私有的方式创建的集合,那么多个进程传入同一个 key 来重复使用 semget 时,不能修改信号量集合中信号量的数量
  • 当使用 semctl 中的 SETALL 来对信号量集合设置值时,第二个参数为 0 表示对整个信号量集合进行的操作
  • SEM_UNDO
    • 当一个进程在使用信号量时,如果该进程在信号量上出现异常终止,那么该信号量的值将会处于不一致的状态。为了避免这种情况,可以在信号量上设置 SEM_UNDO 标志
    • SEM_UNDO 标志的作用是在进程异常终止时自动将信号量的值恢复到其原始状态。具体来说,如果一个进程在使用信号量时被中断(例如进程收到了一个信号),那么内核会自动将该进程在信号量上进行的操作全部撤销,以确保信号量的值不会处于不一致的状态
    • 这种自动撤销操作是由内核完成的,并且只能在使用 semop()函数时才能生效
    • 在使用 SEM_UNDO 标志时,需要确保所有的 semop()函数调用都指定了该标志,否则可能会导致信号量的值不一致
    • 在使用 SEM_UNDO 标志时,需要注意信号量的值是否会超出其取值范围,如果超出范围,则可能会导致不可预期的行为
  • 可以使用 ipcrm 命令删除指定信号量集合
$ ipcrm -s semid
  • 在使用 semctl 时,cmd 命令支持 IPC_RMID,跟共享内存不同的是,信号量的 IPC_RMID 是直接删除

信号量性能

  • 在使用 p 操作时,如果信号量进行 sem_op 操作后为负数,则会处于睡眠状态,一直等待到操作完为正数的情况才会变为就绪状态
  • 使用 gettimeofday 来测试一次 p v 操作所花费的时间,其中用到了结构体 [[文件操作#5.2.2 timeval|timeval]]
#include <sys/time.h>

int gettimeofday(struct timeval* tv, struct timezone* tz); // tz 表示时区,可以传入 NULL
  • 示例代码
#include <func.h>
#define NUM 10000000
int main()
{
    // 创建共享内存
    key_t key = ftok("file", 1);
    int shmid = shmget(key, 4096, IPC_CREAT|0600);
    int* p = (int*) shmat(shmid, NULL, 0);
    ERROR_CHECK(p, (int *)-1, "shmat");
    // 创建二元信号
    int semid = semget(key, 1, IPC_CREAT|0600);
    semctl(semid, 0, SETVAL, 1);
    int val = semctl(semid, 0, GETVAL);
    printf("sem val = %d\n", val);
    // 定义semop中的sembuf结构体,实现p v操作
    struct sembuf sp, sv;
    sp.sem_num = 0;
    sp.sem_op = -1;
    sp.sem_flg = SEM_UNDO;
    sv.sem_num = 0;
    sv.sem_op = 1;
    sv.sem_flg = SEM_UNDO;
    p[0] = 0;
    // 统计程序运行花费时间
    struct timeval bg, ed;
    gettimeofday(&bg, NULL);
    if (fork() == 0)
    {
        for (int i = 0; i < NUM; i ++)
        {
            semop(semid, &sp, 1);
            p[0]++;
            semop(semid, &sv, 1);
        }
    }
    else
    {
        for (int i = 0; i < NUM; i ++)
        {
            semop(semid, &sp, 1);
            p[0]++;
            semop(semid, &sv, 1);
        }
        wait(NULL);
        printf("p[0] = %d\n", p[0]);
        gettimeofday(&ed, NULL);
        // ed 和 bg 两个结构体的 tv_sec 是 time_t 类型数据即长整型,如果乘的是 1e6 C++ 编译器会把 1e6 解释为一个 double 类型的浮点数常量,此时的数据类型从长整型强转换为了 double 类型
        printf("spend time = %ld\n", (ed.tv_sec - bg.tv_sec) * 1000000 + ed.tv_usec - bg.tv_usec);
    }
    shmdt(p);
}

生产者-消费者

  • 生产者消费者问题是最经典的进程间通信问题之一
  • 仓库的库存是有限的,消费者可以从仓库中取出商品,生产者可以放入商品,但是商品的数量不能超过库存的限度也不能取完以后再接着取用
  • 使用信号量可以用两种方式来实现,第一种是通过二元信号量 + 共享内存来实现,第二种是通过计数信号量来实现
  • 代码执行结束记得 ipcrm -a 把共享内存和信号量都删除,除此以外如果数值很奇怪需要查看有没有和文件名相同的后台进程在执行
  • 运行中出现的问题:两个 semid 相同的信号在两个进程中无法共享信号量,电脑重启后问题解决了
// 二元信号量 + 共享内存
#include <func.h>
int main()
{
    // 通过文件file生成key
    key_t key = ftok("file", 1);
    // 通过shmget生成大小为4096权限为0600的共享内存
    int shmid = shmget(key, 4096, IPC_CREAT | 0600);
    int *p = shmat(shmid, NULL, 0);
    // 设置初值 p[0] 仓库的库存为 10 p[1] 商品的数量为 0
    p[0] = 10;
    p[1] = 0;
    // 通过semget生成1个二元信号量
    int semid = semget(key, 1, IPC_CREAT | 0600);
    // 设置二元信号量的初值为1
    int ret = semctl(semid, 0, SETVAL, 1);
    ERROR_CHECK(ret, -1, "semctl setval");
    // 实现二元信号量的 P V 操作
    struct sembuf P, V;
    P.sem_num = 0;
    P.sem_op = -1;
    P.sem_flg = SEM_UNDO;
    V.sem_num = 0;
    V.sem_op = 1;
    V.sem_flg = SEM_UNDO;
    // 创建生产者子进程
    if (fork() == 0)
    {
        while (1)
        {
            // P V 操作
            semop(semid, &P, 1);
            if (p[0] > 0)
            {
                printf("before produce product = %d, space = %d\n", p[1], p[0]);
                p[0] --;
                p[1] ++;
                printf("after produce product = %d, space = %d\n", p[1], p[0]);
                puts("-----------------------");
            }
            semop(semid, &V, 1);
            sleep(1);
        }
    }
    // 创建第一个消费者子进程
    else if (fork() == 0)
    {
        while (1)
        { 
            semop(semid, &P, 1);
            if (p[1] > 0)
            {
                printf("before consume product = %d, space = %d\n", p[1], p[0]);
                p[1] --;
                p[0] ++;
                printf("after sonsume product = %d, space = %d\n", p[1], p[0]);
                puts("-----------------------");
            }
            semop(semid, &V, 1);
            sleep(3);
       }
    }
    // 父进程为第二个消费者子进程
    else
    {
        while (1)
        { 
            semop(semid, &P, 1);
            if (p[1] > 0)
            {
                printf("before consume product = %d, space = %d\n", p[1], p[0]);
                p[1] --;
                p[0] ++;
                printf("after consume product = %d, space = %d\n", p[1], p[0]);
                puts("-----------------------");
            }
            semop(semid, &V, 1);
            sleep(3);
       }
        wait(NULL);
    }
}
// 计数信号量
#include <func.h>
int main()
{
    key_t key = ftok("file", 1);
    int semid = semget(key, 2, IPC_CREAT | 0600);
    // 通过 array 设置 semid 的值分别为 10(库存) 0(商品数量)
    unsigned short array[2] = {10, 0};
    int ret = semctl(semid, 2, SETALL, array);
    ERROR_CHECK(ret, -1, "semctl setval");
    if (fork() == 0)
    {
        // 生产者的操作
        struct sembuf p[2];
        p[0].sem_num = 0;
        p[0].sem_op = -1;
        p[0].sem_flg = SEM_UNDO ;
        p[1].sem_num = 1;
        p[1].sem_op = 1;
        p[1].sem_flg = SEM_UNDO;
        while (1)
        {
            // 将导致进程阻塞的信号操作放前面来决定是否要生产
            semop(semid, &p[0], 1);
            printf("before produce product = %d, space = %d\n", semctl(semid, 1, GETVAL), semctl(semid, 0, GETVAL) + 1);
            printf("after produce product = %d, space = %d\n", semctl(semid, 1, GETVAL) + 1, semctl(semid, 0, GETVAL));
            puts("-----------------------");
            // 将其余操作在末尾实现
            semop(semid, &p[1], 1);
            sleep(1);
        }
    }
    else if (fork() == 0)
    {
        // 消费者的操作
        struct sembuf c[2];
        c[0].sem_num = 0;
        c[0].sem_op = 1;
        c[0].sem_flg = SEM_UNDO;
        c[1].sem_num = 1;
        c[1].sem_op = -1;
        c[1].sem_flg = SEM_UNDO;
        while (1)
        {
            semop(semid, &c[1], 1);
            printf("before consume product = %d, space = %d\n", semctl(semid, 1, GETVAL) + 1, semctl(semid, 0, GETVAL));
            printf("after consume product = %d, space = %d\n", semctl(semid, 1, GETVAL), semctl(semid, 0, GETVAL) + 1);
            puts("-----------------------");
            semop(semid, &c[0], 1);
            sleep(5);
       }
    }
    else
    {
        struct sembuf c[2];
        c[0].sem_num = 0;
        c[0].sem_op = 1;
        c[0].sem_flg = SEM_UNDO;
        c[1].sem_num = 1;
        c[1].sem_op = -1;
        c[1].sem_flg = SEM_UNDO;
        while (1)
        {
            semop(semid, &c[1], 1);
            printf("before consume product = %d, space = %d\n", semctl(semid, 1, GETVAL) + 1, semctl(semid, 0, GETVAL));
            printf("after consume product = %d, space = %d\n", semctl(semid, 1, GETVAL), semctl(semid, 0, GETVAL) + 1);
            puts("-----------------------");
            semop(semid, &c[0], 1);
            sleep(5);
        }
        wait(NULL);
    }
}
  • 这里用到了 [[文件操作#5.2.2 timeval|timeval]]

消息队列

  • 区别于管道的流式结构,消息队列最显著的特点就是维持了一个消息链表,可以使用先进先出的方式来取出消息,且存在边界
  • 消息队列使用的时候不需要先打开读端写端,可以直接往其中写入数据
  • 相关函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg); // return value 为 0 时表示正常 msgflag 通常为 0
ssize_t msgrcv(int msqid, void* msgp, size_t msgsz, long msgtype, int msgflg); // return value 为 接收信息的长度时表示正常 msgflag 通常为 0
int msgctl(int msqid, int cmd, struct msqid_ds *buf); // 通常用 IPC_RMID 来及时的删除消息队列 与 ipcrm -q msqid 实现的效果相同

// msgp 指针常指向 msgbuf 这样的结构体,mtype 储存获取该消息的索引,mtext 储存消息内容,使用消息队列时需要对 msgp 进行重写
struct msgbuf {
	long mtype;
	char mtext[1];
}
  • 示例代码
// msgsnd
#include <func.h>
typedef struct msgbuf {
    long mtype;
    char mtext[256];
} Msg;
int main()
{
    key_t key = ftok("file", 1);
    int msqid = msgget(key, IPC_CREAT | 0600);
    ERROR_CHECK(msqid, -1, "msgget");
    Msg msg1;
    Msg msg2;
    // mtype 的值从 1 开始
    msg1.mtype = 1;
    strcpy(msg1.mtext, "Hello ");
    msg2.mtype = 2;
    strcpy(msg2.mtext, "World!");
    int ret = msgsnd(msqid, &msg1, strlen(msg1.mtext), 0);
    ERROR_CHECK(ret, -1, "msgsnd");
    msgsnd(msqid, &msg2, strlen(msg2.mtext), 0);
    puts("send over!");
}

// msgrsv
#include <func.h>
typedef struct msgbuf{
    long mtype;
    char mtext[256];
} Msg;
int main()
{
    key_t key = ftok("file", 1);
    int msqid = msgget(key, IPC_CREAT | 0600);
    Msg msg1;
    Msg msg2;
    memset(&msg1, 0, sizeof(msg1));
    puts("begin recive!");
    // 第四个参数表示 mtype 通过 mtype 索引到消息队列中具体的消息 如果这个参数是 0 则会取出第一个消息
    msgrcv(msqid, &msg1, sizeof(msg1.mtext), 1, 0);
    msgrcv(msqid, &msg2, sizeof(msg2.mtext), 2, 0);
    printf("Message send is %s%s\n", msg1.mtext, msg2.mtext);
    msgctl(msqid, IPC_RMID, NULL);
    puts("message has been delated!");
}

proc 文件系统

  • proc 文件系统是一种虚拟文件系统,它提供了一种以文件的形式来访问内核数据结构的方式。通过 proc 文件系统,用户可以直接访问系统内核和进程等各种信息
  • proc 文件系统是一种特殊的文件系统,它不存储在磁盘上,而是在内存中动态生成的。当用户访问一个 proc 文件时,内核会根据用户的请求动态生成相应的内容,并将其返回给用户。因此,proc 文件系统提供了一种非常灵活的方式来访问内核数据结构,同时也提供了一个方便的接口来获取系统状态信息
  • proc 文件系统中,每个进程都有一个以其进程 ID 命名的目录,在该目录下包含一些文件,这些文件包含了进程的一些信息,如进程的状态、命令行参数、内存使用情况等。用户可以通过访问这些文件来获取进程的相关信息
  • 其中 N 为正在运行的进程,如下所示
1     1123  1285  1332  1543  197  215   2265  244   258   36   4382  60    72   79   87          cpuinfo        kmsg          self
10    1125  1290  1333  1544  198  216   227   245   259   37   4703  6061  723  8    88          crypto         kpagecgroup   slabinfo