在很久以前,我们学过一个函数 fcntl,当时学习它时挺费劲,因为一个函数,竟然有如此多的功能。今天要学习 shmctl 函数,也是一样。这种以 cntl/ctl 为后缀的函数,往往都具备这样的特性:它们都有一个命令控制参数,你传递不同的命令,这个函数有具备着不同的功能,可谓身兼数职说的就是它吧。

1. shmctl 函数

1.1 函数原型

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

这个函数的参数 shmid 是 ipc 内核对象的 id。cmd 是命令,这里只介绍 IPC_STAT, IPC_SET 和 IPC_RMID 三个命令。其中 IPC_RMID 命令表示删除内核对象。实际上不只是针对 shmctl 这个函数,消息队列和信号量的控制函数(msgctl 和 semctl) 的命令也是这样。

先看看第三个参数 shmid_ds 结构体。

1.2 shmid_ds 结构体

有时候,我们需要获取 IPC 内核对象的相关信息,这时候要怎么办呢? 毕竟 ipc 内核对象它位于内核空间中,普通用户程序根本无法直接访问,为此 linux 提供了这样的结构体 shmid_ds 给用户态的程序使用。shmctl 函数可以将位于内核空间的 ipc 内核对象的信息拷贝一份给用户空间的 shmid_ds,也可以通过将此结构体中的信息拷贝到内核空间用来设置 ipc 内核对象,具体是哪种情况依赖于参数 cmd。

当 cmd = IPC_STAT 的时候, 第三个参数 buf 用于接收返回值,当 cmd = IPC_SET 的时候,第三个参数用于传递值并设置内核对象。当 cmd = IPC_RMID 的时候,第三个参数被忽略,可以直接传 NULL.

该结构体的详细内容见注释。

struct shmid_ds {
struct ipc_perm shm_perm; /* 所有权和权限位 */
size_t shm_segsz; /* 段大小 */
time_t shm_atime; /* 最后挂接时间 */
time_t shm_dtime; /* 最后卸载时间 */
time_t shm_ctime; /* 最后改变当前结构体的时间(由IPC_SET命令改变) */
pid_t shm_cpid; /* 创建 ipc 内核对象的进程 pid */
pid_t shm_lpid; /* 最后执行 shmat/shmdt 的进程 pid */
shmatt_t shm_nattch; /* 挂接进程个数 */

其中成员 shm_perm 是所有 System V IPC 内核对象都包含的,它的结构如下:

struct ipc_perm {
uid_t uid; /* 所有者有效用户 id */
gid_t gid; /* 所有者有效用户组 id */
uid_t cuid; /* 创建者有效用户 id */
gid_t cgid; /* 创建者有效用户组 id */
unsigned short mode; /* 权限位*/

2. 实验

下面的程序 shmctl 可以用来创建、删除内核对象,也可以挂接、卸载共享内存,还可以打印、设置内核对象信息。具体使用方法具体见下面的说明:

  • ​./shmctl -c​​ : 创建内核对象。
  • ​./shmctl -d​​ : 删除内核对象。
  • ​./shmctl -v​​ : 显示内核对象信息。
  • ​./shmctl -s​​ : 设置内核对象(将权限设置为 0600)。
  • ​./shmctl -a​​ : 挂接和卸载共享内存(挂接 5 秒后,再执行 shmdt,然后退出)。

shmctl 函数的代码见 2.5 节。

2.1 创建内核对象


57-System V 共享内存-shmctl_共享内存


图1 创建内核对象,打印内核对象信息


2.2 设置内核对象


57-System V 共享内存-shmctl_ipc_02


图2 将内核对象权限设置为 0600


2.3 挂接后内核对象信息

先在另一个终端执行 ​​./shmctl -a​​​,然后在当前终端执行​​./shmctl -v​​(注意手速,5秒内要搞定)。


57-System V 共享内存-shmctl_ipc_03


图3 挂接共享内存后 ipc 内核对象的信息


2.4 卸载后内核对象信息

当 2.3 节上的 ​​./shmctl -a​​​ 结束后,再执行一次 ​​./shmctl -v​​.


57-System V 共享内存-shmctl_进程间通信_04


图4 卸载共享内存后 ipc 内核对象的信息


2.5 shmctl 程序代码

这段代码比较长,你可以直接复制过去进行编译。

// shmctl.c
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>

#define ASSERT(res) if((res)<0){perror(__FUNCTION__);exit(-1);}

// 打印 ipc_perm
void printPerm(struct ipc_perm *perm) {
printf("euid of owner = %d\n", perm->uid);
printf("egid of owner = %d\n", perm->gid);
printf("euid of creator = %d\n", perm->cuid);
printf("egid of creator = %d\n", perm->cgid);
printf("mode = 0%o\n", perm->mode);
}

// 打印 ipc 内核对象信息
void printShmid(struct shmid_ds *shmid) {
printPerm(&shmid->shm_perm);
printf("segment size = %d\n", shmid->shm_segsz);
printf("last attach time = %s", ctime(&shmid->shm_atime));
printf("last detach time = %s", ctime(&shmid->shm_dtime));
printf("last change time = %s", ctime(&shmid->shm_ctime));
printf("pid of creator = %d\n", shmid->shm_cpid);
printf("pid of last shmat/shmdt = %d\n", shmid->shm_lpid);
printf("No. of current attaches = %ld\n", shmid->shm_nattch);
}

// 创建 ipc 内核对象
void create() {
int id = shmget(0x8888, 123, IPC_CREAT | IPC_EXCL | 0664);
printf("create %d\n", id);
ASSERT(id);
}

// IPC_STAT 命令使用,用来获取 ipc 内核对象信息
void show() {
int id = shmget(0x8888, 0, 0);
ASSERT(id);
struct shmid_ds shmid;
ASSERT(shmctl(id, IPC_STAT, &shmid));
printShmid(&shmid);
}

// IPC_SET 命令使用,用来设置 ipc 内核对象信息
void set() {
int id = shmget(0x8888, 123, IPC_CREAT | 0664);
ASSERT(id);
struct shmid_ds shmid;
ASSERT(shmctl(id, IPC_STAT, &shmid));
shmid.shm_perm.mode = 0600;
ASSERT(shmctl(id, IPC_SET, &shmid));
printf("set %d\n", id);
}

// IPC_RMID 命令使用,用来删除 ipc 内核对象
void rm() {
int id = shmget(0x8888, 123, IPC_CREAT | 0664);
ASSERT(id);
ASSERT(shmctl(id, IPC_RMID, NULL));
printf("remove %d\n", id);
}

// 挂接和卸载
void at_dt() {
int id = shmget(0x8888, 123, IPC_CREAT | 0664);
ASSERT(id);
char *buf = shmat(id, NULL, 0);
if (buf == (char*)-1) ASSERT(-1);
printf("shmat %p\n", buf);
sleep(5); // 等待 5 秒后,执行 shmdt
ASSERT(shmdt(buf));
printf("shmdt %p\n", buf);
}



int main(int argc, char *argv[]) {
if (argc < 2) {
printf("usage: %s <option -c -v -s -d -a>\n", argv[0]);
return -1;
}

printf("I'm %d\n", getpid());

if (!strcmp(argv[1], "-c")) {
create();
}
else if (!strcmp(argv[1], "-v")) {
show();
}
else if (!strcmp(argv[1], "-s")) {
set();
}
else if (!strcmp(argv[1], "-d")) {
rm();
}
else if (!strcmp(argv[1], "-a")) {
at_dt();
}

return 0;
}

3. 总结

  • 知道如何获取 ipc 内核对象的信息
  • 掌握 shmctl 函数的使用