OS 實驗2 建立共享記憶體 thread_create()分析
OS 實驗2 建立共享記憶體 thread_create()分析
建立共享記憶體
通過共享記憶體,完成生產者-消費者模型的建立,並用gdb進行除錯。
使用man
+函式名即可檢視函式幫助文件。
mmap()
#include <sys/mman.h> void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *start, size_t length);
建立新的虛擬記憶體區域,並將一個檔案或物件對映到該區域。
執行成功時,返回被對映區域的指標;否則返回MAP_FAILED
,其值為(void *)-1
。
引數:
start
:對映區的開始地址,為0時表示由系統決定對映區的起始地址。
length
:對映區的長度,以位元組為單位(會補齊到整數倍記憶體頁大小)。
prot
:期望的記憶體保護標誌(使用該空間的許可權),不能與檔案的開啟模式衝突。
prot
可通過or
組合以下值作為引數:
PROT_EXEC
:頁內容可被執行。
PROT_READ
:頁內容可讀。
PROT_WRITE
:頁內容可寫。
PROT_NONE
:頁不可訪問。
flags
:指定對映物件的型別,對映選項和對映頁是否可以共享。
flags
可通過or
組合以下值作為引數(僅列舉常用值):
MAP_FIXED
:使用指定的對映起始地址,且起始地址必須落在頁的邊界上。如果由start和len引數指定的記憶體區與已有的對映空間重疊,重疊部分會被丟棄。如果指定的起始地址不可用,操作失敗。
MAP_SHARED
:與其它所有對映這個物件的程序共享對映空間。對共享區的寫入,相當於輸出到檔案。但程序在對映空間對共享內容的改變並不會直接寫到磁碟檔案中,只有呼叫msync()
或munmap()
後,共享區才會被更新。與MAP_PRIVATE
互斥,只能且必須使用其中一個。
MAP_PRIVATE
:建立一個寫時拷貝的私有對映。對記憶體區域的寫入不會影響到原檔案。與MAP_SHARED
互斥,只能且必須使用其中一個。
MAP_ANONYMOUS
:匿名對映,對映區不與任何檔案關聯(無需指定fd)。可避免檔案的建立與開啟,但只能用於具有父子關係的程序。
fd
:檔案描述符,一般為open()
的返回值。可以為-1,表示不指定檔案,此時flags必須包含MAP_ANON
。
offset
:被對映物件內容的起點。
功能:
- 允許使用者程式直接訪問裝置記憶體,相比於在使用者空間和核心空間互相拷貝資料,效率更高。
- 使程序之間可通過對映同一個普通檔案實現共享記憶體。
munmap()
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *start, size_t length);
取消start
所指的虛擬對映記憶體,length
表示取消的空間大小。
執行成功時,返回0;否則返回-1。
程序結束,或通過exec
執行其他程式時,對映記憶體會自動被解除。但關閉檔案描述符時不會解除對映。
shm_open()
#include <fcntl.h> /* For O-* constants */
#include <sys/stat.h> /* For mode constants */
#include <sys/mman.h>
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
注意編譯時要連結庫,即在最後加引數
-lrt
。
shm_open
的幫助文件中,語法要求Link with -lrt.
。NOTES中:Programs using these functions must specify the -lrt flag to cc in order to link against the required ("realtime") library.
Linux共享記憶體通過tmpfs檔案系統實現。tmpfs檔案系統完全駐留在RAM中,其讀寫速度極快。
tmpfs預設位於/dev/shm
目錄,因此對該目錄下的檔案讀寫即通過tmpfs檔案系統進行,其速度與讀寫記憶體速度一樣。
/dev/shm
的預設容量為系統記憶體的一半。只有在其中含有檔案時,才真正佔用對應的記憶體大小,否則不會佔用記憶體。
用於開啟或建立檔案。
執行成功時,返回對應檔案描述符;否則返回-1。返回的檔案描述符一定是最小的未被使用的描述符。
shm_open
與open
基本相同,但其操作的檔案一定位於tmpfs檔案系統,即位於/dev/shm
。
引數:
name
:指定要開啟或建立的檔名。注意因為shm_open
操作的檔案位於/dev/shm
,所以不需且不能包含路徑,不同於open()
的pathname
(不過也可包含路徑,但要保證/dev/shm
中包含對應路徑。此外tmpfs不是一定在/dev/shm
中)。
oflag
:指定開啟或建立的檔案模式。
oflags
可通過or
組合以下值作為引數(僅列舉常用值):
O_RDONLY
:以只讀模式開啟。
O_WRONLY
:以只寫模式開啟。
O_RDWR
:以可讀可寫模式開啟。以上三種模式只可選擇一種。
O_APPEND
:以追加方式開啟。
O_CREAT
:如果檔案不存在,則建立檔案。
O_EXCL
:如果使用了O_CREAT
且檔案已存在,返回-1(並更新錯誤資訊errno)。
O_TRUNC
:如果檔案已存在(且以可寫模式開啟),清空該檔案。
mode
:使用O_CREAT
新建檔案時,該檔案的許可權標誌。由4位數字組成。
mode的第一位數代表特殊許可權(suid:4,sgid:2,sbit:1,即setUid/setGid/粘著位),一般為0即可,可省略。
後三位數分別表示:所有者、群組、其他使用者所具有的許可權。每位數通過加權表示許可權:4:讀許可權,2:寫許可權,1:執行許可權。
shm_unlink()
#include <fcntl.h> /* For O-* constants */
#include <sys/stat.h> /* For mode constants */
#include <sys/mman.h>
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
刪除/dev/shm
目錄下的指定檔案。
執行成功時,返回0;否則返回-1。
用unlink()
並指定/dev/shm
+name
作為目錄,可實現同樣效果。但tmpfs不是一定在/dev/shm
中。
shm_open()
建立的檔案,如果不使用shm_unlink()
刪除,會一直位於/dev/shm
中,直到作業系統重啟或用rm
刪除。
ftruncate()
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
truncate
和ftruncate
均可重置檔案大小為length位元組。若檔案縮小,則部分資訊會丟失;若檔案擴大,則用空字元\0
填充。
執行成功時,返回0;否則返回-1。
任何通過open()
或shm_open()
開啟的檔案都可使用。
使用truncate
,需保證檔案可寫;使用ftruncate
,需保證檔案已開啟且可寫。
生產者-消費者模型除錯
編譯檔案
檔案使用傳入的引數決定執行生產者還是消費者。
除錯生產者、執行消費者
在輸出資訊的位置(78行)新增斷點。使用producer
引數執行生產者。
使用consumer
引數執行消費者。
輸出/接收資訊
除錯在輸出資訊的位置(斷點處)暫停,生產者用continue
輸出一條資訊,消費者收到資訊。
輸出/接收資訊(第二條)
除錯在輸出第二條資訊的位置暫停,生產者繼續用continue
輸出第二條資訊,消費者收到第二條資訊。
輸出/接收資訊(第三條)
除錯在輸出第三條資訊的位置暫停,生產者繼續用continue
輸出第三條資訊,消費者收到第三條資訊。
程式碼:
使用需傳入引數:p
或producer
表示生產者,c
或consumer
表示消費者。
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
void *Mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
{
void *ptr = mmap(start, length, prot, flags, fd, offset);
if(ptr==MAP_FAILED)
{
puts("mmap failed.");
exit(-1);
}
return ptr;
}
int Munmap(void *start, size_t length)
{
int res=munmap(start, length);
if(res==-1)
{
puts("munmap failed.");
exit(-1);
}
return res;
}
int Shm_open(const char *name, int oflag, mode_t mode)
{
int res=shm_open(name, oflag, mode);
if(res==-1)
{
puts("shm_open failed.");
exit(-1);
}
return res;
}
int Shm_unlink(const char *name)
{
int res=shm_unlink(name);
if(res==-1)
puts("shm_unlink failed."), exit(-1);
return res;
}
int Ftruncate(int fd, off_t length)
{
int res=ftruncate(fd, length);
if(res==-1)
puts("ftruncate failed."), exit(-1);
return res;
}
const int SIZE = 4096;
const char *NAME = "Messages";
namespace Producer
{
const int message_size=3;
const char *messages[message_size]={
"message 1,",
"message 2,",
"message 3!"
};
int main()
{
puts("Producer begins.");
//用tmpfs檔案系統建立檔案,並設定大小
int shm_fd = Shm_open(NAME, O_RDWR|O_CREAT, 0666);
Ftruncate(shm_fd, SIZE);
//將檔案對映到ptr所指的虛擬記憶體區域
void *ptr = Mmap(0, SIZE, PROT_WRITE, MAP_SHARED, shm_fd, 0);
for(int i=0; i<message_size; ++i)
{
sprintf((char *)ptr, "%s", messages[i]);
ptr += strlen(messages[i]);
}
return 0;
}
}
namespace Consumer
{
int main()
{
puts("Consumer begins.");
//開啟與生產者相同的檔案
int shm_fd = Shm_open(NAME, O_RDONLY, 0666);
//將同一檔案對映到虛擬記憶體區域,以共享記憶體
void *ptr = Mmap(0, SIZE, PROT_READ, MAP_SHARED, shm_fd, 0);
for(int i=0; i<3; )
if(strlen((char *)ptr)>0)
{
printf("Consumer: %s\n", (char *)ptr);
ptr += strlen((char *)ptr);
++i; //強制接收3條訊息
}
//消費者使用完成後刪除檔案(shm_open()建立的檔案不會在結束時自動刪除)
Shm_unlink(NAME);
return 0;
}
}
int main(int argc, char **argv)
{
for(int i=0; i<argc; ++i)
printf("argv[%d]=%s\n",i,argv[i]);
if(argc!=2)
return puts("Argument Error."), 1;
if(argv[1][0]=='p')
return Producer::main();
else if(argv[1][0]=='c')
return Consumer::main();
else
return puts("Argument Error."), 1;
return 0;
}
thread_create()解析
分析pintos中建立執行緒的函式thread_create()
的原始碼。
函式原型:
#include <thread.c>
tid_t thread_create (const char *name, int priority, thread_func *, void *);
函式介紹:
巨集定義
tid_t
執行緒識別符號,為int。
typedef int tid_t;
uint8_t
8位無符號整數,即unsigned char
。用於表示棧指標。
typedef unsigned char uint8_t;
thread_func
void型別函式的函式指標,引數為void *aux
。
typedef void thread_func (void *aux);
PGBITS
頁的位數,為\(12\)。
#define PGBITS 12 /* Number of offset bits. */
PGSIZE
頁的大小,為\(2^{12}\)。
#define PGSIZE (1 << PGBITS) /* Bytes in a page. */
THREAD_MAGIC
堆疊金絲雀的預設值,為一個隨機出的值。
#define THREAD_MAGIC 0xcd6abf4b
連結串列
核心中使用迴圈連結串列。
結構list
表示迴圈連結串列,含有頭、尾兩個連結串列元素。
結構list_elem
表示連結串列元素,含有兩個雙向的連結串列元素指標。
用到的結構體/函式
allocate_tid()
為新執行緒分配一個可用的tid。
會執行:將tid_lock
上鎖(阻塞後來需要分配tid的執行緒);分配tid,值為next_tid++
;將tid_lock
解鎖(解除後來執行緒的阻塞)。
tid_lock:程序識別符號tid的鎖。用於分配tid。
static tid_t allocate_tid (void);
alloc_frame()
為執行緒t分配size位元組的棧幀(將其棧頂指標減少size),並返回當前的棧指標。
size必須為整數倍的字大小(即\(4k\)位元組)。
static void *alloc_frame (struct thread *t, size_t size);
intr_level
用來表示是否接受中斷。
含兩種值。
enum intr_level
{
INTR_OFF, /* 關閉中斷 */
INTR_ON /* 開啟中斷 */
};
init_thread()
初始化一個執行緒t,並將其加入到all_list
(一個包含所有執行緒的連結串列)。
初始化包括:將其命名為name,設定其優先順序為priority;設定狀態status為阻塞THREAD_BLOCKED
,呼叫enum intr_level intr_disable (void);
關閉中斷,並更新old_level
為INTR_OFF
(?);設定棧指標(值為(uint8_t *) t + PGSIZE
);設定金絲雀值magic為THREAD_MAGIC
。
static void init_thread (struct thread *t, const char *name, int priority);
kernel_thread()
核心執行緒的基礎函式。
用於:開啟中斷接收(排程程式在執行時關閉了中斷接收),執行執行緒的函式,結束並殺死執行緒。
static void
kernel_thread (thread_func *function, void *aux)
{
ASSERT (function != NULL);
intr_enable (); /* 排程程式在執行時會關閉中斷 */
function (aux); /* 執行執行緒函式 */
thread_exit (); /* 如果函式成功返回,結束執行緒 */
}
kernel_thread_frame
核心執行緒函式kernel_thread()
的棧幀。
儲存了:執行緒函式的返回地址、要呼叫的函式、呼叫函式的輔助資訊。
struct kernel_thread_frame
{
void *eip; /* 返回地址 */
thread_func *function; /* 要呼叫的函式 */
void *aux; /* 函式的輔助資訊 */
};
lock
鎖。
包含兩個值:擁有該鎖的執行緒(用於除錯),控制權限的二進位制訊號量。
struct lock
{
struct thread *holder; /* Thread holding lock (for debugging). */
struct semaphore semaphore; /* Binary semaphore controlling access. */
};
lock_acquire()
請求鎖。
會將給定鎖的訊號量減1(如果減1前為0則等待,直至其為正可減),並設定給定鎖的所有者為當前執行緒thread_current()
。
該函式可能會等待(sleep),所以不能在中斷處理程式中被呼叫(會導致核心錯誤)。
void lock_acquire (struct lock *lock);
lock_release()
釋放鎖。
會將給定鎖的訊號量加1,並設定給定鎖的所有者為NULL。
需保證鎖被當前執行緒擁有。
void lock_release (struct lock *lock);
palloc_flags
表示分配的頁的模式。
包含三種:PAL_ASSERT
(核心錯誤),PAL_ZERO
(將頁用0填充),PAL_USER
(使用者頁,表示從使用者池中獲取頁,否則從核心池中獲取頁)。
enum palloc_flags
{
PAL_ASSERT = 001, /* 核心錯誤 */
PAL_ZERO = 002, /* 頁內容用0填充 */
PAL_USER = 004 /* 使用者頁 */
};
核心錯誤(Kernel Panic)指作業系統在監測到內部的致命錯誤,但無法安全處理此錯誤時採取的操作。此時核心會盡可能將它此時能獲取的全部資訊打印出來。
常見原因:
- 中斷處理程式執行時,它不處於任何一個程序上下文,此時使用可能導致睡眠的函式(如訊號量),會破壞系統排程,導致核心錯誤。
- 棧溢位。
- 對於除0異常、記憶體訪問越界、緩衝區溢位等錯誤,若發生在應用程式,則核心的異常處理程式會進行處理,即使終止原程式也不會影響其它程式;若發生在核心,則會引起核心錯誤。
- 核心陷入死鎖狀態,自旋鎖有巢狀使用的情況。
- 核心執行緒中存在死迴圈。
palloc_get_page()
建立一個頁,返回其虛擬地址(如果無可用頁則返回NULL)。
新頁的模式與傳入的flags有關:PAL_ASSERT
(核心錯誤),PAL_ZERO
(將頁用0填充),PAL_USER
(使用者頁,表示從使用者池中獲取頁,否則從核心池中獲取頁)。
void *palloc_get_page (enum palloc_flags flags);
switch_entry()
程序切換入口?找不到函式的實現。
void switch_entry (void);
switch_entry_frame
switch_entry()
的棧幀。
儲存了一個返回地址,其型別為void
型別函式的指標。
struct switch_entry_frame
{
void (*eip) (void);
};
switch_threads()
程序切換函式。
將當前程序從cur切換到next,並在next的上下文中返回cur(?)。cur必須是當前正在執行的執行緒,next必須同樣正在執行該函式。
struct thread *switch_threads (struct thread *cur, struct thread *next);
switch_threads_frame
switch_threads()
的棧幀。
儲存了4個暫存器的值、返回地址(為void
型別函式的指標)、switch_threads()
的cur引數、switch_threads()
的next引數。
struct switch_threads_frame
{
uint32_t edi; /* 0: 儲存 %edi */
uint32_t esi; /* 4: 儲存 %esi */
uint32_t ebp; /* 8: 儲存 %ebp */
uint32_t ebx; /* 12: 儲存 %ebx */
void (*eip) (void); /* 16: 返回地址 */
struct thread *cur; /* 20: switch_threads()的 CUR 引數 */
struct thread *next; /* 24: switch_threads()的 NEXT 引數 */
};
thread
一個執行緒結構表示一個核心執行緒或使用者程序。
一個執行緒結構儲存在一個4KB的頁中。頁的最下方(偏移量為0的位置)儲存頁資訊,通常為若干位元組,不超過1KB;頁的剩餘部分為核心棧,自頂向下增長(偏移量為4KB的位置)。
頁資訊的最上方為magic
,即棧內金絲雀,用以判斷棧使用的空間是否過大。
elem
是一個連結串列元素,既可以表示執行佇列裡的一個元素(當執行緒處於就緒狀態時,位於thread.c
),也可表示訊號等待佇列裡的一個元素(當執行緒處於阻塞狀態時,位於synch.c
)。
每個執行緒只含有4KB的棧大小,所以大陣列或大的資料結構應動態分配其記憶體。
struct thread
{
/* Owned by thread.c. */
tid_t tid; /* 執行緒識別符號 */
enum thread_status status; /* 執行緒狀態 */
char name[16]; /* 執行緒名稱(除錯用) */
uint8_t *stack; /* 棧指標 */
int priority; /* 優先順序 */
struct list_elem allelem; /* 連結串列元素,用於放在一個包含所有執行緒連結串列中 */
/* 該元素在 thread.c 和 synch.c 間共享 */
struct list_elem elem; /* 連結串列元素 */
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint32_t *pagedir; /* 頁路徑(當執行緒為使用者程序時) */
#endif
/* Owned by thread.c. */
unsigned magic; /* 檢測棧溢位 */
};
thread_current()
返回正在執行的執行緒。此外會對要返回的結果進行檢查。
struct thread *thread_current (void);
thread_start()
執行需要搶先執行的執行緒(preemptive thread,由中斷安排)。同時建立idle執行緒。
void thread_start (void);
thread_status
表示程序所處的狀態。
enum thread_status
{
THREAD_RUNNING, /* Running thread. */
THREAD_READY, /* Not running but ready to run. */
THREAD_BLOCKED, /* Waiting for an event to trigger. */
THREAD_DYING /* About to be destroyed. */
};
thread_unblock()
將程序t從阻塞狀態轉為就緒狀態,並將其加入到就緒佇列。
需保證t處於阻塞狀態。
該函式不會取代正在執行的執行緒。如果呼叫者關閉了中斷,它可能會自動解除一個執行緒的阻塞並更新其它資訊(?)。
void thread_unblock (struct thread *t);
解析
功能:
建立一個核心執行緒,其名稱為name,優先順序為priority,需要執行函式function,需要的引數為aux。
如果建立成功,返回其tid;否則返回TID_ERROR
。
整體過程:
- 為新執行緒建立一個頁、分配tid,並將其設為阻塞狀態。
- 建立三個函式的棧幀,這三個函式用於切換並呼叫執行緒。(猜測)當執行緒被排程執行時,呼叫執行緒切換函式
switch_threads()
,然後進入相應的執行緒切換入口switch_entry()
,最後進入函式kernel_thread()
,執行該執行緒的函式、最終殺死執行緒。 - 建立完棧幀後,將程序設為就緒狀態。
- 返回其tid。
注意:
如果thread_start()
已經被呼叫,則在thread_create()
返回之前,新執行緒可能就已被安排呼叫,甚至已經結束返回。相反地,在新執行緒被安排呼叫之前,原執行緒可能會執行任意長的時間。如果要確保有序,需使用訊號量或其他的同步方式。
該函式為新執行緒分配了優先順序priority,但pintos利用優先順序影響排程的功能還未實現,這就是問題1-3的目標。
程式碼:
//所有涉及的型別與函式均在上面介紹過
tid_t
thread_create (const char *name, int priority,
thread_func *function, void *aux)
{
// 定義新執行緒的指標t,為函式 kernel_thread(), switch_entry(), switch_threads() 分配棧幀。
// 棧幀用於儲存函式的資訊/資料。
struct thread *t;
struct kernel_thread_frame *kf;
struct switch_entry_frame *ef;
struct switch_threads_frame *sf;
tid_t tid;
ASSERT (function != NULL);
// 為執行緒分配1頁,令t指向分配的空間。若無可用頁則返回TID_ERROR。
t = palloc_get_page (PAL_ZERO);
if (t == NULL)
return TID_ERROR;
// 初始化執行緒(具體內容見`init_thread()`),並分配其tid。
init_thread (t, name, priority);
tid = t->tid = allocate_tid ();
// 分配`kernel_thread()`的棧幀,初始化資訊為:返回地址NULL;要呼叫的函式function;引數aux。
kf = alloc_frame (t, sizeof *kf);
kf->eip = NULL;
kf->function = function;
kf->aux = aux;
// 分配`switch_entry()`的棧幀,初始化資訊為:返回地址,指向`kernel_thread()`。
ef = alloc_frame (t, sizeof *ef);
ef->eip = (void (*) (void)) kernel_thread;
// 分配`switch_threads()`的棧幀,初始化資訊為:返回地址,指向`switch_threads()`;儲存的%ebp值為0。
sf = alloc_frame (t, sizeof *sf);
sf->eip = switch_entry;
sf->ebp = 0;
// 解除執行緒t的阻塞狀態,設為就緒狀態,並將其加入到就緒佇列。
thread_unblock (t);
// 建立成功,返回其tid。
return tid;
}
無心插柳柳成蔭才是美麗
有哪種美好會來自於刻意
這一生波瀾壯闊或是不驚都沒問題
只願你能夠擁抱那種美麗