歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

Linux下的TCP/IP編程----進程及多進程服務端

在之前的學習中我們的服務端同一時間只能為一個客戶端提供服務,即使是將accept()函數包含在循環中,也只能是為多個客戶端依次提供服務,並沒有並發服務的能力,這顯然是不合理的。通過多進程的使用,我們可以很便捷的實現服務端的多進程,這樣就可以同時為多個客戶端提供服務。


首先我們要理解程序,進程,進程ID,僵屍進程,線程的概念。

程序:廣泛的說就是為了達到某一目的二規定的途徑,在編程中具體的就是為了實現某一功能而編寫的代碼實體,是靜態的。

進程:程序的一次動態執行就是一個進程,它是占用了一定內存空間,正在運行的程序。

進程ID:在操作系統中,PCB(進程控制塊)是進程存在的唯一標識,而其中進程ID則用於區分進程控制塊,在系統中是唯一的,每個進程都有一個唯一的進程ID用於區分。

線程:進程更小的資源集合體,它不持有系統資源,而是通過向所在進程申請資源運行,是系統進行調度的最小單位。一個進程中可以包含有多個線程。

僵屍進程:在進程執行完之後本因該銷毀,但有時因為一些原因而是的其保留了下來,雖然其已經釋放了絕大部分的系統資源,但是仍保留了一小部分系統資源,對於這種進程我們稱之為”僵屍進程”。

僵屍進程產生的原因:在Linux中,為了使得子進程在結束之後其結束時的信息和狀態可以被父進程查詢到,所以設置了一個機制:即當進程運行完成之後會保留一部分的資源用於保存結束時的狀態和信息(例如進程ID,返回值等),直到其父進程獲取了這些信息,子進程才完全的釋放了所占用的資源。由於這種機制的存在,當父進程中執行的任務比子進程繁重時(即子進程比父進程先結束)子進程就必須要等待父進程來主動的獲取信息,這樣就使得僵屍進程產生了。

附:僵屍進程的產生是由於子進程比父進程先結束,必要要等到父進程查詢其結束信息而產生,那麼自然而然的就考慮到若是父進程比子進程先結束又會發生聲麼呢?當子進程還沒有執行完全部任務時,若是父進程已經結束運行,那麼子進程會托管給系統中進程ID為1的進程(用於協助操作系統的進程),即子進程的父進程變成了進程ID為1的進程,這時候系統會自動進行信息的采集,以保證不會產生僵屍進程。


在了解了基本的概念之後,我們就可以嘗試進行多進程的實踐。創建進程的方式有很多,我們只了解創建多進程服務端的函數。

pid_t fork(void):復制當前進程成為新進程

成功時返回進程ID,失敗時返回-1

子進程中返回ID為0

父進程中返回ID為子進程的進程ID

詳解:通過調用fork()函數,我們可以復制當前的進程(調用fork()函數的進程)成為一個新的子進程,倆者擁有相同而又各自獨立的內存空間(也就是把父進程的內存完全復制到了另一塊內存中)。我們可以根據函數的返回值來區分父進程和子進程。

#include 
#include

int main(int argc ,char *argv[]){

    // 創建子進程
    pid_t pid = fork();

    //兩個進程將根據PID的不同來執行不同的代碼,在父進程中pid的值為子進程的進程ID,在子進程中pid的值為0
    if(pid == 0){
        printf("I am Child process  pid %d\n ",pid);
    }else{
        printf("I am parent process Child Pid %d \n",pid );
        sleep(30);
    }

    return 0;
}

對僵屍進程的解決辦法:

1. 使用wait()或者是waitpid()函數:

pid_t wait(int *statloc):獲取子進程的返回值

statloc(內存地址):當調用該函數時,若是存在已經結束的子進程,那麼該子進程的返回值就保存在該地址所指的內空間中。若是沒有已經結束的子進程,那麼就會阻塞主進程,直到有子進程結束為止。

成功時返回終止的子進程ID,失敗時返回-1

#include 
#include
#include

