1. 程式人生 > >併發伺服器--多程序實現

併發伺服器--多程序實現

通過簡單的socket可以實現一對一的c/s通訊,當多個客戶端同時進行伺服器訪問,那麼伺服器只能按序的一一進行處理,除了第一個客戶端,其餘客戶端都會陷入等待。並且這樣的程式只能實現半雙工通訊(資料能雙向傳輸,但同一時刻只能單向傳遞,通過切換傳輸方向實現雙工),而且實現方式繁瑣,功能拘束,實用價值很低。那麼要想實現一個伺服器能同時接受多個客戶端訪問並且能夠雙工通訊的併發伺服器,其中一種實現方式----多程序。

一.fork()函式

函式原型:

#include <unistd.h>

pid_t fork(void);   //失敗時返回-1

fork函式將會複製正在執行的、呼叫fork函式的程序(實際Linux實現了寫時賦值,建立新程序並不會直接建立全部記憶體副本,程序可隨意讀取記憶體資料,只有當有程序需要會記憶體進行修改時,作業系統才會為新程序建立新的記憶體副本)兩個程序都將執行fork函式呼叫之後的程式碼。為加以區分父程序和子程序,fork函式在不同程序返回值不同。

  • 父程序:fork函式返回子程序ID
  • 子程序:fork函式返回0
int mian(void)
{
    ...
    pid_t pid = fork();
    if(pid == -1)
    {
        printf("程序建立失敗\n");
    }
    if( 0 == pid ) 
    {
        ...//子程序執行程式碼
    }
    else
    {
        ...//父程序執行程式碼
    }
    return 0;
}

二、殭屍程序

當子程序執行完畢後,並不會釋放所有資源,子程序會保留一部分資源(子程序的結束狀態等資訊)等待父程序回收,當子程序退出,父程序未回收這段時間,子程序就成為殭屍程序。

銷燬殭屍程序1:wait函式

#include <sys/wait.h>
pid_t wait(int *status);  //成功時返回終止的子程序ID,失敗時返回-1

呼叫此函式時如果已有子程序終止,那麼子程序終止時傳遞的返回值(exit函式的引數值、main函式的return返回值)將儲存到該函式的引數所指記憶體空間。但函式引數指向的單元中還包含其他資訊,需要通過下列巨集進行分離。

  • WIFEXITED 子程序正常終止時返回"true"。
  • WEXITSTATUS 返回子程序的返回值。

示例:

if(WIFEXITED(status)) //是否正常終止
{
    puts("Normal termination!");
    printf("Child pass num: %d",WEXITSTATUS(status)); //返回值
}

呼叫wait函式時,如果沒有已終止的子程序,那麼程式將阻塞直到有子程序終止。

銷燬殭屍程序2:waitpid函式

wait函式會引起程式阻塞,呼叫waitpid函式既可以銷燬殭屍程序,又能防止阻塞。

#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);   //成功時返回終止的子程序ID(或0),失敗時返回-1。
  • pid       等待終止的目標子程序ID,若傳遞-1,則與wait函式相同,可以等待任意子程序終止。
  • status   傳遞子程序退出的返回值。
  • options 傳遞標頭檔案sys/wait.h中宣告的常量WNOHANG,即使沒有終止的子程序也不會進入阻塞狀態,而是返回0並退出函式。
waitpid(-1,&status,WNOHANG);  //銷燬任意殭屍程序,若沒有終止的子程序則返回0

訊號處理

子程序不知何時結束時,waitpid函式要麼迴圈呼叫,要麼在程序中阻塞等待,無論哪一種都不是優秀的解決方案。

子程序終止的識別主體是作業系統,因此,若作業系統能把如下資訊告訴正忙於工作的父程序,就能搞笑的解決問題。

所以這裡引入了訊號處理(Signal Handling)機制。當特定的事件發生時由作業系統向程序發出訊息,程序執行相關操作響應該訊息。

函式原型:

#include <signal.h>
void (*signal(int signo,void (*func)(int)))(int); 
  • 函式名:signal
  • 引數:int signo , void (*func)(int)    //返回值為void 引數列表為int的函式的指標
  • 返回型別:引數型別位int 返回void型的函式指標

引數signo代表監聽的訊號,void (*func)(int)代表在訊號發生時呼叫的函式地址值(指標)。

在signal函式中註冊的部分特殊情況(訊號)和對應的常數。

  • SIGALRM:已到通過呼叫alarm函式註冊的時間
  • SIGINT:輸入CTRL+C
  • SIGCHLD:子程序終止
