在傳統的Unix模型中,當一個進程需要由另一個實體執行某件事時,該進程派生(fork)一個子進程,讓子進程去進行處理。
Unix下的大多數網路伺服器程式都是這麼編寫的,即父進程接受串連,派生子進程,子進程處理與客戶的互動。
雖然這種模型很多年來使用得很好,但是fork時有一些問題:
1. fork是昂貴的。記憶體映像要從父進程拷貝到子進程,所有描述字要在子進程中複製等等。目前有的Unix實現使用一種叫做寫時拷貝(copy-on-write)的技術,可避免父進程資料空間向子進程的拷貝。儘管有這種最佳化技術,fork仍然是昂貴的。
2. fork子進程後,需要用處理序間通訊(IPC)在父子進程之間傳遞資訊。Fork之前的資訊容易傳遞,因為子進程從一開始就有父進程資料空間及所有描述字的拷貝。但是從子進程返回資訊給父進程需要做更多的工作。
線程有助於解決這兩個問題。線程有時被稱為輕權進程(lightweight process),因為線程比進程“輕權”,一般來說,建立一個線程要比建立一個進程快10~100倍。
一個進程中的所有線程共用相同的全域記憶體,這使得線程很容易共用資訊,但是這種簡易性也帶來了同步問題。
一個進程中的所有線程不僅共用全域變數,而且共用:進程指令、大多數資料、開啟的檔案(如描述字)、訊號處理常式和訊號處置、當前工作目錄、使用者ID和組ID。
但是每個線程有自己的線程ID、寄存器集合(包括程式計數器和棧指標)、棧(用於存放局部變數和返回地址)、error、訊號掩碼、優先順序。
在Linux中線程編程符合Posix.1標準,稱為Pthreads。所有的pthread函數都以pthread_開頭。
以下先講述5個基本線程函數,在調用它們前均要包括pthread.h標頭檔。然後再給出用它們編寫的一個TCP客戶/伺服器程式例子。
第一個函數:
int pthread_create (pthread_t *tid,const pthread_attr_t *attr,void *(*func)(void *),void *arg);
一個進程中的每個線程都由一個線程ID(thread ID)標識,其資料類型是pthread_t(常常是unsigned int)。如果新的線程
建立成功,其ID將通過tid指標返回。
每個線程都有很多屬性:優先順序、起始棧大小、是否應該是一個守護線程等等,當建立線程時,我們可通過初始化一個pthread_attr_t
變數說明這些屬性以覆蓋預設值。我們通常使用預設值,在這種情況下,我們將attr參數說明為空白指標。
最後,當建立一個線程時,我們要說明一個它將執行的函數。線程以調用該函數開始,然 後或者顯式地終止(調用pthread_exit) 或者隱式地終止(讓該函數返回)。函數的地址由func參數指定,該函數的調用參數是一個指標arg,如果我們需要多個調用參數,我們必須將它們打包成一 個結構,然後將其地址當作唯一的參數傳遞給起始函數。
在func和arg的聲明中,func函數取一個通用指標(void *)參數,並返回一個通用指標(void *),這就使得我們可以傳遞一個
指標(指向任何我們想要指向的東西)給線程,由線程返回一個指標(同樣指向任何我們想要指向的東西)。
調用成功,返回0,出錯時返回正Exxx值。Pthread函數不設定errno。
第二個函數:
int pthread_join(pthread_t tid,void **status);
該函數等待一個線程終止。把線程和進程相比,pthread_creat類似於fork,而pthread_join類似於waitpid。
我們必須要等待線程的tid,很可惜,我們沒有辦法等待任意一個線程結束。
如果status指標非空,線程的傳回值(一個指向某個對象的指標)將存放在status指向的位置。
第三個函數;
pthread_t pthread_self(void);
線程都有一個ID以在給定的進程內標識自己。線程ID由pthread_creat返回,我們可以pthread_self取得自己的線程ID。
第四個函數:
int pthread_detach(pthread_t tid);
線程或者是可匯合的(joinable)或者是脫離的(detached)。當可匯 合的線程終止時,其線程ID和退出狀態將保留,直到另外一個線程調用pthread_join。脫離的線程則像守護進程:當它終止時,所有的資源都釋放, 我們不能等待它終止。如果一個線程需要知道另一個線程什麼時候終止,最好保留第二個線程的可匯合性。
Pthread_detach函數將指定的線程變為脫離的。
該函數通常被想脫離自己的線程調用,如:pthread_detach (pthread_self ( ));
第五個函數:
void pthread_exit(void *status);
該函數終止線程。如果線程未脫離,其線程ID和退出狀態將一直保留到調用進程中的某個其他線程調用pthread_join函數。
指標status不能指向局部於調用線程的對象,因為線程終止時這些對象也消失。
有兩種其他方法可使線程終止:
1. 啟動線程的函數(pthread_creat的第3個參數)返回。既然該函數必須說明為返回一個void指標,該傳回值便是線程的終止狀態。
2. 如果進程的main函數返回或者任何線程調用了exit,進程將終止,線程將隨之終止。
以下給出一個使用線程的TCP回射客戶/伺服器的例子,完成的功能是用戶端使用線程 給伺服器發從標準輸入得到的字元,並在主線程中將從伺服器端返回的字元顯示到標準輸出,伺服器端將用戶端發來的資料原樣返回給用戶端,每一個客戶在伺服器 上對應一個線程。利用該程式架構,通過擴充用戶端和伺服器端的處理功能,可以完成多種基於多線程的客戶機/伺服器程式。該程式在RedHat 6.0和TurboLinux4.02下調試通過。
共用標頭檔如下:
(head.h) #include #include #include #include #include #include #include #include #include #include #include #define MAXLINE 1024 #define SERV_PORT 8000 #define LISTENQ 1024 static int sockfd; static FILE *fp; |
公用函數如下(common.c):
/* 從一個描述字讀文本行 */ ssize_t readline(int fd, void *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; for (n=1; n0) { if ( (nwritten=write (fd, ptr, nleft ) )<=0 ) { if (errno==EINTR ) nwritten=0; else return (-1); } nleft-=nwritten; ptr++=nwritten; } |
用戶端主程式如下:
(client.c) #include “head.h"; #include “common.c"; /* 在str_cli中定義的要被線程執行的函數 */ void *copyto (void *arg) { char sendline[MAXLINE]; while (fgets (sendline,MAXLINE,fp) !=NULL ) writen(sockfd,sendline,strlen(sendline)); shutdown(sockfd,SHUT_WR); return(NULL); }
void str_cli(FILE *fp_arg, int sockfd_arg) { char recvline[MAXLINE]; pthread_t tid; sockfd=sockfd_arg; fp=fp_arg; pthread_creat(&tid, NULL, copyto, NULL); while (readline (sockfd,recvline,MAXLINE) >0) ---- fputs(recvline,stdout); }
int main ( int argc, char **argv ) { int sockfd; struct sockaddr_in servaddr; if (argc!=2 ) printf ( “ usage: tcpcli " ); exit(0); bzero(&servaddr, sizeof (servaddr)) ; servaddr.sin_family=AF_INET; servaddr.sinport=htons(SERV_PORT); inet_pton (AF_INET, argv[1], &servaddr.sin_addr ); connect (sockfd, (struct sockaddr *)&servaddr, siziof (servaddr ) ); str_cli (stdin, sockfd ); exit (0 ); } |
伺服器端主程式如下:
(server.c) #include “head.h"; #include “common.c"; void str_echo (int sockfd ) { ssize_t n; char line[MAXLINE]; for (; ; ) { if ( (n=readline (sockfd, line, MAXLINE) )==0) return; writen (sockfd, line, n); } }
static void *doit ( void *arg) { pthread_detach(pthread_self ( ) ); str_echo ( (int ) arg ); close ( (int ) arg ); return ( NULL ) ; }
int main ( int argc, char **argv ) { int listenfd, connfd; socklen_t addrlen,len; struct sockaddr_in cliaddr, servaddr; pthread_t tid; listenfd=socket (AF_INET, SOCK_STREAM, 0 ); bzero (&servaddr, sizeof (servaddr ) ); servaddr.sin_family=AF_INET; servaddr.sin_addr.s_addr=htonl (INADDR_ANY ); servaddr.sin_port=SERV_PORT; bind (listenfd, ( struct sockaddr * )&servaddr, sizeof (servaddr ) ); listen (listenfd, LISTENQ ); addrlen=sizeof ( cliaddr ); cliaddr=malloc(addrlen ); for ( ; ; ) { len=addrlen; connfd=accept(listenfd, (struct sockaddr * )&cliaddr, &len ); pthread_creat ( &tid, NULL, &doit, ( void * )connfd ); } } |