int main(int argc ,char *argv[]){
    //用於保存狀態
    int status;
    //fork一個子進程
    pid_t pid = fork();
    if(pid == 0){
        return 3;
    }else{
        printf("Child PID :%d \n",pid);
        //在父進程中在fork一個子進程
        pid = fork();
        if(pid == 0){
            exit(7);
        }else{
            printf("Child PID :%d \n",pid);
            //獲取子進程的返回值
            wait(&status);
            /**
             * 由於status所指向的單元中還包含其他的信息,所以要通過以下宏定義進行判斷和分離
             * WIFEXITED(status)------子進程正常終止時返回真
             * WEXITSTATUS(status)------子進程的返回值
            **/

            //判斷子進程是否正常結束
            if(WIFEXITED(status)){
                printf("Child send one : %d \n",WEXITSTATUS(status));
            }

            wait(&status);
            if(WIFEXITED(status)){
                printf("Child send one : %d \n",WEXITSTATUS(status));
            }
            sleep(60);

        }

    }
    return 0;
}

pid_t waitpid(pid_t pid , int *statloc , int options)獲取子進程的返回值:

pid(進程ID):要結束的進程ID statloc(地址指針):子進程的返回值等信息存放在該參數所指向的內存地址中 option(配置信息):傳遞頭文件sys/wait.h中聲明的常量
#include 
#include
#include

int main(int argc , char * argv[]){
      //用於保存狀態
    int status;
    //fork一個子進程
    pid_t pid = fork();
    if(pid == 0){
        sleep(15);
        return 24;
    }else{
        while(!waitpid(pid,&status,WNOHANG)){
            sleep(3);
            puts("sleap 3 seconds");
        }

        if(WIFEXITED(status)){
            printf("Child send %d \n",WEXITSTATUS(status));
        }
    }
return 0;
}

2. 通過信號量機制:

void (signal(int signum,void( func)(int)))(int)注冊信號量產生時需要調用的函數:

signo(信號量常量):要注冊的信號量類型

常見信號量:

信號量 對應事件 SIGALRM 到了使用alarm()函數注冊的時間 SIGINT 輸入Ctrl+C SIGCHLD 子進程結束 (* func)(int)(函數地址指針):指向處理信號量的函數的地址

返回之前注冊的函數指針。在發生信號量時會由系統喚醒正處於睡眠狀態的線程(即使是sleep時間未到也會被喚醒)

#include 
#include
#include
/**超時處理函數**/
void time_out (int sig){
    if(sig == SIGALRM){
        puts("Time Out ");
        alarm(2);
    }

}

/**鍵盤輸入處理函數**/

void key_control(int sig){
    if(sig == SIGINT){
        puts("Ctrl + C pressed");
    }
}


int main(int argc , char * argv[]){
   int i = 0;
    //向系統注冊超時處理信號
    signal(SIGALRM,time_out);
    //向系統注冊鍵盤輸入Ctrl+C處理信號的
    signal(SIGINT,key_control);
    //設置時鐘
    alarm(2);

    for(;i<3;i++){
        puts("wait.........");
        sleep(100);
    }
return 0;
}

int sigaction(int signo , const struct sigaction *act , struct signation *oldaction)處理系統發出的信號量

signo(信號量常數):傳入需要監聽的信號量

act(結構體指針):傳入處理信號量的結構體的地址

oldaction(結構體指針):通過次參數可獲得之前注冊的處理信號量

的結構體地址成功時返回0,失敗時返回-1

sigaction結構體詳解:

struct sigaction{
    void (* sa_handler) (int);//保存信號處理函數的地址
    sigset_t sa_mask;  //一個調用信號捕捉函數之前要加到進程信號屏蔽字中的信號集
    int sa_flags;//信號處理選項
}

該結構體後邊的兩個字段時用於指定信號相關的選項和特性,在此只需要全部填為0即可

使用sigaction()函數:

#include 
#include
#include
#include

