情報処理において共有メモリ(きょうゆう-)とは、複数のプログラムが同時並行的にアクセスするメモリである。
背景
コンテナ⇔ホスト間やコンテナ⇔コンテナ間で共有メモリを使用できるって話を聞いたので試してみた記事
今の自分には用途は分からないけど面白そうだったので書いてみました。
共有メモリとは
共有メモリとはプロセス間で同じメモリを共有します。
1つのプロセスがメモリ上に他のプロセスからもアクセスできる領域を作成することで共有を可能にします。
共有メモリ以外にもソケット通信やセマフォを利用してプロセス間で通信を行えますが、共有メモリのメリットは処理速度が高速な点です。
共有メモリは一度作成してしまえばカーネルを通さずにプロセス内のメモリアクセスと同等の速さで行えます。
勿論デメリットもあります。それは排他制御の機能がない点で、自前で排他処理が必要となりプログラムが複雑化する点です。
共有メモリの仕組み
仕組み的には仮想メモリの仕組みを使って実現しています。
プロセスがメモリへアクセスする際は全てページテーブル経由でのアクセスとなっています。
このプロセステーブルはプロセスごとに独立して衝突することはありませんが、共有メモリの場合は、
2プロセスがそれぞれ物理ページフレーム番号を参照するといった仕組みです。
プロセスAの仮想ページフレーム1が参照している物理ページフレームが2のとき
プロセスBの仮想ページフレーム4が参照している物理ページフレームも2となります。
物理ページフレーム番号を同一のものを2プロセスで共有しているイメージです。
(ここら辺は実装までは読んでいないのでこちらをご参照ください!)
関連するシステムコール
共有メモリの操作には下記のシステムコールを利用します。
shmget(2) : 共有メモリ・セグメント識別子を獲得する
shmat(2) : 自プロセスのデータセグメントにマップする
shmdt(2) : 共有メモリをアンマップする
shmctl(2) : 共有メモリをシステム上から削除する
共有メモリが存在している間はipcsコマンドで確認することができます。
以下の例ではshmid=360457でバイトが512Bの共有メモリが作成されていることが確認できます。
また、nattchが1であることから共有メモリへアタッチしているプロセスが1つあることも確認できます。
ちなみに、共有メモリを作成しただけでは物理メモリの使用量は変わりません。
実際にメモリを操作しない限りはRSSの値は変わりません。
$ ipcs -m
------ 共有メモリセグメント --------
キー shmid 所有者 権限 バイト nattch 状態
0x00000000 65536 root 666 512 0
0x00000000 98305 root 666 512 0
0x00000000 131074 root 666 512 0
0x00000000 163843 root 666 512 0
0x00000000 196612 root 666 512 0
0x00000000 229381 root 666 512 0
0x00000000 262150 root 666 512 0
0x00000000 294919 root 666 512 0
0x00000000 327688 root 666 512 0
0x00000000 360457 root 666 512 1 #これ
オプション | 説明 |
---|---|
-m | シェアードメモリセグメントを指定する。 |
-q | メッセージキューを指定する。 |
-s | セマフォを指定する。 |
-a | すべてのリソースの情報が出力される(これは、省略時の動作である)。 |
-t | リソースが最後に変更された時間を出力する。 |
-p | リソースの所有、作成、最終変更を示すプロセスIDを出力する。 |
-c | リソースの作成ユーザーおよびグループの情報を出力する。 |
-l | 各リソースの上限値を出力する。 |
-u | 各リソースの使用状況を示すサマリが出力される. |
作成された共有メモリエリアはshmid_ds構造体で表されます。
共有メモリのサイズや使用しているプロセス数と所有者などが確認できます。
作成された共有メモリエリアはshmid_ds構造体で表されます。
共有メモリのサイズや使用しているプロセス数と所有者などが確認できます。
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int 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 */
unsigned short shm_cpid; /* pid of creator */
unsigned short shm_lpid; /* pid of last operator */
short shm_nattch; /* no. of current attaches */
/* the following are private */
unsigned short shm_npages; /* size of segment (pages) */
unsigned long *shm_pages; /* array of ptrs to frames -> SHMMAX */
struct vm_area_struct *attaches; /* descriptors for attaches */
};
Dockerと共有メモリ
コンテナ間でプロセス間通信に利用される、セマフォ、メッセージキュー、共有メモリといった機構は分離されています。これは"namespace"というカーネルの機能の一つを使用して実装しています。
異なる名前空間の共有メモリやセマフォにアクセスできないようになっている為コンテナ間やコンテナ⇔ホスト間ではこの空間は衝突することがありません。
今回の記事ではこのIPC namespaceを分離しないコンテナを作成し共有メモリを使っていきます。
ちなみにIPCのnamespaceに関してのカーネルでの実装は下記で見れます。
linux/ipc/namespace.c
共有メモリを使用するための簡易プログラム
共有メモリを操作するための簡単なプログラムを下記へ記載します。
recv.cでは共有メモリの作成とshmidを表示します。
表示した後はその領域を5秒ごとに表示し続けます。
send.cではshmidをキーとしてその領域へ文字列を書き込みます。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(int argc, char **argv)
{
int memid;
char *adr;
// 共有メモリの作成
if((memid = shmget(IPC_PRIVATE, 512, IPC_CREAT|0666)) == -1){
perror("shmget");
exit(-1);
}
printf("Share Memory ID = %d\n",memid);
if(( adr = (char *)shmat(memid, NULL, 0)) == (void *)-1){
perror("shmat");
} else {
strcpy(adr,"Initial");
while(1){
printf("%s\n",adr);
if (strcmp(adr, "end") == 0) {
break;
}
sleep(1);
}
if(shmdt(adr) == -1) {
perror("shmdt");
}
}
if(shmctl(memid, IPC_RMID, 0) == -1){
perror("shmctl");
exit(EXIT_FAILURE);
}
return 0;
}
shmgetの第一引数には共有メモリ・セグメントに対するキーを指定しています。
System V 共有メモリーセグメントの識別子を返します。 key の値が IPC_PRIVATE の場合、
もしくは key に対応する共有メモリーセグメントが存在せず、
shmflg に IPC_CREAT が指定されていた場合、 新しい共有メモリセグメントを作成します。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(int argc, char **argv)
{
int memid;
char *adr;
if( argc <= 2) {
fprintf(stderr, "Usage: shm_writer shm_id string\n");
exit(EXIT_FAILURE);
}
memid = atoi(argv[1]);
if(( adr = (char *)shmat(memid, 0, 0)) == (void *)-1) {
perror("shmat");
} else {
strcpy(adr, argv[2]);
fprintf(stderr, Operation completion."\n");
if( shmdt(adr) == -1) {
perror("shmdt");
}
}
}
※上記サンプルで使用しているshmget等はC言語のライブラリ関数では無いのでコンパイラにより使えない場合はあります
ホスト⇔コンテナ
ここではコンテナ内で共有メモリを作成し、そのメモリ操作をホストで行う例を記載します。
コンテナではCをビルドして実行するだけでよいのでとりあえずビルド環境のみが入ったコンテナを用意します。
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y build-essential
RUN mkdir -p /myapp
$ sudo docker build -t recv .
$ sudo docker container run --ipc=host -it recv /bin/bash -v src:/myapp
# コンテナ内
root@29ebf4acb91c:~# gcc recv.c -o recv
root@29ebf4acb91c:~# ./recv
Share Memory ID = 327688 # これをホスト側プログラムのsend.cの引数へ与えます
各プロセスには/proc/${PID}/nsサブディレクトリがありここを確認することで各namespace
# ホストで実行
$ ls -l /proc/$$/ns
合計 0
lrwxrwxrwx 1 root root 0 2月 4 19:12 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 2月 4 19:12 ipc -> 'ipc:[4026531839]' #コンテナ内のbashプロセスのipc namespaceの情報
lrwxrwxrwx 1 root root 0 2月 4 19:12 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 2月 4 19:12 net -> 'net:[4026531993]'
lrwxrwxrwx 1 root root 0 2月 4 19:12 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 2月 4 19:12 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 2月 4 19:12 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 2月 4 19:12 uts -> 'uts:[4026531838]'
# コンテナ内で実行
$ ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Feb 4 10:12 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb 4 10:12 ipc -> 'ipc:[4026531839]' #★
lrwxrwxrwx 1 root root 0 Feb 4 10:12 mnt -> 'mnt:[4026532210]'
lrwxrwxrwx 1 root root 0 Feb 4 10:12 net -> 'net:[4026532214]'
lrwxrwxrwx 1 root root 0 Feb 4 10:12 pid -> 'pid:[4026532212]'
lrwxrwxrwx 1 root root 0 Feb 4 10:12 pid_for_children -> 'pid:[4026532212]'
lrwxrwxrwx 1 root root 0 Feb 4 10:12 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb 4 10:12 uts -> 'uts:[4026532211]'
コンテナ⇔コンテナ
コンテナ間でも同様に共有メモリを扱えます。
ホストと共有するコンテナを起動したら別プロンプトで下記を実行します。
--ipcのオプションがホストではなくcontainerとコンテナIDを指定します。
$ sudo docker run --ipc=container:<id> <image>
共有してみる
$ sudo docker run --ipc=container:2cec60912f85 -it recv /bin/bash
$ ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Feb 4 10:14 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb 4 10:14 ipc -> 'ipc:[4026531839]' #上と値が一致している
lrwxrwxrwx 1 root root 0 Feb 4 10:14 mnt -> 'mnt:[4026532384]'
lrwxrwxrwx 1 root root 0 Feb 4 10:14 net -> 'net:[4026532388]'
lrwxrwxrwx 1 root root 0 Feb 4 10:14 pid -> 'pid:[4026532386]'
lrwxrwxrwx 1 root root 0 Feb 4 10:14 pid_for_children -> 'pid:[4026532386]'
lrwxrwxrwx 1 root root 0 Feb 4 10:14 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb 4 10:14 uts -> 'uts:[4026532385]'
余談 セマフォとは
セマフォとは複数プロセスによるアクセス制御に使われる仕組みのこと。
よく使われるバイナリセマフォは、0と1の状態を持ち、プロセスが処理を続行可能であるか否かの判断を行うために用いられる。
似たような排他制御にミューテックスがあるが違いはミューテックスはロックと非ロックの管理だけだだがセマフォでは
回数の管理を行える。つまりセマフォは個数カウンタ付きのミューテックス。
他にスピンロックというのがあります。スピンロックはロックを獲得(変数のチェック)するまで、ビジーウエイトと言ってループで待ちますが、
セマフォを他のタスクに実行を譲ります。
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
あとがき
IPCについて触れるのがメインの記事でした。
色々調べているとIPC使わなくてもコンテナそれぞれで同一のボリュームをマウントし、ファイルをmmapすることで
似たような通信が行えるといった記事をみました。なるほど、、、って感じですね。
面白そうなので一回やってみようと思います。
誤り等あれば教えてください!