目次
(2) セマフォを実現し、プロデューサー・コンシューマー・プログラムを使用してチェックする
(3) プロデューサー/コンシューマー プログラムを実行する
1. 実験の目的
1. プロセスの同期と相互排除の概念についての理解を深めます。
2. セマフォの使用法をマスターし、それを生産者と消費者の問題の解決に適用します。
3. セマフォの実現原理をマスターします。
2. 実験内容
(1) セマフォを使用して生産者と消費者の問題を解決する
Ubuntu 上でアプリケーションを作成し、古典的な生産者と消費者の問題を解決し、次の機能を実現します。 pc.c
- プロデューサー プロセスと N 個のコンシューマー プロセスを作成します (N > 1)
- ファイルを使用して共有バッファを作成する
- プロデューサ プロセスは、整数 0、1、2、...、M を共有バッファに順次書き込みます (M >= 500)
- コンシューマプロセスは共有バッファから1つずつ読み込み、読み取った番号をバッファから削除し、「このプロセスID + 削除した番号」を標準出力に出力します。
- バッファには同時に最大10 個の数値しか保存できません
【例】考えられる出力効果
10: 0
10: 1
10: 2
10: 3
10: 4
11: 5
11: 6
12: 7
10: 8
12: 9
12: 10
12: 11
12: 12
……
11: 498
11: 499
- プロセス ID の順序は大幅に変更される可能性がありますが、コロンの後の数字は 0 から始まり 1 ずつ増加する必要があります。
pc.c
また 、、 、などのセマフォ関連のシステムコールは、で使用される ため 、Linux 0.11 で独自に実装する必要があります。 sem_open()
sem_close()
sem_wait()
sem_post()
(2) セマフォを実現し、プロデューサー・コンシューマー・プログラムを使用してチェックする
Linux バージョン 0.11 はまだセマフォを実装していないため、Linus はこの困難な仕事をあなたに任せています。POSIX (UNIX のポータブル オペレーティング システム インターフェイス) 仕様に完全に準拠した模倣バージョンのセマフォを実現できれば、間違いなく達成感が得られるでしょう。しかし、当面は時間がないので、最初に POSIX 風のセマフォの縮小版のセットを実装することができます。その関数プロトタイプは標準とまったく同じではなく、次の 4 つのシステムのみが含まれています呼び出し:
sem_t *sem_open(const char *name, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_unlink(const char *name);
上記 4 つの機能の具体的な機能と関連パラメーターについては、以下に説明します。
sem_open()
関数 | セマフォを作成するか、既存のセマフォを開きます。 |
パラメータ |
|
戻り値 | 作成またはオープンが成功した場合、戻り値はセマフォの一意の識別子 (カーネル内のアドレス、ID など) であり、他の 2 つのシステム コールで使用されます。失敗した場合は、値は NULL です。 sem_open() |
sem_wait()
関数 | これはセマフォの P アトミック操作であり、その機能はセマフォの値から 1 を減算することです。実行を継続するための条件が満たされない場合、呼び出しプロセスはセマフォ sem で待機させられます。 |
パラメータ | sem : セマフォへのポインタ。 |
戻り値 | 成功した場合は 0 を返し、失敗した場合は -1 を返します。 |
sem_post()
関数 | これはセマフォの V アトミック操作であり、その機能はセマフォの値に 1 を加算することです。sem を待機しているプロセスがある場合は、そのうちの 1 つを起動します。 |
パラメータ | sem : セマフォへのポインタ。 |
戻り値 | 成功した場合は 0 を返し、失敗した場合は -1 を返します。 |
sem_unlink()
関数 | name という名前のセマフォを削除します。 |
パラメータ | name : セマフォの名前。 |
戻り値 | 成功した場合は 0 を返し、失敗した場合は -1 を返します。 |
【実験のヒント】
上記 4 つのシステム コールの機能を実現するには、Linux 0.11 のkernel
ディレクトリ新しいsem.c
ファイルを作成します。次に、実装されたセマフォをテストするために、Linux 0.11 で実行できるように Ubuntupc.c
から。
3. 実験の準備
1、信号量
セマフォ (英語では semaphore) は、オランダの科学者であり、チューリング賞を受賞した EW ダイクストラによって最初に設計されました。オペレーティング システムの教科書の「プロセスの同期」の部分は詳細に説明されています。セマフォは、複数のプロセスの連携が合理的かつ秩序正しく行われることを保証します。
Linux セマフォは POSIX 仕様に準拠しており、ユーザーは man sem_overview
関連情報を表示できます。
この実験に関係するセマフォ関連のシステム コールには、次のものが含まれます。sem_open()
、
sem_wait()
、
sem_post()
sem_unlink()
生産者と消費者の問題
生産者と消費者の問題の解決策は、ほとんどすべてのオペレーティング システムの教科書に記載されており、その基本構造は次のとおりです。
Producer()
{
// 生产一个产品 item;
/* 空闲缓存资源 */
P(Empty);
/* 互斥信号量 */
P(Mutex);
// 将item放到空闲缓存中;
V(Mutex);
/* 产品资源 */
V(Full);
}
Consumer()
{
P(Full);
P(Mutex);
//从缓存区取出一个赋值给item;
V(Mutex);
// 消费产品item;
V(Empty);
}
- 明らかに、このプロセスをデモンストレーションするときは、2 つのタイプのプロセスを作成する必要があります。1 つのタイプは関数を実行し 、もう 1 つのタイプは関数を実行します。
Producer()
Consumer()
2. マルチプロセス共有ファイル
Linux で C 言語を使用すると、次の 3 つの方法を使用してファイルを読み書きできます (ただし、Linux 0.11 では最初の 2 つの方法のみが使用できます)。
(1) 標準の C 、、、などを使用します。fopen()
fread()
fwrite()
fseek()
fclose()
(2) システムコール 、、、 など を使用する。open()
read()
write()
lseek()
close()
(3) メモリイメージファイル経由で システムコールを使用します。mmap()
fork()
呼び出しが成功すると、作成された子プロセスは、親プロセスによって開かれたファイルなど、親プロセスが所有するリソースのほとんどを継承します。したがって、子プロセスは、親プロセスによって作成されたファイル ポインタ/記述子/ハンドルを直接使用して、親プロセスと同じファイルにアクセスできます。
標準の C ファイル操作関数を使用する場合、プロセス空間内のファイル バッファ を使用し、バッファは親プロセスと子プロセス間で共有されないことに注意してください。 したがって、いずれかのプロセスが書き込み操作を完了した後、他のプロセスが必要なデータを読み取れるように、データを強制的にディスクに更新する必要があります。 fflush()
要約すると、ファイル操作にはシステム コールを直接使用することをお勧めします。
3. 端末も重要なリソースです
printf()
端末に 情報を出力するために使用するのは当然ですが、複数のプロセスが同時に出力する場合、端末も重要なリソースとなるため、相互排他保護も必要であり、そうしないと出力情報が乱れることがあります。
また、その後は出力バッファprintf()
に情報が保存されるだけで、実際には標準出力(通常はターミナルコンソール)には出力されないため、出力情報のタイミングがずれる可能性があります。したがって、各呼び出しの後、データが端末に送信されることを確認します。printf()
stdio.h
fflush(stdout)
4. アトミック操作、スリープとウェイクアップ
Linux 0.11 は同時実行性をサポートする最新のオペレーティング システムです。アプリケーション用のロックやセマフォは実装されていませんが、内部的にロック メカニズムを使用する必要があります。つまり、複数のプロセスが共有カーネル データにアクセスする場合、ロックを通じて実装する必要があります。そして同期。
ロックはアトミック操作(スケジューリング メカニズムによって中断されない操作。この操作は一度開始されると、終了するまで実行されます) である必要があります。セマフォは、Linux 0.11 のロックをエミュレートすることで実装できます。
たとえば、複数のプロセスによるディスクへの同時アクセスでは、ロックが必要になります。Linux 0.11 がディスクにアクセスするための基本的な処理方法は、ディスクへのアクセスを高速化するためにメモリ内にディスク キャッシュのセクションを確保することです。プロセスによって行われたディスク アクセス要求は、最初にディスク キャッシュ内で検索される必要があり、見つかった場合は直接返されます。見つからなかった場合は、空きディスク キャッシュが適用され、これを使用してディスクの読み取りおよび書き込み要求が開始されます。ディスクキャッシュをパラメータとして指定します。リクエストが送信された後、プロセスはスリープして待機する必要があります (ディスクの読み取りと書き込みが非常に遅いため、この時点では CPU を他のプロセスに実行を任せる必要があります)。このアプローチは、多くのオペレーティング システム (最新の Linux、UNIX などを含む) で採用されている、より一般的なアプローチです。これには、ディスク キャッシュを一緒に操作する複数のプロセスが含まれ、プロセスが操作中にスケジュールされ、CPU が失われる可能性があります。そのため、ディスク キャッシュを操作するときは相互排他の問題を考慮する必要があるため、ロックを使用する必要があり、プロセスも同様にする必要があります。寝たり起きたりすることに慣れています。
[例]ファイルから取得した 2 つの関数は次のとおりです。 kernel/blk_drv/ll_rw_blk.c
static inline void lock_buffer(struct buffer_head * bh)
{
// 关中断
cli();
// 将当前进程睡眠在 bh->b_wait
while (bh->b_lock)
sleep_on(&bh->b_wait);
bh->b_lock = 1;
// 开中断
sti();
}
static inline void unlock_buffer(struct buffer_head * bh)
{
if (!bh->b_lock)
printk("ll_rw_block.c: buffer not locked\n\r");
bh->b_lock = 0;
// 唤醒睡眠在 bh->b_wait 上的进程
wake_up(&bh->b_wait);
}
解析から、ロック変数 b_lock へのアクセス時には、プロセス切り替えの発生を防ぐために割り込みをオン/オフすることでアトミックな動作を実現していることlock_buffer()
がわかります 。もちろん、この方法には欠点もあり、マルチプロセッサ環境での使用には適していませんが、Linux 0.11 では、シンプルで直接的で効果的なメカニズムです。なぜなら、私たちの実験で bochs によってシミュレートされた Linux 0.11 は単一 CPU システムだからです。
さらに、上記の関数は、Linux 0.11 がそのようなインターフェイスを提供していることを示しています。つまり、 sleep_on()
プロセスのスリープを実現するために使用し、 wake_up()
プロセスのウェイクアップを実現するために使用します。それらのパラメータは構造体ポインタ struct task_struct *
(つまり、sched.h で定義されたプロセスの PCB) です。つまり、プロセスは、このパラメータが指すプロセス PCB 構造体リンク リスト上でスリープまたはウェイクアップします。
したがって、この実験では、割り込みの切り替えによってアトミックな操作を実装することもできます。またsleep_on()
、Linux 0.11 に付属している呼び出しによってプロセスのスリープとウェイクアップを実装することもできます。 wake_up()
【ノート】
sleep_on()
この機能は、パラメータで指定されたリンク リスト上の現在のプロセスをスリープさせることです (このリンク リストは非常に隠されたリンク リストであることに注意してください。詳細については「メモ」を参照してください)。wake_up()
の機能は、リンクされたリスト上でスリープしているすべてのプロセスを起動することです。これらのプロセスは実行するようにスケジュールされるため、起動された後、実行を継続できるかどうかを再判断する必要があります。whileループインを参照してください。lock_buffer()
4. 実験プロセス
一般に、この実験の基本的な内容は、Linux 0.11 カーネルでセマフォを実現し、そのセマフォを使用したインターフェイスをユーザーに提供し、ユーザーはこのインターフェイスを使用して実際のプロセス同期問題を解決することです。
(1) 生産者・消費者検査プログラムの作成
1.pc.cを書きます
oslab/exp_06
の下に新しいディレクトリを作成しますpc.c
。
【パソコン】
#define __LIBRARY__
#include <unistd.h>
#include <linux/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/sched.h>
/* 添加系统调用API */
_syscall2(sem_t *,sem_open,const char *,name,unsigned int,value)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)
const char *FILENAME = "/usr/root/buffer_file"; /* 消费or生产的产品存放的缓冲文件的路径 */
const int NR_CONSUMERS = 5; /* 消费者数量 */
const int NR_ITEMS = 520; /* 产品最大量 */
const int BUFFER_SIZE = 10; /* 缓冲区大小,表示可同时存在的产品数量 */
sem_t *mutex, *full, *empty; /* 3个信号量 */
unsigned int item_pro, item_used; /* 刚生产的产品号,刚消费的产品号 */
int fi, fo; /* 供生产者写入或消费者读取的缓冲文件的句柄 */
int main(int argc, char *argv[])
{
char *filename;
int pid;
int i;
filename = argc > 1 ? argv[1] : FILENAME;
/*
* O_TRUNC 表示:当文件以只读或只写打开时,若文件存在,则将其长度截为0(即清空文件)
* 0222 表示:文件只写(前面的0是八进制标识)
* 0444 表示:文件只读
*/
/* 以只写方式打开文件给生产者写入产品编号 */
fi = open(filename, O_CREAT| O_TRUNC| O_WRONLY, 0222);
/* 以只读方式打开文件给消费者读出产品编号 */
fo = open(filename, O_TRUNC| O_RDONLY, 0444);
mutex = sem_open("MUTEX", 1); /* 互斥信号量,防止生产和消费同时进行 */
full = sem_open("FULL", 0); /* 产品剩余信号量,大于0则可消费 */
empty = sem_open("EMPTY", BUFFER_SIZE); /* 空信号量,它与产品剩余信号量此消彼长,大于0时生产者才能继续生产 */
item_pro = 0;
if ( (pid = fork()) ) /* 父进程用来执行生产者动作 */
{
printf("pid %d:\tproducer created....\n", pid);
/*
* printf输出的信息不会马上输出到标准输出(通常为终端控制台),而是先保存到输出缓冲区。
* 为避免偶然因素的影响造成输出信息时序不一致,
* 每次printf()后都调用一下 stdio.h 中的 fflush(stdout),
* 来确保将输出内容立刻输出到标准输出。
*/
fflush(stdout);
while (item_pro <= NR_ITEMS) /* 生产完所需产品 */
{
sem_wait(empty); /* P(empty) */
sem_wait(mutex); /* P(mutex) */
/*
* 生产完一轮产品(文件缓冲区只能容纳 BUFFER_SIZE 个产品编号)后,
* 将缓冲文件的位置指针重新定位到文件首部。
*/
if( !(item_pro % BUFFER_SIZE) ) /* item_pro = 10 */
lseek(fi, 0, 0);
write(fi, (char *) &item_pro, sizeof(item_pro)); /* 写入产品编号 */
printf("pid %d:\tproduces item %d\n", pid, item_pro);
fflush(stdout);
item_pro++;
sem_post(full); /* 唤醒消费者进程 */
sem_post(mutex);
}
}
else /* 子进程来创建消费者 */
{
i = NR_CONSUMERS;
while(i--)
{
if( !(pid=fork()) ) /* 创建i个消费者进程 */
{
pid = getpid();
printf("pid %d:\tconsumer %d created....\n", pid, NR_CONSUMERS-i);
fflush(stdout);
while(1)
{
sem_wait(full);
sem_wait(mutex);
/* read()读到文件末尾时返回0,将文件的位置指针重新定位到文件首部 */
if(!read(fo, (char *)&item_used, sizeof(item_used)))
{
lseek(fo, 0, 0);
read(fo, (char *)&item_used, sizeof(item_used));
}
printf("pid %d:\tconsumer %d consumes item %d\n", pid, NR_CONSUMERS-i+1, item_used);
fflush(stdout);
sem_post(empty); /* 唤醒生产者进程 */
sem_post(mutex);
if(item_used == NR_ITEMS) /* 如果已经消费完最后一个商品,则结束 */
goto OK;
}
}
}
}
OK:
close(fi);
close(fo);
return 0;
}
2.PCのマウント
pc.c
仮想マシンの Linux 0.11 ディレクトリにコピーします。/usr/root/
// oslab 目录下
sudo ./mount-hdc
cp ./exp_06/pc.c ./hdc/usr/root/
sudo umount hdc/
(2) セマフォの実現
コンテンツのこの部分では、実験 3 のシステム コールを参照できます: Li Zhijun 著「オペレーティング システム」 | 実験 3 - System Call_Amentos のブログ - CSDN ブログ
1. システムコールAPIの追加
次のコードをアプリケーションpc.c
に(上で追加)。
_syscall2(sem_t *,sem_open,const char *,name,unsigned int,value)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)
2. 新しいセミナー
セマフォ名、セマフォ値、待機プロセス キューなどのセマフォのデータ構造を定義するために、ディレクトリlinux-0.11/include/linux
の下に新しいセマフォを作成します。 sem.h
【sem.h】
#ifndef _SEM_H
#define _SEM_H
#include <linux/sched.h>
#define SEMTABLE_LEN 20
#define SEM_NAME_LEN 20
typedef struct semaphore
{
char name[SEM_NAME_LEN]; /* 信号量名称 */
int value; /* 信号量值 */
struct task_struct *queue; /* 信号量等待队列 */
} sem_t;
extern sem_t semtable[SEMTABLE_LEN]; /* 定义一个信号量表 */
#endif
ここでの#ifndef、#define、および #endifの役割は、繰り返しの参照によって引き起こされるヘッダー ファイルの繰り返しコンパイルを防ぐことです。具体的な原則については、次の記事を参照してください: #ifndef #define #endif をヘッダー ファイルに追加する理由
3. 新しいセミナー
linux-0.11/kernel
ディレクトリの下に、 sem.c
4 つのセマフォ関数を実装するための新しいソース コード ファイルを作成します。
【セミc】
#include <linux/sem.h>
#include <linux/sched.h>
#include <unistd.h>
#include <asm/segment.h>
#include <linux/tty.h>
#include <linux/kernel.h>
#include <linux/fdreg.h>
#include <asm/system.h>
#include <asm/io.h>
//#include <string.h>
sem_t semtable[SEMTABLE_LEN]; /* 定义一个信号量表 */
int cnt = 0;
sem_t *sys_sem_open(const char *name,unsigned int value)
{
char kernelname[100];
int isExist = 0;
int i = 0;
int name_cnt = 0;
while( get_fs_byte(name+name_cnt) != '\0' )
name_cnt++;
if( name_cnt > SEM_NAME_LEN )
return NULL;
/* 从用户态复制到内核态 */
for(i=0;i<name_cnt;i++)
kernelname[i] = get_fs_byte(name+i);
int name_len = strlen(kernelname);
int sem_name_len = 0;
sem_t *p = NULL;
for(i=0;i<cnt;i++)
{
sem_name_len = strlen(semtable[i].name);
if(sem_name_len == name_len)
{
if( !strcmp(kernelname,semtable[i].name) )
{
isExist = 1;
break;
}
}
}
if(isExist == 1)
{
p = (sem_t*)(&semtable[i]);
//printk("find previous name!\n");
}
else
{
i = 0;
for(i=0;i<name_len;i++)
{
semtable[cnt].name[i] = kernelname[i];
}
semtable[cnt].value = value;
p = (sem_t*)(&semtable[cnt]);
//printk("creat name!\n");
cnt++;
}
return p;
}
int sys_sem_wait(sem_t *sem)
{
cli(); /* 关中断 */
while( sem->value <= 0 )
sleep_on( &(sem->queue) ); /* 所有小于0的进程都阻塞 */
sem->value--;
sti(); /* 开中断 */
return 0;
}
int sys_sem_post(sem_t *sem)
{
cli();
sem->value++;
if( (sem->value) <= 1 )
wake_up( &(sem->queue) );
sti();
return 0;
}
int sys_sem_unlink(const char *name)
{
char kernelname[100]; /* 应该足够大了 */
int isExist = 0;
int i = 0;
int name_cnt = 0;
while( get_fs_byte(name+name_cnt) != '\0' )
name_cnt++;
if( name_cnt > SEM_NAME_LEN )
return NULL;
for(i=0;i<name_cnt;i++)
kernelname[i] = get_fs_byte(name+i);
int name_len = strlen(name);
int sem_name_len = 0;
for(i=0;i<cnt;i++)
{
sem_name_len = strlen(semtable[i].name);
if(sem_name_len == name_len)
{
if( !strcmp(kernelname,semtable[i].name) )
{
isExist = 1;
break;
}
}
}
if(isExist == 1)
{
int tmp = 0;
for(tmp=i;tmp<=cnt;tmp++)
{
semtable[tmp] = semtable[tmp+1];
}
cnt = cnt-1;
return 0;
}
else
return -1;
}
4.unistd.hを変更する
4 つの新しいシステム コールが追加されました。linux-0.11/include
ディレクトリ開きunistd.h
、新しいシステム コール番号を追加します。
#define __NR_sem_open xx
#define __NR_sem_wait xx
#define __NR_sem_post xx
#define __NR_sem_unlink xx
5. system_call.s を変更します。
ディレクトリに入って開き、システム コールの合計数を変更します。 linux-0.11/kernel
system_call.s
6.sys.hを変更する
を入力しlinux-0.11/include/linux
、 を開き、4 つの新しいシステム コールのシステム コール関数名を追加し、システム コール テーブルを保守します。 sys.h
sys_call_table 配列内のシステム コール関数名の位置は、unistd.h 内の __NR_name の値と同じである必要があることに 注意してください 。
7. メイクファイルを変更する
linux-0.11/kernel
ディレクトリ配下に次の変更を加えますMakefile
。
まず、[OBJS] の後に追加します。
sem.o
2 番目に、[依存関係] の後に追加します。
sem.s sem.o: sem.c ../include/linux/sem.h ../include/linux/kernel.h \
../include/unistd.h
8. ファイルをマウントします
作成sem.h
・unistd.h
Linux 0.11 システムにコピーします。これは実験 3 の「システムコール」の原理と同じです。
// oslab 目录下
sudo ./mount-hdc
cp ./linux-0.11/include/unistd.h ./hdc/usr/include/
cp ./linux-0.11/include/linux/sem.h ./hdc/usr/include/linux/
sudo umount hdc/
9. 再コンパイル
// linux-0.11 目录下
make all
(3) プロデューサー/コンシューマー プログラムを実行する
1. pc.c をコンパイルして実行します。
oslab ディレクトリにLinux 0.11 を入力し ./run
、コンパイルして実行し
pc.c
、出力情報をpc.txt
ファイル。
gcc -o pc pc.c
./pc > pc.txt
sync
最後に同期する必要があることに注意してください。
2. 出力を表示する
pc.txt
コピーしてUbuntu で確認します。
sudo ./mount-hdc
sudo cp ./hdc/usr/root/pc.txt ./exp_06
sudo chmod 777 exp_06/pc.txt
cat exp_06/pc.txt | more
ターミナルのcat
コマンドを、直接ダブルクリックしpc.txt
て開くこともできます。
「ファイルを開くために必要な権限がありません」と表示された場合は、次のコマンドを発行して権限を変更してください。
sudo chmod 777 exp_06/pc.txt
3. 結果を出力する
……
【実験のヒント】
1. カオスなボッチ仮想画面への対処
Linux 0.11 のバグなのか bochs のバグなのか分かりませんが、ターミナルにこれ以上の情報を出力すると bochs の仮想画面が混乱してしまいます。この時、Ctrl+Lを押して画面を再初期化しますが、出力情報が多すぎるとやはり混乱してしまいます。たとえば、プログラムを最初から直接実行すると ./pc
、次のように結果が表示されます。
./pc > pc.txt (即重定向到 pc.txt)
したがって、出力情報をファイルにリダイレクトし、vi、more、その他のツールを使用して画面を押してこのファイルを表示することをお 勧めします。これにより、この問題は基本的に解決されます。ファイルを Ubuntu システムにコピーして表示することもできます。
vi pc.txt:
2. string.h に関するヒント
以下に説明する問題は普遍的な重要性を持っていない可能性があり、実験者に注意を払ってください。
include/string.h は C 言語の文字列操作の完全なセットを実装しており、それらはすべてアセンブリ + インラインによって最適化されます。しかし、使用していると、場合によっては奇妙な問題が発生することがあります。たとえば、strcmp() がパラメータの内容を破壊するという問題に遭遇した人がいます。デバッグ中に「奇妙な」状況に遭遇した場合は、ヘッダー ファイルを含めないようにしてみると、通常は解決できます。string.h が含まれていないため、これらの関数はインラインで呼び出されず、より正常に動作します。