/**超時處理函數**/
void time_out(int sig){
        if(sig == SIGALRM){
            puts("Time Out ");
        }
        alarm(2);
}



int main(int argc ,char *argv[]){
    int i = 0;
    //聲明一個sigaction變量
    struct sigaction act;
    /**初始化act變量中的值**/
    act.sa_handler = time_out;//初始化信號處理函數的地址
    act.sa_flags = 0;//初始化信號處理標志
    sigemptyset(&act.sa_mask);//用來將act.sa_mask信號集初始化並清空
    sigaction(SIGALRM,&act,0);//調用sigaction函數

    alarm(2);//設置時鐘

    for(;i<3;i++){
        puts("wait..........");
        sleep(100);
    }
return 0;
}

有了這些關於進程,僵屍進程的處理的知識我們就可以自己做一個多進程的服務端,這樣就可以同時為多個客戶端提供服務。

多進程服務端:

#include 
#include
#include
#include
#include
#include
#include
#include
#include

#define BUFF_SIZE 30

void error_handling(char *message);
void read_child_proc(int sig);

int main(int argc , char *argv[]){

    //服務端和客戶端socket
    int server_socket;
    int client_socket;
    //服務端和客戶端地址
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    //用於保存進程ID
    pid_t pid;
    //信號量結構體變量
    struct sigaction act;
    //用於保存socket地址長度
    socklen_t addr_size;
    //用於保存字符串長度
    int str_len;
    //用於記錄設置信號量的結果
    int state;
    //字符緩沖
    char buff[BUFF_SIZE];
    //用於控制程序的結束與否
    bool is_running = true;
    //檢查傳入的參數個數是否合法
    if(argc!=2){
        printf("Usage : %s  \n",argv[0]);
        exit(1);
    }

    //初始化信號量機制
    act.sa_handler = read_child_proc;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    state = sigaction(SIGCHLD,&act,0);

    //初始化socket
    server_socket = socket(PF_INET,SOCK_STREAM,0);
    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(atoi(argv[1]));

    //綁定地址
      if(bind(server_socket,(struct sockaddr *) &server_addr,sizeof(server_addr)) == -1){
        error_handling("bind() error");
      }
    //設置監聽
    if(listen(server_socket,5) == -1){
        error_handling("listen() error");
    }
    //通過循環來不斷的提供服務
    while(is_running){
        addr_size = sizeof(client_addr);
        client_socket = accept(server_socket,(struct sockaddr *) &client_addr,&addr_size);
        //進行檢查
        if(client_socket == -1){
            continue;
        }else{
            printf("new client connected...............\n");
        }
        //創建新的進程
        pid = fork();
        //檢查是否創建成功
        if(pid == -1){
            close(client_socket);
            continue;
        }
        //子進程運行部分,在這裡進行服務端和客戶端的交互
        if(pid == 0){
            close(server_socket);
            //不斷的向客戶端發送讀取到的數據,直到讀取完畢
            while((str_len = read(client_socket,buff,BUFF_SIZE)) != 0){
                write(client_socket,buff,str_len);
            }
            //發送完畢之後關閉客戶端的連接
            close(client_socket);
            puts("client disconnected.........");
            //子進程完成任務,返回
            return 0;
        }else{
            close(client_socket);
        }
    }
    //徹底關閉服務端,但是由於前邊的while循環是死循環,正常情況下執行不到
    close(server_socket);
return 0;
}
/**子進程處理函數**/
void read_child_proc(int sig){
    pid_t  pid;
    int status;
    //在信號量處理函數中調用waitpid()函數來讀取子進程的結束信息,徹底銷毀子進程,同時父進程也可以根據status中的信息來對子進程的處理結果進程進一步的處理
    pid = waitpid(-1,&status,WNOHANG);
    printf("remove proc id : %d \n",pid);
}
/**出錯處理函數**/
void error_handling(char * message){
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);
}

自此就完成了一個多進程服務端程序的建立,我們可以使用該服務端同時為多個客戶端提供服務。

Copyright © Linux教程網 All Rights Reserved