1. 程式人生 > 其它 >OS 實驗2 建立共享記憶體 thread_create()分析

OS 實驗2 建立共享記憶體 thread_create()分析

這不比毒瘤演算法題簡單 目錄

OS 實驗2 建立共享記憶體 thread_create()分析

https://www.zybuluo.com/SovietPower/note/1824664


建立共享記憶體

通過共享記憶體,完成生產者-消費者模型的建立,並用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:被對映物件內容的起點。

功能:

  1. 允許使用者程式直接訪問裝置記憶體,相比於在使用者空間和核心空間互相拷貝資料,效率更高。
  2. 使程序之間可通過對映同一個普通檔案實現共享記憶體。

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_openopen基本相同,但其操作的檔案一定位於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:執行許可權。

#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);

truncateftruncate均可重置檔案大小為length位元組。若檔案縮小,則部分資訊會丟失;若檔案擴大,則用空字元\0填充。
執行成功時,返回0;否則返回-1。

任何通過open()shm_open()開啟的檔案都可使用。
使用truncate,需保證檔案可寫;使用ftruncate,需保證檔案已開啟且可寫。

生產者-消費者模型除錯

編譯檔案
檔案使用傳入的引數決定執行生產者還是消費者。

除錯生產者、執行消費者
在輸出資訊的位置(78行)新增斷點。使用producer引數執行生產者。
使用consumer引數執行消費者。

輸出/接收資訊
除錯在輸出資訊的位置(斷點處)暫停,生產者用continue輸出一條資訊,消費者收到資訊。

輸出/接收資訊(第二條)
除錯在輸出第二條資訊的位置暫停,生產者繼續用continue輸出第二條資訊,消費者收到第二條資訊。

輸出/接收資訊(第三條)
除錯在輸出第三條資訊的位置暫停,生產者繼續用continue輸出第三條資訊,消費者收到第三條資訊。

程式碼:
使用需傳入引數:pproducer表示生產者,cconsumer表示消費者。

#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_levelINTR_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)指作業系統在監測到內部的致命錯誤,但無法安全處理此錯誤時採取的操作。此時核心會盡可能將它此時能獲取的全部資訊打印出來。
常見原因

  1. 中斷處理程式執行時,它不處於任何一個程序上下文,此時使用可能導致睡眠的函式(如訊號量),會破壞系統排程,導致核心錯誤。
  2. 棧溢位。
  3. 對於除0異常、記憶體訪問越界、緩衝區溢位等錯誤,若發生在應用程式,則核心的異常處理程式會進行處理,即使終止原程式也不會影響其它程式;若發生在核心,則會引起核心錯誤。
  4. 核心陷入死鎖狀態,自旋鎖有巢狀使用的情況。
  5. 核心執行緒中存在死迴圈。

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

整體過程:

  1. 為新執行緒建立一個頁、分配tid,並將其設為阻塞狀態。
  2. 建立三個函式的棧幀,這三個函式用於切換並呼叫執行緒。(猜測)當執行緒被排程執行時,呼叫執行緒切換函式switch_threads(),然後進入相應的執行緒切換入口switch_entry(),最後進入函式kernel_thread(),執行該執行緒的函式、最終殺死執行緒。
  3. 建立完棧幀後,將程序設為就緒狀態。
  4. 返回其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;
}

無心插柳柳成蔭才是美麗
有哪種美好會來自於刻意
這一生波瀾壯闊或是不驚都沒問題
只願你能夠擁抱那種美麗