void child(int)
{
    ...
}

signal(SIGCHLD,child);   //子程序終止時,呼叫child函式(child函式返回型別為void,引數列表為int)

發生SIGALRM訊號,需要介紹alarm函式

#include <unistd.h>
unsigned int alarm(unsigned int seconds);  //返回0或以秒為單位的距SIGALRM訊號發生所剩時間

示例:

void timeout(int sig)
{
    if(SIGALRM == sig)
        puts("Time out!");
}

signal(SIGALRM,timeout);
alarm(2);  //兩秒後發出SIGALRM訊號

利用sigaction函式進行訊號處理

由於signal函式在UNIX系列的不同作業系統中可能存在區別,但sigaction函式完全相同,相比signal函式更穩定常用。

函式原型:

#include <signal.h>
int sigaction(int signo,const struct sigaction * act,struct sigaction * oldact);  //成功時返回0,失敗時返回-1
  • signo    與signal函式相同,傳遞訊號資訊
  • act        對應於第一個引數的訊號處理函式(訊號處理器)資訊
  • oldact   通過此引數獲取之前註冊的訊號處理函式指標,若不需要則傳遞0

sigaction結構體:

struct sigaction
{
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
}

結構體的sa_handler成員儲存訊號處理函式的指標值。sa_mask和sa_flags初始化為0即可,這兩個成員用於指定訊號相關的選項和特性。

//銷燬殭屍程序
void dest_childpro(int sig)
{
    int status;
    pid_t pid = waitpid(-1,&status,WNOHANG);
    if(WIFEXITED(status))
    {
        printf("Removed proc id: %d \n",pid);
        printf("Child send: %d \n",WEXITSTATUS(status));
    }
}

int main(void)
{
    int i;
    struct sigaction act;
    act.sa_handler = dest_childpro;
    sigemptyset(&act.sa_mask);    //呼叫sigemptyset函式將sa_mask成員的所有位初始化為0
    act.sa_flags = 0;
    sigaction(SIGCHLD,&act,0);
    pid_t pid = fork();
    if(0==pid)
    {
        puts("I'm child process");
        sleep(3);
        return 1;
    }
    else
    {
        for(i=0;i<5;i++)
            sleep(5);
    }
    return 0;
}

利用以上知識加上socket程式設計即可完成多程序的併發伺服器,值得一提的是套接字作為作業系統的資源,並不會在fork函式後被複制,但會複製其檔案描述符,所以最好子程序中close監聽套接字檔案描述符,一個套接字同時存在多個檔案描述符時,只有所有檔案描述符都銷燬時,才能銷燬套接字。

I/O分割

pid_t pid = fork();
if( 0 == pid)
{
    write();  //寫
    ...
}
else
{
    read();  //讀
    ...
}
...
分割I/O後,子程序負責寫,父程序負責讀,就能實現雙工服務端了。

程序間通訊

程序通訊(IPC)意味著兩個不同程序間可以交換資料,為了完成這一點,有多種方式實現,在這介紹一種較為簡單的單工通訊--管道。

管道並非程序資源,和套接字一樣,屬於作業系統,也就是不會被fork函式複製。

函式原型:

#include <unistd.h>
int pipe(int filedes[2]);   //成功時返回0,失敗時返回-1
  • filedes[0]   通過管道接收資料時使用的檔案描述符,即管道出口
  • filedes[1]   通過管道傳輸資料時使用的檔案描述符,即管道入口

以2個元素的int陣列地址作為引數呼叫上述函式時,陣列中存在兩個檔案描述符,它們將被用作管道的出口和入口。父程序呼叫該函式建立管道,同時獲取對應於出入口的檔案描述符,通過fork函式,檔案描述符也會被複制,子程序也能通過檔案描述符進行管道通訊。

char str1[] = "helloworld";
char str2[11];
int fds[2];
int *read_fd = &fds[0];
int *write_fd = &fds[1];
pid_t pid = fork();
if(0==pid)
{
    read(write_fd,str2,10);
}
else
{
    write(read_fd,str1,10);
}

以上為管道的單向通訊,其實當父子程序都擁有管道的檔案描述符後,不難實現可以利用管道雙向通訊(父子程序都利用兩個檔案描述符),但是資料進入管道後就成為無主資料,先進行讀操作的程序會把資料取走。這樣一來,就增加了程式設計的複雜度和降低了安全性。既然一個管道用作單向通訊,那麼我們可以再建立一個管道,兩個管道分別作為父子程序的讀寫。(以此思路可以實現程序間雙工通訊)。