操作系统实验报告12:线程2
操作系統實驗報告12
實驗內容
- 實驗內容:線程(2)。
- 編譯運行課件 Lecture14 例程代碼:
- Algorithms 14-1 ~ 14-7.
- 比較 pthread 和 clone() 線程實現機制的異同
- 對 clone() 的 flags 采用不同的配置,設計測試程序討論其結果
- 配置包括 COLNE_PARENT, CLONE_VM, CLONE_VFORK, CLONE_FILES, CLONE_SIGHAND, CLONE_NEWIPC, CLONE_THREAD
- 編譯運行課件 Lecture14 例程代碼:
實驗環境
- 架構:Intel x86_64 (虛擬機)
- 操作系統:Ubuntu 20.04
- 匯編器:gas (GNU Assembler) in AT&T mode
- 編譯器:gcc
技術日志
編譯運行課件 Lecture14 例程代碼
Thread Local Storage 線程局部存儲(TLS)
實驗內容原理:
- 線程本地存儲(TLS)允許每個線程擁有自己的數據副本。
- 當我們無法控制線程創建過程時,TLS很有用。
- 我們不能向創建的線程傳遞任何參數。
- 例如,使用線程池時。
- TLS不同于局部變量。
- 局部變量僅在單個函數調用期間可見。
- TLS在函數調用中是可見的。
- 與靜態數據類似:
- TLS對每個線程都是唯一的。
- TLS的實施
- __thread int tlsvar;//每個線程都有一個變量tlsvar;由語言編譯器解釋,是TLS的語言級解決方案
- 通過pthread_key_create()函數
其中實驗中用到的函數有:
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));pthread_key_create函數用來創建線程私有數據,從TSD池中分配一個值賦給key以后使用。
第一個參數為一個pthread_key_t *類型的指針,pthread_key_t是宏定義typedef unsigned int pthread_key_t,參數指向一個這個類型的變量。
第二個參數指向一個destructor即清理函數,如果這個參數為NULL,那么系統會自動調用默認的清理函數,釋放第一個參數key指向的內存塊,否則使用指定的清理函數釋放內存塊。
創建了key后,所有線程都可以訪問這個值,但是每個線程可以使用不同的值,相當于一個同名但不同值的全局變量。
int pthread_setspecific(pthread_key_t key, const void *value);pthread_setspecific()函數用來給指定的線程特定的數據鍵值設置屬于這個線程的特定鍵值,第一個參數key代表要設定的數據鍵值,第二個參數value指向設置給key的特定鍵值。
void *pthread_getspecific(pthread_key_t key);pthread_getspecific()函數用來獲取指定線程的特定鍵值,其中參數key代表要獲得的特定鍵值,返回一個指向這個鍵值的指針。
int pthread_key_delete(pthread_key_t key);pthread_key_delete()函數用來銷毀線程特定數據鍵值,釋放與該鍵值相關的所有內存,其中參數key代表要銷毀的特定數據鍵值。
- 驗證實驗alg.14-1-tls-thread.c
執行程序命令:
gcc alg.14-1-tls-thread.c -pthread ./a.out分析:
可以看到,主線程和兩個子線程異步執行,每個線程中的__thread int類型變量tlsvar的值都是獨立的,互不影響,說明每個線程包括主線程都有它的局部存儲數據副本,即變量tlsvar。
實現細節解釋:
一開始使用語句pthread_create(&tid1, NULL, &thread_worker, para1)和pthread_create(&tid2, NULL, &thread_worker, para2)創建兩個線程,這兩個線程運行的函數相同,在線程運行函數中:
static void* thread_worker(void* arg) { char *param = (char *)arg; int randomcount;for (int i = 0; i < 5; ++i) {randomcount = rand() % 100000;for (int k = 0; k < randomcount; k++) ;printf("%s%ld, tlsvar = %d\n", param, gettid(), tlsvar);tlsvar++; /* each thread has its local tlsvar */}pthread_exit(0);}傳入的參數arg用來分隔不同的線程的打印情況,比如主線程在第一列,tid1對應的線程在第二列,tid2在第三列,便于顯示美觀
然后進入一個for循環,循環5次,每次隨機等待一定時間后,打印線程號和變量tlsvar的值,最后返回。
參數param指向的是傳遞的參數argv[1],sum是全局變量,函數的作用是對1到參數之間的所有正整數進行求和并把結果保存在全局變量sum里,最后使用語句pthread_exit(0)返回值為0。
在主線程中,創建了兩個線程之后,繼續異步執行,也進入一個for循環,循環5次,每次隨機等待一定時間后,打印線程號和變量tlsvar的值,最后休眠1s,等待兩個子線程結束后,程序結束。
- 驗證實驗alg.14-2-tls-pthread-key-1.c
執行程序命令:
gcc alg.14-2-tls-pthread-key-1.c -pthread ./a.out分析:
可以看到,不同的fp_log指針指向的不同的文件流可以對應同一個線程私有變量log_key的值。
調用系統命令lsof +d ./log輸出./log目錄及目錄下所有打開的文件和目錄,可以看到,在pthread_key_create()函數中指定的清理函數close_log_file已經將文件全部關閉,文件./log/thread-1.log到./log/thread-5.log中的信息對應靜態變量thcnt1到5不同的值。
實現細節解釋:
一開始使用語句pthread_key_create(&log_key, &close_log_file)在主線程中創建pthread_key_t類型線程私有變量log_key,并指定close_log_file()為清理函數,作用是關閉文件流并刷新所有的緩沖區。
然后進入一個for循環,使用pthread_create(&tids[i], NULL, &thread_worker, NULL)語創建n個線程,這里n為5,線程運行函數為:
static void *thread_worker(void *args) {static int thcnt = 0;char fname[64], msg[64];FILE *fp_log; /* a local variable */sprintf(fname, "log/thread-%d.log", ++thcnt); /* directory ./log must exist */fp_log = fopen(fname, "w");if(!fp_log) {printf("%s\n", fname);perror("fopen()");return NULL;}pthread_setspecific(log_key, fp_log); /* fp_log is associated with log_key */sprintf(msg, "Here is %s\n", fname);write_log(msg); }在線程運行函數中, 首先將文件路徑名寫入變量fname中,根據不同thcnt值文件路徑名不同,這里文件名為log/thread-1.log到log/thread-5.log,使用語句fopen(fname, "w")在./log文件目錄下創建文件,若文件已存在,那么將內容清空,文件只允許寫。
接著使用pthread_setspecific(log_key, fp_log)語句設置當前線程中的log_key與fp_log相關聯
然后將語句Here is和fname中的內容寫入字符串msg中,然后使用write_log(msg),這個函數如下:
void write_log(const char *msg) { FILE *fp_log;fp_log = (FILE *)pthread_getspecific(log_key); /* fp_log is shared in the same thread */fprintf(fp_log, "writing msg: %s\n", msg);printf("log_key = %d, tid = %ld, address of fp_log %p\n", log_key, gettid(), fp_log); }在write_log()函數中, 首先使用語句fp_log = (FILE *)pthread_getspecific(log_key)獲取當前線程中log_key的內容,并轉換為FILE *類型賦給變量fp_log,此時write_log()函數中fp_log指向的內容即之前線程運行函數thread_worker()中變量fp_log指向的內容,即文件log/thread-1.log到文件log/thread-5.log,然后使用語句fprintf(fp_log, "writing msg: %s\n", msg)向fp_log指向的文件流中寫入信息,然后打印當前線程的log_key的值,線程號,以及fp_log指向的文件流的地址。
回到主函數中, 使用pthread_join()函數使主線程等待所有子線程結束后再運行,子線程全部結束后,使用語句pthread_key_delete(log_key)釋放log_key的內存空間,然后調用系統命令lsof +d ./log輸出./log目錄及目錄下所有打開的文件和目錄,最后調用系統命令cat ./log/thread-1.log ./log/thread-5.log查看./log/thread-1.log和./log/thread-5.log文件中的內容。
- 驗證實驗alg.14-3-tls-pthread-key-2.c
執行程序命令:
gcc alg.14-3-tls-pthread-key-2.c -pthread ./a.out分析:
可以看到,無論是處在線程棧區的臨時變量還是處在堆區動態內存分配的結構體變量,都可以和pthread_key_t類型的線程私有變量tls_key綁定,線程調用其它函數的時候也可以使用這個結構體變量的內容。
實現細節解釋:
首先主線程使用pthread_key_create(&tls_key, NULL)創建一個線程私有變量tls_key,然后使用語句pthread_create(&ptid1, NULL, &thread_func1, NULL)和pthread_create(&ptid2, NULL, &thread_func2, NULL)創建兩個線程,線程函數分別為thread_func1和thread_func2,在線程函數thread_func1中:
static void *thread_func1(void *args) {struct msg_struct1 ptr[5]; /* local variable in thread stacke */printf("thread_func1: tid = %ld ptr = %p\n", gettid(), ptr);pthread_setspecific(tls_key, ptr); /* binding ptr to the tls_key */sprintf(ptr[0].stuno, "18000001");sprintf(ptr[0].stuname, "Alex");sprintf(ptr[4].stuno, "18000005");sprintf(ptr[4].stuname, "Michael");print_msg1();pthread_exit(0); }首先打印當前線程號和線程棧區的臨時的struct msg_struct1類型的數組變量ptr的首地址,然后使用語句pthread_setspecific(tls_key, ptr)將這個線程中的tls_key與ptr綁定,然后設置這個線程中的ptr[0]的學生學號stuno和姓名stuname分別設置為18000001和Alex,ptr[4]的學生學號和姓名分別設置為18000005和Michael,然后使用語句print_msg1()首先通過pthread_getspecific()獲取與線程私有變量tls_key綁定的ptr,然后打印當前線程的線程號和ptr數組的首地址,然后循環5次,間隔隨機時間打印當前線程號,i的值(從1到5),ptr[i]中的學生學號stuno和學生姓名stuname。
在線程函數thread_func2中:
static void *thread_func2(void *args) {struct msg_struct2 *ptr;ptr = (struct msg_struct2 *)malloc(5*sizeof(struct msg_struct2)); /* storage in process heap */printf("thread_func2: tid = %ld ptr = %p\n", gettid(), ptr); pthread_setspecific(tls_key, ptr);ptr->stuno = 19000001;sprintf(ptr->stuname, "Bob");sprintf(ptr->nationality, "United Kingdom");(ptr+2)->stuno = 19000003;sprintf((ptr+2)->stuname, "John");sprintf((ptr+2)->nationality, "United States");print_msg2();free(ptr);ptr = NULL;pthread_exit(0); }首先打印當前線程號和線程堆區的動態申請內存的struct msg_struct2 *類型的指針變量ptr的地址,然后使用語句pthread_setspecific(tls_key, ptr)將這個線程中的tls_key與ptr綁定,然后設置這個線程中的ptr指向的學生學號stuno、姓名stuname和國籍nationality分別設置為19000001、Bob和United Kingdom,ptr+2指向的學生學號和姓名分別設置為19000003、John和United States,然后使用語句print_msg2()首先通過pthread_getspecific()獲取與線程私有變量tls_key綁定的ptr,然后打印當前線程的線程號和ptr指針的地址,然后循環5次,間隔隨機時間打印當前線程號,i的值(從1到5),ptr[i]中的學生學號stuno、學生姓名stuname和學生國籍nationality。
回到主線程中, 使用pthread_join()函數使主線程等待連個子線程結束后再運行,子線程全部結束后,使用語句pthread_key_delete(tls_key)釋放tls_key的內存空間,然后返回。
- 驗證實驗alg.14-4-tls-pthread-key-3.c
執行程序命令:
gcc alg.14-4-tls-pthread-key-3.c -pthread ./a.out分析:
相比之前一個程序alg.14-3-tls-pthread-key-2,這個程序的兩個線程沒有分別調用print_msg1()和print_msg2()函數,而是調用了同一個print_msg()函數,可以看到,兩個線程的print_msg()函數打印出線程號和ptr首地址不同,說明同一個print_msg()函數分別有兩個線程各自ptr變量的數據副本。
實現細節解釋:
與之前一個程序alg.14-3-tls-pthread-key-2相比,這個程序的第二個線程運行函數thread_func2()的ptr由之前的動態內存分配處于堆區變成了臨時數組變量處于棧區,兩個線程也沒有分別調用print_msg1()和print_msg2()函數,而是調用了同一個print_msg()函數,這個print_msg()和之前print_msg1()函數與print_msg2()函數的作用基本相同。
- 驗證實驗alg.14-5-tls-pthread-key-4.c
執行程序命令:
gcc alg.14-5-tls-pthread-key-4.c -pthread ./a.out分析:
可以看到,無論與線程私有變量綁定的變量是否有效,數據是否丟失,線程私有變量都可以繼續工作。
可以看到,在創建的子線程中調用的函數中將線程棧區的臨時變量與線程私有變量綁定時,函數返回時棧區會被釋放,在子進程中想打印與線程私有變量綁定的棧區變量時數據會發生丟失,產生亂碼,因為棧區數據已經釋放掉了。
而在創建的子線程中調用的函數中將線程堆區的動態內存分配的變量與線程私有變量綁定時,函數返回時堆區數據如果不調用free()函數則不會被釋放,在子進程中想打印與線程私有變量綁定的堆區變量時數據不會丟失,可以正常打印,這也提醒我們如果不及時釋放內存會導致內存泄露。
實現細節解釋:
與之前的程序alg.14-3-tls-pthread-key-2相比,這個程序的主線程中只創建了一個子線程,線程函數為:
static void *thread_func(void *args) {struct msg_struct *ptr;thread_data1();ptr = (struct msg_struct *)pthread_getspecific(tls_key); /* get ptr from thread_data1() */perror("pthread_getspecific()");printf("ptr from thread_data1() in thread_func(): %p\n", ptr);for (int i = 1; i < 6; i++) {printf("tid = %ld i = %2d %s %*.*s\n", gettid(), i, (ptr+i-1)->stuno, 8, 8, (ptr+i-1)->stuname);}thread_data2();ptr = (struct msg_struct *)pthread_getspecific(tls_key); /* get ptr from thread_data2() */perror("pthread_getspecific()");printf("ptr from thread_data2() in thread_func(): %p\n", ptr);for (int i = 1; i < 6; i++) {printf("tid = %ld i = %2d %s %*.*s\n", gettid(), i, (ptr+i-1)->stuno, 8, 8, (ptr+i-1)->stuname);}free(ptr);ptr = NULL;pthread_exit(0); }在這個線程函數中,和之前的程序在創建的子線程中設置線程私有變量tls_key的值不同,這個程序在創建的子線程中調用函數在這個函數中設置線程私有變量tls_key的值。
首先程序運行了相當于alg.14-3中的thread_func1()函數作用的thread_data1()函數,在函數中將棧區的臨時數組變量ptr和線程私有變量tls_key綁定,但是由于線程棧區在函數返回時會被釋放,所以回到子線程中運行和之前print_msg1()函數作用相同的代碼塊時,會發現和線程私有變量tls_key綁定的ptr發生了丟失,產生許多亂碼,因為ptr已經被釋放掉了。
運行了相當于alg.14-3中的thread_func2()函數作用的thread_data2()函數,在函數中將堆區的動態內存分配的指針變量ptr和線程私有變量tls_key綁定,但是由于線程堆區的變量在函數返回時如果沒有調用free()函數就不會被釋放,所以回到子線程中運行和之前print_msg2()函數作用相同的代碼塊時,會發現和線程私有變量tls_key綁定的ptr沒有丟失,可以正常打印數據。
Linux clone()
- Linux提供fork()和vfork()系統調用,具有復制進程的傳統功能。Linux還提供了使用clone()系統調用創建線程的能力。
-
事實上,Linux在提到程序中的控制流時使用的是術語“任務”,而不是“進程”或“線程”。它不區分進程和線程。
-
帶有一組標志的clone()允許子任務共享父任務的一些資源。這些標志確定父任務和子任務之間要進行多少共享。
-
如果在調用clone()時沒有設置這些標志,則不會發生共享,這類似于fork()系統調用提供的共享。
標志含義 CLONE_FS 共享文件系統信息 CLONE_VM 共享相同的內存空間 CLONE_SIGHAND 共享信號處理程序 CLONE_FILES 共享一組打開的文件
-
clone()函數可以用來創建線程,其中第一個參數fn是函數指針,指向線程要執行的函數,第二個參數child_stack是為子線程分配的系統堆棧空間,指定子線程使用的堆棧的位置,第三個參數flags為復制資源的標志,用來表示子線程需要繼承哪些資源,第四個參數arg是傳給子進程的參數。
- 驗證實驗alg.14-6-clone-demo.c
執行程序命令:
gcc alg.14-6-clone-demo.c -pthread ./a.out分析:
可以看到,最后parent read buf中buf中的內容沒有被子線程改變,說明每一個線程或進程(任務)都有它的不同內存空間。
主線程等待它創建的任意一個子線程執行結束后再和另外一個子線程異步執行,因為只有一個子線程返回主線程就繼續執行然后結束了,所以可以看到線程號為40706的子線程成為了僵尸線程。
若編譯指令:
./a.out vm那么flag選項設置CLONE_VM,父進程和子進程運行時會共享相同的內存空間。
可以看到,最后parent read buf中buf中的內容被子線程改變,說明每個線程或進程(任務)共享相同的內存空間。
主線程先等待和它創建的任意一個子線程結束,然后跟另一個子線程異步執行。
若編譯指令:
./a.out vm vfork那么flag選項設置CLONE_VFORK,那么父進程會被掛起,直到子進程釋放虛擬內存資源才繼續運行。
可以看到,主線程被掛起,直到子線程結束后再繼續執行。
實現細節解釋:
一開始動態內存申請兩個大小為STACK_SIZE的char類型指針stack1和stack2,初始化標志變量flag為0,如果傳入的第一個參數為vm,那么設置flag為flags | CLONE_VM,代表可以父進程和子進程運行時共享相同的內存空間,如果傳入的第二個參數為vfork,那么設置flag為flags | CLONE_VFORK,代表運行時父進程被掛起,直到子進程釋放虛擬內存資源。
然后打印父進程的進程號和語句parrent clone ...,表示準備要使用clone()函數了。接著使用語句clone(child_func1, stack1 + STACK_SIZE, flags | SIGCHLD, buf)創建一個子線程并把返回值即創建的線程的線程號賦給變量chdtid1,其中第一個參數child_func1是線程執行函數,第二個參數stack1 + STACK_SIZE是子線程使用的系統堆棧的棧頂位置,第三個參數flags | SIGCHLD代表設置子線程從主線程繼承的資源,同時SIGCHLD代表在子線程終止時,向主線程發送信號,第四個參數buf是向子線程傳送的參數。這里是說明主線程的線程號的一條語句。
在線程運行函數child_func1()中:
static int child_func1(void *arg) {char *chdbuf = (char*)arg; /* type casting */printf("child_func1 read buf: %s\n", chdbuf);sleep(1);sprintf(chdbuf, "I am child_func1, my tid = %ld, pid = %d", gettid(), getpid());printf("child_func1 set buf: %s\n", chdbuf);sleep(1);printf("child_func1 sleeping and then exists ...\n");sleep(1);return 0; }首先打印傳遞進的字符串的參數的內容,接著休眠1s,打印子線程的線程號和進程號,然后休眠1s,打印語句child_func1 sleeping and then exists ...,然后結束。
回到主線程中, 然后使用語句clone(child_func2, stack2 + STACK_SIZE, flags | SIGCHLD, buf)創建一個子線程并把返回值即創建的線程的線程號賦給變量chdtid2,線程執行函數child_func2()和之前的child_func1()的作用差不多。
接著使用waitpid(-1, &status, 0) == -1讓主線程等待任意一個子線程結束后再繼續執行,參數-1表示不等待某個特定的子進程而是回收任意一個子進程,參數0表示以默認的阻塞方式來進行等待任意一個子線程結束然后繼續執行。
休眠1s,打印父進程的進程號,系統調用語句ps顯示當前進程狀態。
- 驗證實驗alg.14-7-clone-stack.c
執行程序命令:
gcc alg.14-7-clone-stack.c -pthread ./a.out分析:
可以看到,在實驗環境下使用clone()函數創建出的子線程遞歸調用可以使用棧空間的上限遞歸次數為732605,514288 *4096-1936125 *1024 = 123931648?,123931648?/1936125 = 64(字節), 說明每次遞歸的實驗環境系統開銷大概是64字節。
實現細節解釋:
一開始動態內存申請一個大小為STACK_SIZE的char類型指針stack,初始化標志變量flag為0。
接著使用語句clone(test, stack + STACK_SIZE, flags | SIGCHLD, buf)創建一個子線程并把返回值即創建的線程的線程號賦給變量chdtid,在線程運行函數test()中:
static int test(void *arg) { static int i = 0;char buffer[1024]; if(i == 0) {printf("test: my ptd = %d, tid = %ld, ppid = %d\n", getpid(), gettid(), getppid());printf("\niteration = %8d", i); }printf("\b\b\b\b\b\b\b\b%8d", i); i++; test(arg); /* recursive calling */ }首先初始化靜態變量i為0,然后如果i為0,那么打印子線程的進程號、線程號、父進程號和迭代次數即i的值,退出判斷語句,打印i的值,使i自增,最后使用test(arg)語句遞歸調用test()函數。
打印傳遞進的字符串的參數的內容,接著休眠1s,打印子線程的線程號和進程號,然后休眠1s,打印語句child_func1 sleeping and then exists ...,然后結束。
回到主線程中, 打印父進程的進程號和子線程的線程號,接著使用waitpid(-1, &status, 0) == -1讓主線程等待任意一個子線程結束后再繼續執行,并把返回的子線程的線程號賦給變量ret。
休眠2s,打印父進程的進程號和返回的子線程的線程號。
比較 pthread 和 clone() 線程實現機制的異同
不同點
pthread實現機制是基于用戶級線程的,在用戶空間運行線程庫,線程庫完成線程的創建、消息傳遞等操作,內核感知不到用戶線程的存在,此時以進程為單位,管理進程的執行狀態。
因為pthread創建出的線程是用戶線程,所以可以跨操作系統運行,不需要切換到內核模式就可以完成線程的切換,節省開銷和內核資源。但是在操作系統調度進程時,因為每個進程只有一個創建出來的線程可以執行,所以這個線程阻塞就會使整個進程阻塞,只能使用非內核調度自己實現的調度算法來實現這個線程。
clone() 實現機制是基于輕量級進程(LWP)的,LWP是內核支持的用戶線程,進行建立線程等操作時,內核可以感知用戶線程的存在,并且進行調度。
每個LWP都是獨立的線程調度單元,和特定的內核線程相聯系,具有部分內核線程的特點,會消耗內核棧空間,進行系統調度時需要在內核線程和用戶線程之間切換,系統調用的代價較高,所以一個系統不能支持大量LWP,但是因為每個LWP是獨立的線程調度單元,所以在操作系統調度進程時,即使創建出來的LWP被阻塞,不會影響整個進程的執行。
相同點
在Linux系統中,由于并沒有進程線程的區分,統一稱為任務,所以pthread中創建線程的pthread_create()函數,內部使用的也是clone()函數,為clone()函數設置特定標志后,實現了pthread_create()。
對 clone() 的 flags 采用不同的配置,設計測試程序討論其結果
- 配置包括 CLONE_PARENT, CLONE_VM, CLONE_VFORK, CLONE_FILES, CLONE_SIGHAND, CLONE_NEWIPC, CLONE_THREAD
| CLONE_PARENT | 子進程和調用者共享父進程 |
| CLONE_VM | 共享內存空間 |
| CLONE_VFORK | 運行時父進程被掛起,直至子進程釋放內存資源 |
| CLONE_FILES | 共享文件描述符表 |
| CLONE_SIGHAND | 共享信號處理表 |
| CLONE_NEWIPC | 子進程使用新的IPC命名空間 |
| CLONE_THREAD | 共享線程群 |
在文件alg.14-6-clone-demo.c中,已經測試了參數CLONE_VM和CLONE_VFORK的作用,所以設計程序時,參照了文件alg.14-6-clone-demo.c的部分內容。
- 測試參數CLONE_PARENT
子線程執行函數:
// 測試參數CLONE_PARENT所用到的子線程執行函數 static int CLONE_PARENT_func(void *arg) {// 打印子線程的線程號、進程號、父進程號printf("I am CLONE_PARENT_func, my tid = %ld, pid = %d, ppid = %d\n", gettid(), getpid(), getppid());return 0; }主函數中的測試代碼:
// 測試參數CLONE_PARENT printf("------------------------------------------------------------------\n"); // 設置參數CLONE_PARENT前 printf("Before set flags to CLONE_PARENT\n"); // 設置參數為0 flags = 0; printf("Result:\n"); chdtid_CLONE_PARENT = clone(CLONE_PARENT_func, stack_CLONE_PARENT + STACK_SIZE, flags | SIGCHLD, NULL); if(chdtid_CLONE_PARENT == -1) {perror("CLONE_PARENT before:clone()");exit(1); } // 打印主線程的進程號和父進程號 printf("I am main thread, my pid = %d, my ppid = %d\n", getpid(), getppid()); // 休眠1s以便子線程結束 sleep(1); printf("\n");// 設置參數CLONE_PARENT后 printf("After set flags to CLONE_PARENT\n"); // 設置參數為CLONE_PARENT flags |= CLONE_PARENT; printf("Result:\n"); chdtid_CLONE_PARENT = clone(CLONE_PARENT_func, stack_CLONE_PARENT + STACK_SIZE, flags | SIGCHLD, NULL); if(chdtid_CLONE_PARENT == -1) {perror("CLONE_PARENT after:clone()");exit(1); } // 打印主線程的進程號和父進程號 printf("I am main thread, my pid = %d, my ppid = %d\n", getpid(), getppid()); // 休眠1s以便子線程結束 sleep(1); printf("------------------------------------------------------------------\n\n");分析:
可以看到,在沒有設置參數時,主線程的線程號是81752,子線程的父進程的線程號是81752,說明子線程的父進程是創建它的主線程。
在設置了參數之后,主線程的父進程的線程號是79440,子線程的父進程的線程號是81572,說明子線程的父進程也是創建它的主線程的父進程,子線程和主線程是“兄弟”關系,共享同一個父進程。
- 測試參數CLONE_VM
子線程執行函數:
// 測試參數CLONE_VM所用到的子線程執行函數 static int CLONE_VM_func(void *arg) {// 獲取主線程傳來的緩沖區參數bufchar *chdbuf = (char*)arg;printf("CLONE_VM_func read buf: %s\n", chdbuf);sleep(1);// 設置緩沖區buf中的內容為子線程的信息sprintf(chdbuf, "I am CLONE_VM_func, my tid = %ld, pid = %d", gettid(), getpid());printf("CLONE_VM_func set buf: %s\n", chdbuf);sleep(1);// 子線程退出printf("CLONE_VM_func sleeping and then exists ...\n");sleep(1);return 0; }主函數中的測試代碼:
// 測試參數CLONE_VM printf("------------------------------------------------------------------\n"); printf("Before set flags to CLONE_VM\n"); // 設置參數為0 flags = 0; printf("Result:\n"); // 設置緩沖區buf中的內容為主線程的信息 sprintf(buf,"I am main thread, my pid = %d", getpid()); printf("main thread set buf: %s\n", buf); sleep(1); printf("parent clone ...\n"); chdtid_CLONE_VM = clone(CLONE_VM_func, stack_CLONE_VM + STACK_SIZE, flags | SIGCHLD, buf); if(chdtid_CLONE_VM == -1) {perror("CLONE_VM before:clone()");exit(1); } // 等待子線程執行完后主線程再繼續執行,測試子線程改變了緩沖區buf的內容是否會影響到主線程 waitpid(chdtid_CLONE_VM, &status, 0); // 打印此時緩沖區buf中的內容 printf("parent read buf: %s\n", buf); printf("\n");printf("After set flags to CLONE_VM\n"); // 設置參數為CLONE_VM flags |= CLONE_VM; printf("Result:\n"); // 設置緩沖區buf中的內容為主線程的信息 sprintf(buf,"I am main thread, my pid = %d", getpid()); printf("main thread set buf: %s\n", buf); sleep(1); printf("parent clone ...\n"); chdtid_CLONE_VM = clone(CLONE_VM_func, stack_CLONE_VM + STACK_SIZE, flags | SIGCHLD, buf); if(chdtid_CLONE_VM == -1) {perror("CLONE_VM after:clone()");exit(1); } // 等待子線程執行完后主線程再繼續執行,測試子線程改變了緩沖區buf的內容是否會影響到主線程 waitpid(chdtid_CLONE_VM, &status, 0); // 打印此時緩沖區buf中的內容 printf("parent read buf: %s\n", buf); printf("------------------------------------------------------------------\n\n");分析:
可以看到,在沒有設置參數時,在主線程設置了緩沖區buf里的內容之后,即使子線程在線程執行函數中也修改了緩沖區buf中的內容,但是回到主線程后,緩沖區buf中的內容仍為之前主線程設置的內容。
在設置了參數之后,在主線程設置了緩沖區buf里的內容之后,子線程在線程執行函數中也修改了緩沖區buf中的內容,回到主線程后,緩沖區buf中的內容變成了子線程設置的內容。
說明設置參數后,子線程和主線程在運行時共享內存空間。
- 測試參數CLONE_VFORK
子線程執行函數:
// 測試參數CLONE_VFORK所用到的子線程執行函數 static int CLONE_VFORK_func(void *arg) {printf("I am CLONE_VFORK_func, my tid = %ld, pid = %d\n", gettid(), getpid());printf("CLONE_VFORK_func sleeping 3s and then exists ...\n");// 休眠3s,如果主線程與子線程異步執行,那么主線程有足夠時間在這期間繼續執行,否則主線程會等待子線程執行完再繼續執行sleep(3);// 標志子線程執行完退出printf("CLONE_VFORK_func exists successfully!\n");return 0; }主函數中的測試代碼:
// 測試參數CLONE_VFORK printf("------------------------------------------------------------------\n"); printf("Before set flags to CLONE_VFORK\n"); // 設置參數為0 flags = 0; printf("Result:\n"); chdtid_CLONE_VFORK = clone(CLONE_VFORK_func, stack_CLONE_VFORK + STACK_SIZE, flags | SIGCHLD, buf); if(chdtid_CLONE_VFORK == -1) {perror("CLONE_VFORK before:clone()");exit(1); } // 在waitpid()函數之前打印主線程的信息,觀察主線程是否會等待子線程執行完后再執行 printf("I am main thread, my pid = %d\n", getpid()); waitpid(chdtid_CLONE_VFORK, &status, 0); printf("\n");printf("After set flags to CLONE_VFORK\n"); // 設置參數為CLONE_VFORK flags |= CLONE_VFORK; printf("Result:\n"); chdtid_CLONE_VFORK = clone(CLONE_VFORK_func, stack_CLONE_VFORK + STACK_SIZE, flags | SIGCHLD, buf); if(chdtid_CLONE_VFORK == -1) {perror("CLONE_VFORK after:clone()");exit(1); } // 在waitpid()函數之前打印主線程的信息,觀察主線程是否會等待子線程執行完后再執行 printf("I am main thread, my pid = %d\n", getpid()); waitpid(chdtid_CLONE_VFORK, &status, 0); printf("------------------------------------------------------------------\n\n");分析:
可以看到,在沒有設置參數時,子線程和主線程異步執行,打印主線程信息的語句在子線程還未執行完就直接執行。
在設置了參數之后,主線程被掛起,直到子線程終止后再繼續執行,即使子線程休眠了3s,主線程也未執行打印語句,直到子線程退出后,主線程才繼續執行,打印了主線程信息語句。
說明設置參數后,主線程被掛起,直到子線程執行完釋放資源后再繼續執行。
- 測試參數CLONE_FILES
子線程執行函數:
// 測試參數CLONE_FILES所用到的子線程執行函數 static int CLONE_FILES_func(void *arg) {// 獲取主線程傳來的文件描述符int *numptr = (int *)arg;int fd = *numptr;// 設置文件的FD_CLOEXEC參數為1fcntl(fd, F_SETFD, 1);printf("I am CLONE_FILES_func, my tid = %ld, pid = %d, ppid = %d\n", gettid(), getpid(), getppid());printf("CLONE_FILES_func sets the FD_COLEXEC of fd to %d\n", fcntl(fd, F_GETFD));return 0; }主函數中的測試代碼:
// 測試參數CLONE_FILES printf("------------------------------------------------------------------\n"); printf("Before set flags to CLONE_FILES\n"); int fd = open("./test.txt", O_RDWR | O_CREAT, 0666); if (fd < 0) {perror("CLONE_FILES:open()");exit(EXIT_FAILURE); }// 設置參數為0 flags = 0; printf("Result:\n"); // 設置文件的FD_CLOEXEC參數為0 fcntl(fd, F_SETFD, 0); printf("I am main thread, my pid = %d, my ppid = %d\n", getpid(), getppid()); printf("In the beginning, main thread sets the FD_COLEXEC of fd to %d\n\n", fcntl(fd, F_GETFD));chdtid_CLONE_FILES = clone(CLONE_FILES_func, stack_CLONE_FILES + STACK_SIZE, flags | SIGCHLD, &fd); if(chdtid_CLONE_FILES == -1) {perror("CLONE_FILES before:clone()");exit(1); }// 等待子線程執行完后主線程再繼續執行,測試子線程改變了文件的FD_CLOEXEC參數是否會影響到主線程 waitpid(chdtid_CLONE_FILES, &status, 0); // 查看文件的FD_CLOEXEC參數 printf("\nIn the last, the FD_COLEXEC of fd in main thread is %d\n\n\n", fcntl(fd, F_GETFD));printf("After set flags to CLONE_FILES\n"); // 設置參數為CLONE_FILES flags |= CLONE_FILES; printf("Result:\n"); // 設置文件的FD_CLOEXEC參數為0 fcntl(fd, F_SETFD, 0); printf("I am main thread, my pid = %d, my ppid = %d\n", getpid(), getppid()); printf("In the beginning, main thread sets the FD_COLEXEC of fd to %d\n\n", fcntl(fd, F_GETFD));chdtid_CLONE_FILES = clone(CLONE_FILES_func, stack_CLONE_FILES + STACK_SIZE, flags | SIGCHLD, &fd); if(chdtid_CLONE_FILES == -1) {perror("CLONE_FILES after:clone()");exit(1); }// 等待子線程執行完后主線程再繼續執行,測試子線程改變了文件的FD_CLOEXEC參數是否會影響到主線程 waitpid(chdtid_CLONE_FILES, &status, 0); // 查看文件的FD_CLOEXEC參數 printf("\nIn the last, the FD_COLEXEC of fd in main thread is %d\n", fcntl(fd, F_GETFD)); printf("------------------------------------------------------------------\n\n");分析:
可以看到,在沒有設置參數時,一開始,主線程先設置文件的FD_CLOEXEC文件描述符標志為0,然后子線程設置文件的FD_CLOEXEC文件描述符標志為1,最后在主線程中,查看文件的FD_CLOEXEC文件描述符標志,發現為0,說明子線程和主線程并不共享文件描述符表。
在設置了參數之后,一開始,主線程先設置文件的FD_CLOEXEC文件描述符標志為0,然后子線程設置文件的FD_CLOEXEC文件描述符標志為1,最后在主線程中,查看文件的FD_CLOEXEC文件描述符標志,發現為1,說明子線程和主線程共享文件描述符表。
- 測試參數CLONE_SIGHAND
信號處理函數:
// 主線程中的信號處理函數 void main_thread_handler(int signo) {printf("\nThis is main_thread_handler");printf("\nsignal catched: signo = %d\n", signo);return; }// 子線程中的信號處理函數 void CLONE_SIGHAND_handler(int signo) {printf("\nThis is CLONE_SIGHAND_handler");printf("\nsignal catched: signo = %d\n", signo);return; }子線程執行函數:
// 測試參數CLONE_SIGHAND所用到的子線程執行函數 static int CLONE_SIGHAND_func(void *arg) {// 設置捕捉到Ctrl+C信號的信號處理函數為CLONE_SIGHAND_handlersignal(SIGINT, CLONE_SIGHAND_handler);printf("I am CLONE_SIGHAND_func, my tid = %ld, pid = %d, ppid = %d\n", gettid(), getpid(), getppid());printf("CLONE_SIGHAND_func set CLONE_SIGHAND_handler\n\n");return 0; }主函數中的測試代碼:
// 測試參數CLONE_SIGHAND printf("------------------------------------------------------------------\n"); printf("Before set flags to CLONE_SIGHAND\n"); // 設置參數為0 flags = 0;printf("Result:\n"); printf("I am main thread, my pid = %d, my ppid = %d\n", getpid(), getppid()); printf("In the beginning, main thread set main_thread_handler\n\n"); // 設置捕捉到Ctrl+C信號的信號處理函數為main_thread_handler signal(SIGINT, main_thread_handler); // 從linux 2.6.0開始,當指定CLONE_SIGHAND后,必須也指定CLONE_VM chdtid_CLONE_SIGHAND = clone(CLONE_SIGHAND_func, stack_CLONE_SIGHAND + STACK_SIZE, flags | CLONE_VM | SIGCHLD, NULL); if(chdtid_CLONE_SIGHAND == -1) {perror("CLONE_SIGHAND before:clone()");exit(1); }// 等待子線程執行完后主線程再繼續執行,測試子線程改變了捕捉到Ctrl+C信號的信號處理函數是否會影響到主線程 waitpid(chdtid_CLONE_SIGHAND, &status, 0);// 休眠100s,便于輸入Ctrl+C信號,輸入后信號處理完畢后主線程繼續執行 printf("now start catching Ctrl+c\n"); sleep(100);printf("\n");printf("After set flags to CLONE_SIGHAND\n"); // 設置參數為CLONE_SIGHAND flags |= CLONE_SIGHAND;printf("Result:\n"); printf("I am main thread, my pid = %d, my ppid = %d\n", getpid(), getppid()); printf("In the beginning, main thread set main_thread_handler\n\n"); // 設置捕捉到Ctrl+C信號的信號處理函數為main_thread_handler signal(SIGINT, main_thread_handler); // 從linux 2.6.0開始,當指定CLONE_SIGHAND后,必須也指定CLONE_VM chdtid_CLONE_SIGHAND = clone(CLONE_SIGHAND_func, stack_CLONE_SIGHAND + STACK_SIZE, flags | CLONE_VM | SIGCHLD, NULL); if(chdtid_CLONE_SIGHAND == -1) {perror("CLONE_SIGHAND before:clone()");exit(1); }// 等待子線程執行完后主線程再繼續執行,測試子線程改變了捕捉到Ctrl+C信號的信號處理函數是否會影響到主線程 waitpid(chdtid_CLONE_SIGHAND, &status, 0);// 休眠100s,便于輸入Ctrl+C信號,輸入后信號處理完畢后主線程繼續執行 printf("now start catching Ctrl+c\n"); sleep(100);printf("------------------------------------------------------------------\n\n");分析:
可以看到,在沒有設置參數時,一開始,主線程先設置捕捉到Ctrl+C信號后的信號處理函數為main_thread_handler,然后子線程設置捕捉到Ctrl+C信號后的信號處理函數為CLONE_SIGHAND_handler,最后在主線程中,運行程序準備捕捉Ctrl+C信號,捕捉到后發現信號處理函數為main_thread_handler,說明子線程和主線程并不共享信號處理表。
在設置了參數之后,一開始,主線程先設置捕捉到Ctrl+C信號后的信號處理函數為main_thread_handler,然后子線程設置捕捉到Ctrl+C信號后的信號處理函數為CLONE_SIGHAND_handler,最后在主線程中,運行程序準備捕捉Ctrl+C信號,捕捉到后發現信號處理函數為CLONE_SIGHAND_handler,說明子線程和主線程共享信號處理表。
- 測試參數CLONE_NEWIPC
子線程執行函數:
// 測試參數CLONE_NEWIPC所用到的子線程執行函數 static int CLONE_NEWIPC_func(void *arg) {// 查看線程所處的IPC命名空間的消息隊列的信息printf("Message Queues in CLONE_NEWIPC_func:\n");system("ipcs -q");return 0; }主函數中的測試代碼:
// 測試參數CLONE_NEWIPC printf("------------------------------------------------------------------\n"); // 首先在主線程中創建一個消息隊列 printf("First create a message queue in main thread\n\n"); char pathname[10] = {"./test"}; struct stat fileattr; key_t key; int msqid; if(stat(pathname, &fileattr) == -1) {ret = creat(pathname, O_RDWR);if (ret == -1) {ERR_EXIT("CLONE_NEWIPC: creat()");}printf("shared file object created\n"); }key = ftok(pathname, 0x27); if(key < 0) {ERR_EXIT("ftok()"); }msqid = msgget((key_t)key, 0666 | IPC_CREAT); if(msqid == -1) {ERR_EXIT("msgget()"); }printf("Before set flags to CLONE_NEWIPC\n"); // 設置參數為0 flags = 0; printf("Result:\n\n");// 查看主線程的IPC命名空間中消息隊列的情況 printf("Command: ipcs -q\n\n"); printf("Message Queues in main thread:\n"); system("ipcs -q"); chdtid_CLONE_NEWIPC = clone(CLONE_NEWIPC_func, stack_CLONE_NEWIPC + STACK_SIZE, flags | SIGCHLD, NULL); if(chdtid_CLONE_NEWIPC == -1) {perror("CLONE_NEWIPC before:clone()");exit(1); } // 等待子線程執行完后主線程再繼續執行,測試子線程的命名空間是否和主線程一樣 waitpid(chdtid_CLONE_NEWIPC, &status, 0); printf("\n");printf("After set flags to CLONE_NEWIPC\n"); // 設置參數為CLONE_NEWIPC flags |= CLONE_NEWIPC; printf("Result:\n\n");// 查看主線程的IPC命名空間中消息隊列的情況 printf("Command: ipcs -q\n\n"); printf("Message Queues in main thread:\n"); system("ipcs -q"); chdtid_CLONE_NEWIPC = clone(CLONE_NEWIPC_func, stack_CLONE_NEWIPC + STACK_SIZE, flags | SIGCHLD, NULL); if(chdtid_CLONE_NEWIPC == -1) {perror("CLONE_NEWIPC after:clone()");exit(1); } // 等待子線程執行完后主線程再繼續執行,測試子線程改變了捕捉到Ctrl+C信號的信號處理函數是否會影響到主線程 waitpid(chdtid_CLONE_NEWIPC, &status, 0);// 刪除之前創建的消息隊列 sprintf(buf, "ipcrm -q %d", msqid); printf("Command: %s\n", buf); system(buf); printf("------------------------------------------------------------------\n\n");分析:
可以看到,首先創建一個消息隊列,在沒有設置參數時,在主線程和子線程中分別查看線程所處的IPC命名空間中的消息隊列情況,發現主線程和子線程所處的IPC命名空間中消息隊列的情況一樣,說明主線程和子線程處在同一個IPC命名空間中。
在設置了參數之后,,在主線程和子線程中分別查看線程所處的IPC命名空間中的消息隊列情況,發現主線程和子線程所處的IPC命名空間中消息隊列的情況不一樣,子線程的IPC命名空間中沒有消息隊列,說明主線程和子線程不處在同一個IPC命名空間中,子線程和主線程隔離。
- 測試參數CLONE_THREAD
子線程執行函數:
// 測試參數CLONE_THREAD所用到的子線程執行函數 static int CLONE_THREAD_func(void *arg) {// 打印子線程的線程號、進程號、父進程號printf("I am CLONE_THREADs_func, my tid = %ld, pid = %d, ppid = %d\n", gettid(), getpid(), getppid());return 0; }主函數中的測試代碼:
// 測試參數CLONE_THREAD printf("------------------------------------------------------------------\n"); printf("Before set flags to CLONE_THREAD\n"); // 設置參數為0 flags = 0; printf("Result:\n"); // 從Linux 2.5.35開始,如果指定了CLONE_THREAD,則必須同時指定CLONE_SIGHAND。而從Linux 2.6.0開始,指定CLONE_SIGHAND的同時也必須指定CLONE_VM chdtid_CLONE_THREAD = clone(CLONE_THREAD_func, stack_CLONE_THREAD + STACK_SIZE, flags | CLONE_VM | CLONE_SIGHAND | SIGCHLD, NULL); if(chdtid_CLONE_THREAD == -1) {perror("CLONE_THREAD before:clone()");exit(1); } // 打印主線程的進程號和父進程號 printf("I am main thread, my pid = %d, my ppid = %d\n", getpid(), getppid()); // 休眠1s以便子線程結束 sleep(1); printf("\n");printf("After set flags to CLONE_THREAD\n"); // 設置參數為CLONE_THREAD flags |= CLONE_THREAD; printf("Result:\n"); // 從Linux 2.5.35開始,如果指定了CLONE_THREAD,則必須同時指定CLONE_SIGHAND。而從Linux 2.6.0開始,指定CLONE_SIGHAND的同時也必須指定CLONE_VM chdtid_CLONE_THREAD = clone(CLONE_THREAD_func, stack_CLONE_THREAD + STACK_SIZE, flags | CLONE_VM | CLONE_SIGHAND | SIGCHLD, NULL); if(chdtid_CLONE_THREAD == -1) {perror("CLONE_THREAD after:clone()");exit(1); } // 打印主線程的進程號和父進程號 printf("I am main thread, my pid = %d, my ppid = %d\n", getpid(), getppid()); // 休眠1s以便子線程結束 sleep(1); printf("------------------------------------------------------------------\n\n");分析:
可以看到,在沒有設置參數時,主線程的線程號是81752,子線程的父進程的線程號是81752,說明子線程的父進程是創建它的主線程。
在設置了參數之后,主線程的父進程的線程號是79440,子線程的父進程的線程號是81572,說明子線程的父進程也是創建它的主線程的父進程,子線程和主線程是“兄弟”關系,共享線程群。
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的操作系统实验报告12:线程2的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 操作系统实验报告11:ucore Lab
- 下一篇: 操作系统实验报告13:线程池简单实现