線程和進程是面試中最常遇見的問題。有一個問題就是線程之間哪些東西是共享的。線程共享進程的整個地址空間,共享打開的文件,建立的socket等。線程有獨立的棧以及一些寄存器,用來進行調度。堆,數據區和代碼區是共享的。
地址空間
Linux下32位系統進程地址空間有4G,1G是內核地址,3G屬於自己,這3G內存由所有線程共享。這3G內存也包含了棧,那麼爲什麼說線程有自己獨立的棧呢?
看下面的程序
- #include <stdio.h>
- #include <pthread.h>
- #include <sys/types.h>
- #include <unistd.h>
- int global;
- int *heap;
- int *stack_address; //線程1將自己棧中的變量地址賦給stack_address,然後線程2訪問這個地址
- void * run1(void *param)
- {
- int stack_v = 123;
- global = 5;
- heap = malloc(12);
- heap[0] = 2;
- heap[1] = -3;
- stack_address = &stack_v;
- while(1) {}
- }
- void * run2(void *param)
- {
- printf("global %d\n",global);
- printf("heap[1] %d\n",heap[1]);
- printf("stack_v %d\n",*stack_address);
- }
- int main()
- {
- pthread_t tid1,tid2;
- pthread_attr_t attr;
- pthread_attr_init(&attr);
- pthread_create(&tid1,&attr,run1,NULL);
- pthread_create(&tid2,&attr,run2,NULL);
- pthread_join(tid1,NULL);
- pthread_join(tid2,NULL);
- return 0;
- }
運行結果:
可以看出,線程2和線程1的棧是位於同一個地址空間的,都是進程的地址空間。
線程的棧實際上是在進程的棧裏的,每個都是獨立的。
可以使用pthread_attr_getstacksize獲取線程棧的大小。
- #include <stdio.h>
- #include <pthread.h>
- #include <sys/types.h>
- #include <unistd.h>
- int global;
- int *heap;
- int *stack_address;
- pthread_attr_t attr1,attr2;
- void * run1(void *param)
- {
- int stack_v = 123;
- size_t stacksize;
- pthread_attr_getstacksize(&attr1,&stacksize);
- printf("stacksize of thread1 is %d MB\n",stacksize/(1024*1024));
- global = 5;
- heap = malloc(12);
- heap[0] = 2;
- heap[1] = -3;
- stack_address = &stack_v;
- sleep(3);
- }
- void * run2(void *param)
- {
- size_t stacksize;
- printf("global %d\n",global);
- printf("heap[1] %d\n",heap[1]);
- printf("stack_v %d\n",*stack_address);
- pthread_attr_getstacksize(&attr2,&stacksize);
- printf("stacksize of thread2 is %d MB\n",stacksize/(1024*1024));
- }
- void fun()
- {
- static calltime = 1;
- char dummy[1024*1024];
- printf("calltime=%d\n",calltime);
- calltime ++;
- fun();
- }
- int main()
- {
- pthread_t tid1,tid2;
- pthread_attr_init(&attr1);
- pthread_attr_init(&attr2);
- pthread_create(&tid1,&attr1,run1,NULL);
- pthread_create(&tid2,&attr2,run2,NULL);
- pthread_join(tid1,NULL);
- pthread_join(tid2,NULL);
- fun();
- return 0;
- }
運行結果:
可以看到線程1,2和主線程的棧的大小都是10M,調用了11次才崩潰是因爲有一些對齊以及段與段之間有一些gap。
我當前的ulimit將棧的大小設置成了10M。可以使用pthread_attr_setstacksize在創建線程的時候設置線程的棧大小。
因此,對於線程有自己獨立的棧空間的真正理解應該是:線程的棧位於同一個地址空間裏,但是互相不重疊,線程屬性裏有它的棧的起止地址,一個線程可以讀寫其它線程的棧,只要它知道地址。
打開的文件(包括socket)
對於兩個進程,它們分別打開了一個文件,進程1得到的文件描述符是fd1,進程2得到的是fd2。即使進程1通過某種手段將fd1傳到進程2中,進程2也訪問不了fd1對應的文件,因爲進程間文件描述符是獨立的。fd1可能等於fd2,但是它們不對應同一個文件。
對於線程就不一樣了。線程1得到了描述符fd1,那麼線程2就可以通過fd1訪問對應的文件,即使線程1不告訴線程2,線程2自己通過某種手段知道了,它也能訪問。
每個進程有一個文件描述符空間,就類似於地址空間,線程也是共享這個文件描述符空間的。
信號
如果向一個進程發送信號,而這個進程有多個線程。那麼發給哪個呢?還是所有的線程都能收到?
實際上,進程維護一個統一的信號隊列,給這個進程發送一個信號,信號被存到信號隊列裏了,所有線程都可以看到。當調度到某個線程時,它發現隊列裏有信號,它拿出來處理了,這樣其它線程進入被調度時已經看不到這個信號了。
看下面這個例子:
- #include <stdio.h>
- #include <pthread.h>
- #include <sys/types.h>
- #include <unistd.h>
- #include <signal.h>
- void sigroutine1(int no)
- {
- printf("thread1 received a signal\n");
- }
- void sigroutine2(int no)
- {
- printf("thread2 received a signal\n");
- }
- void * run1(void *param)
- {
- while(1)
- {
- signal(SIGHUP,sigroutine1);
- }
- }
- void * run2(void *param)
- {
- while(1)
- {
- signal(SIGHUP,sigroutine2);
- }
- }
- int main()
- {
- pthread_t tid1,tid2;
- pthread_attr_t attr;
- pthread_attr_init(&attr);
- pthread_attr_init(&attr);
- pthread_create(&tid1,&attr,run1,NULL);
- pthread_create(&tid2,&attr,run2,NULL);
- pthread_join(tid1,NULL);
- pthread_join(tid2,NULL);
- return 0;
- }
兩個線程都在等待一個信號,我們向這個進程發送信號。
每發送一個信號,只有一個線程收到了,而且不一定是哪個線程。
(上述內容僅僅是我自己實驗得到的結論,不一定正確)
參考文獻
https://computing.llnl.gov/tutorials/pthreads/#Abstract
http://www.ibm.com/developerworks/cn/linux/kernel/l-thread/
http://lenky.info/2013/02/06/linux%E7%BA%BF%E7%A8%8B%E7%9B%B8%E5%85%B3%E6%A6%82%E5%BF%B5/