c++ 網絡編程(三) LINUX/windows 進程間的通訊原理與實現代碼 基於多進程的服務端實現

原文做者:aircrafthtml

原文連接:https://www.cnblogs.com/DOMLX/p/9613027.html前端

 

 鍥子:進程與線程是什麼,他們的區別在哪裏:python

1 進程概念

進程是程序的一次動態執行過程。程序自己只是指令的集合,進程纔是程序的真正運行,用戶運行程序就產生了進程,用戶關閉程序就結束了進程。一個程序能夠產生多個進程,但一個進程只能由一個程序產生。進程有5種生命週期狀態:建立、就緒、運行、阻塞、退出,下圖表示了5種狀態之間的關係。
進程的5種狀態
與上面的5種狀態不一樣,在Linux中使用top命令查看進程也有5種狀態,但它是以不一樣的角度來描述進程狀態:
R:運行狀態,正在運行或在運行隊列中等待
S:可中斷(自願)的睡眠狀態,在等待某事件發生纔可喚醒。如網頁服務器的httpd進程,在客戶端(瀏覽器)發出請求以前,服務器無事可作,選擇睡眠。是程序角度的睡眠,程序自己控制本身的運行狀態
D:不可中斷(非自願)的睡眠狀態,有時兩個進程試圖同時訪問同一系統資源,例如一個進程試圖從磁盤數據塊上讀取信息,而另外一個進程正在向該數據塊寫入信息。在這種狀況下,內核迫使進程進入非自願睡眠狀態,該進程並無自願選擇睡眠。當資源被釋放時,內核會喚醒進程並將其設置爲可運行狀態。雖然進程斷的進入和離開非自願睡眠,可是它們一般不會在該狀態停留過久。所以,除了在高負荷的系統上,用戶一般看不處處於非自願睡眠狀態的進程。
T:中止或跟蹤狀態,進程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信號後中止運行
Z:僵死狀態,子進程先於父進程退出,同時父進程又沒有調用wait()或waitpid(),則該子進程將成爲殭屍進程。爲了防止產生殭屍進程,在fork子進程以後咱們都要在父進程wait它們ios

2 線程概念

早期的計算機系統只有進程,沒有線程,後來發現進程切換的開銷有點大,因而產生了線程(也稱輕量級進程),線程切換比進程切換的開銷要小不少。線程是程序執行中一個單一的順序控制流,是程序執行流的最小單元,是處理器調度和分派的基本單位,是進程中代碼的不一樣執行路線,一個進程能夠有多個線程。
如今的處理器通常是雙核或四核的,每一個處理核心對應一個內核線程,可是咱們常常看到雙核心四線程或四核心八線程的描述,這實際上是採用了超線程技術將一個物理處理核心模擬成兩個邏輯處理核心,對應兩個內核線程,因此採用了超線程技術的CPU在操做系統中看到的CPU數量是實際物理CPU數量的兩倍。這裏講到了內核線程,內核線程(Kernel Thread, KLT)就是直接由操做系統內核支持的線程,這種線程由內核來完成線程切換,內核經過操做調度器對線程進行調度,並負責將線程的任務映射到各個處理器上。而用戶程序通常是建立用戶線程,而後用戶線程調用內核線程,在如今流行的操做系統中,用戶線程和內核線程是多對多的映射調用關係。c++

3 進程、線程的區別與聯繫

一個程序至少產生一個進程,一個進程至少產生一個線程,線程再映射到內核線程,在處理器上完成計算任務。
進程與線程的區別,關鍵就在於它們獨立擁有的資源不一樣,所以就體現出了不一樣的特性,主要有如下幾方面的不一樣:
(1)資源
進程做爲除CPU之外系統資源的分配單位,每一個進程都擁有獨立的地址空間和資源,所以一個進程崩潰不會引發其餘進程的運行,就好像是不一樣程序建立的不一樣進程互不影響同樣;
線程做爲CPU的分配單位,是被系統獨立調度和分派的基本單位,自己僅擁有CPU中的寄存器和線程函數調用須要的堆棧區這一點點資源,其餘資源如代碼、公有數據、文件描述符等都是與其餘線程共享的進程資源,因此一個線程崩潰,整個進程都崩潰。
(2)系統開銷
因爲每一個進程都擁有獨立的地址空間和資源,所以進程的建立和撤銷都須要資源的分配和回收,系統開銷大;
每一個線程僅僅擁有一點必不可少的資源,因此建立和撤銷都很快。
(3)通訊方式
進程之間主要有6種通訊方式:管道、信號、消息隊列、共享內存、信號量、socket;
線程之間主要有3種通訊方式:鎖機制(包括互斥鎖、條件變量、讀寫鎖)、信號量機制、信號機制。
線程間的通訊目的主要是用於線程同步,因此線程沒有像進程通訊中的用於數據交換的通訊機制。
通訊方式的內容寫得不夠詳細,後續再補充,下面這些是暫時記錄的內容:編程

信號量:一個初始值爲N的信號量容許N個線程併發訪問。線程訪問資源時首先獲取信號量鎖,進行以下操做:
1. 將信號量的值減1;
2. 若是信號量的值小於0,則進入等待狀態,不然繼續執行;
訪問資源結束以後,線程釋放信號量鎖,進行以下操做:
1. 將信號量的值加1;
2. 若是信號量的值小於1(等於0),喚醒一個等待中的線程;
同步:多個事件一塊兒開始,一塊兒結束(一刀切);
異步:多個事件各走各的,完成了通知一下控制中心;
阻塞:所申請資源被佔用、啓動IO傳輸未完成,必須等待資源或事件完成才能夠進行下一步稱爲阻塞;
掛起:掛起是主動的,通常須要用掛起函數進行操做,若沒有resume的動做,則此任務一直不會ready,而阻塞是由於資源被其餘任務搶佔而處於休眠態。windows

 

 

 


 

 

 

 

一.進程間通訊的基本概念

進程間通訊意味着兩個不一樣進程間能夠交換數據,操做系統中應提供兩個進程能夠同時訪問的內存空間。後端

 

多進程之間通訊方式:api

 

          文件映射:本地之間數組

 

          共享內存:本地之間

 

          匿名管道:本地之間

 

          命名管道:跨服務器

 

          郵件槽:一對多的傳輸數據,一般經過網絡向一臺Windows機器傳輸

 

          剪切板:本地之間

 

          socket:跨服務器

 

多線程之間通訊方式:

 

          全局變量

 

          自定義消息響應

 

多線程之間同步機制:

 

          臨界區:不能夠跨進程,忘記解鎖會無限等待,要麼存在要麼沒有,多線程訪問獨佔性共享資源

 

          互斥量:能夠跨進程,忘記解鎖會自動釋放,要麼存在要麼沒有

 

          事件:又叫線程觸發器,不能夠跨進程,要麼存在要麼沒有,一個線程來喚醒另外一個線程(包括自動和人工兩種方式)

 

          信號量:能夠跨進程,始終表明可用資源數量,當資源數爲o時,線程阻塞,容許多個線程同時訪問一個共享資源

 

經過管道實現進程間通訊

基於管道(PIPE)的進程間通訊結構模型:

                                                

經過管道完成進程間通訊。管道不是進程的資源,屬於操做系統的。兩個進程經過操做系統提供的內存空間進行通訊。

建立管道的函數:

 

父進程調用該函數時建立管道,同時獲取對應於出入口的文件描述符。父進程的目的是與子進程進行數據交換,所以須要將入口或出口中的1個文件描述符傳遞給子進程。調用fork函數傳遞。

 

二.進程間通訊的單向傳遞

 

簡單的看一個基礎單向通訊實例代碼來理解進程間的通訊是怎麼實現的:

#include<stdio.h>
#include<unistd.h>
#define BUF_SIZE 30
 
int main(int argc, char *argv[])
{
    int fds[2];
    char str[] = "Who are you?";
    char buf[BUF_SIZE];
    pid_t pid;
 
    pipe(fds);                //建立管道,fds數組中保存用於I/O的文件描述符
    pid = fork();            //子進程將同時擁有管道的I/O文件描述符。
    if (pid == 0)
    {
        write(fds[1],str,sizeof(str));        //fds[1]爲管道入口
    }
    else 
    {
        read(fds[0],buf,BUF_SIZE);            //fds[0]爲管道出口    
        puts(buf);
    }
    return 0;
}

 

運行結果:who are you ?

 

上例中,父子進程均可以訪問管道的I/O路徑,但子進程僅用輸入路徑,父進程僅用輸出路徑。

                                        

 

三.進程間通訊的雙向傳遞

 

管道進行雙向數據交換的通訊方式:

                                        

 

接下來看一個簡單的實例代碼:

/* 雙向通訊的管道 */
#include<stdio.h>
#include<unistd.h>
#define BUF_SIZE 30
 
int main(int argc,char *argv[])
{
    int fds[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for your message!";
    char buf[BUF_SIZE];
    pid_t pid;
 
    pipe(fds);
    pid = fork();
    if (pid == 0)
    {
        write(fds[1],str1,sizeof(str1));        //傳輸數據
        sleep(2);                               //睡眠兩秒,避免被下一行的read函數讀取了數據。
        read(fds[0],buf,BUF_SIZE);
        printf("Child proc output: %s \n",buf);    //接收數據
    }
    else 
    {
        read(fds[0],buf,BUF_SIZE);                //接收數據
        printf("Parent proc output: %s \n",buf);
        write(fds[1],str2,sizeof(str2));        //傳輸數據
        sleep(3);        //睡眠,防止父進程在子進程輸出以前結束,可刪除
            //不理解的話你註釋掉這個sleep體會一下就知道了
    }
    return 0;
}

 

 

 

這裏爲何有這麼多個sleep 呢,搖一搖大家的小腦殼----有沒有聽見水聲???hhh

書上有句話「向管道傳遞數據的時候,先讀的程序會把數據先取走」

看到這裏明白了嗎???  簡而言之就是數據進入管道就變成了無主數據,這時候若是我子進程先寫入數據,在父進程沒有取出數據前又read把本身的數據給讀出來了!!!!大問題!大問題!對吧,這是要搞事情的節奏啊,被誰打死都不知道!!!

 

那麼如何避免這個問題呢?---一個管道不夠,我建兩個唄---唉,真是的。。。。。

 

只用1個管道進行雙向通訊並不是易事,須要預測並控制運行流程。所以建立2個管道完成雙向通訊,各自負責不一樣的數據流動便可:

                                            

由上圖可知,是用2個管道能夠避免程序流程的預測或控制。

接下來看雙通道實現通訊代碼:

/* 雙管道實現進程間通訊 */
#include<stdio.h>
#include<unistd.h>
#define BUF_SIZE 30
 
int main(int argc,char *argv[])
{
    int fds1[2],fds2[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for your message!";
    char buf[BUF_SIZE];
    pid_t pid;
 
    pipe(fds1), pipe(fds2);                        //建立兩個管道
    pid = fork();
    if (pid == 0)
    {
        write(fds1[1],str1,sizeof(str1));    //子進程經過數組fds1傳輸數據
        read(fds2[0],buf,BUF_SIZE);
        printf("Child proc output: %s \n",buf);
    }
    else 
    {
        read(fds1[0],buf,BUF_SIZE);
        printf("Parent proc output: %s \n",buf);    
        write(fds2[1],str2,sizeof(str2));    //父進程經過數組fds2傳輸數據
        sleep(3);
    }
    return 0;
}

輸入結果:Parent proc output: 」Who are you?"

Child proc output: "Thank you for your message!";

 

好的基本概念都介紹完了,那咱們用一下玩玩唄???

 

 

四.基於多進程的回聲服務端實現

 

注意啦這裏是對我上一章博客代碼的擴充,沒有看個人上一張網絡編程(二)......能夠去看看了

 

這裏對網絡編程(二)加了一個功能,「能夠將回聲客戶端傳輸的字符串按序保存到文件中去」

 

 LINUX 下:

/* 實現併發服務器端 */
/* echo_storeserv.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<sys/socket.h>
 
#define BUF_SIZE 100
void error_handling(char *message)
{
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);
}
 
/* Handler */
void read_childproc(int sig)
{
    pid_t pid;
    int status;
    pid = waitpid(-1,&status,WNOHANG);
    printf("removed proc id: %d \n",pid);
}
 
int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    int fds[2];
 
    pid_t pid;
    struct sigaction act;
    socklen_t adr_sz;
    int str_len, state;
    char buf[BUF_SIZE];
    if (argc != 2) {
        printf("Usage: %s <port> \n",argv[0]);
        exit(1);
    }
 
    act.sa_handler = read_childproc;            //設置信號處理函數
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD,&act,0);            //子進程終止時調用Handler
    
    serv_sock = socket(PF_INET,SOCK_STREAM,0);
    memset(&serv_adr,0,sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
 
    if (bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if (listen(serv_sock,5) == -1)
        error_handling("listen() error");
 
    pipe(fds);
  //這裏建立一個子進程來服務寫入文件數據 pid
= fork(); if (pid == 0) { FILE* fp = fopen("echomsg.txt","wt"); char msgbuf[BUF_SIZE]; int i, len; for (i = 0; i < 10; i++ ) { len = read(fds[0],msgbuf,BUF_SIZE); //從管道出口fds[0]讀取數據並保存到文件中 fwrite((void*)msgbuf,1,len,fp); } fclose(fp); return 0; } while (1) { adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz); if (clnt_sock == -1) continue; else puts("new client connected..."); //這裏建立一個子進程來將數據寫入管道 pid = fork(); if (pid == 0) //子進程運行區域 { close(serv_sock); while((str_len = read(clnt_sock,buf,BUF_SIZE)) != 0) { write(clnt_sock,buf,str_len); write(fds[1],buf,str_len); //將從客戶端接收到的數據寫入到管道入口fds[1]中 } close(clnt_sock); puts("client disconnected..."); return 0; //調用Handler } else //父進程運行區域 close(clnt_sock); } close(serv_sock); return 0; }

上面處理文件的進程代碼裏,可能有的人會對那個for循環怎麼實現剛好讀十次數據結束,,,有點疑惑------關鍵在於read函數,這個函數若是沒有從管道里面讀取到數據就會繼續等待!!!  這也是大工程須要注意出現BUG的地方

這裏須要你們多開幾個客戶端來驗證服務端的效果,當10次fwrite函數調用完後,你們就能夠打開文件查看結果了,若是沒有客戶端代碼能夠參考我上一篇博客。

 

windows下基於多進程的回聲服務端實現代碼:

 

/*
 *  @file  : TestEchoServerMultiProcess.cpp
 *  @author: Shilyx
 *  @date  : 2014-04-23 08:43:27.206
 *  @note  : Generated by SlxTemplates, 多進程echo服務器演示
 */
 
#include <WinSock2.h>
#include <Windows.h>
#include <Shlwapi.h>
#pragma warning(disable: 4786)
#include <iostream>
 
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "Shlwapi.lib")
 
using namespace std;
 
// 初始化WinSock,未檢查返回值
void InitWinSock()
{
    WSADATA wd;
 
    WSAStartup(MAKEWORD(2, 2), &wd);
}
 
void Serve(USHORT port)
{
    InitWinSock();
 
    SOCKET sock_base = INVALID_SOCKET;
 
    do
    {
        sock_base = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 
        if (sock_base == INVALID_SOCKET)
        {
            cerr<<"socket error "<<WSAGetLastError()<<endl;
            break;
        }
 
        sockaddr_in sin;
 
        sin.sin_family = AF_INET;
        sin.sin_addr.s_addr = INADDR_ANY;
        sin.sin_port = htons(port);
 
        if (SOCKET_ERROR == bind(sock_base, (sockaddr *)&sin, sizeof(sin)))
        {
            cerr<<"bind error "<<WSAGetLastError()<<endl;
            break;
        }
 
        if (SOCKET_ERROR == listen(sock_base, 100))
        {
            cerr<<"listen error "<<WSAGetLastError()<<endl;
            break;
        }
 
        HANDLE hProcess = NULL;
        DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &hProcess, 0, TRUE, DUPLICATE_SAME_ACCESS);
 
        if (NULL == hProcess)
        {
            cerr<<"DuplicateHandle error "<<GetLastError()<<endl;
            break;
        }
 
        TCHAR szSelfPath[MAX_PATH];
 
        GetModuleFileName(GetModuleHandle(NULL), szSelfPath, RTL_NUMBER_OF(szSelfPath));
        PathQuoteSpaces(szSelfPath);
 
        while (true)
        {
            int len = sizeof(sin);
            SOCKET sock = accept(sock_base, (sockaddr *)&sin, &len);
 
            if (sock == INVALID_SOCKET)
            {
                cerr<<"accept error "<<WSAGetLastError()<<endl;
                break;
            }
            else
            {
                TCHAR szCommand[MAX_PATH * 2];
                STARTUPINFO si = {sizeof(si)};
                PROCESS_INFORMATION pi;
 
                si.dwFlags = STARTF_USESHOWWINDOW;
                si.wShowWindow = SW_SHOW;
 
                wnsprintf(szCommand, RTL_NUMBER_OF(szCommand), TEXT("%s %u %u"), szSelfPath, sock, hProcess);
 
                if (CreateProcess(NULL, szCommand, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
                {
                    CloseHandle(pi.hProcess);
                    CloseHandle(pi.hThread);
                }
                else
                {
                    cerr<<"CreateProcess error "<<GetLastError()<<endl;
                }
 
                closesocket(sock);
            }
        }
 
    } while (false);
 
    if (sock_base != INVALID_SOCKET)
    {
        closesocket(sock_base);
    }
}
 
DWORD CALLBACK WorkProc(LPVOID lpParam)
{
    SOCKET sock = (SOCKET)lpParam;
 
    while (TRUE)
    {
        char szBuffer[4096];
        int len = recv(sock, szBuffer, sizeof(szBuffer), 0);
 
        if (len <= 0)
        {
            break;
        }
 
        if (send(sock, szBuffer, len, 0) <= 0)
        {
            break;
        }
    }
 
    closesocket(sock);
 
    return 0;
}
 
void Work(SOCKET sock, HANDLE hParentProcess)
{
    InitWinSock();
 
    HANDLE hObjects[] = {hParentProcess, CreateThread(NULL, 0, WorkProc, (LPVOID)sock, 0, NULL)};
 
    WaitForMultipleObjects(RTL_NUMBER_OF(hObjects), hObjects, FALSE, INFINITE);
 
    CloseHandle(hObjects[0]);
    CloseHandle(hObjects[1]);
}
 
int main(int argc, char *argv[])
{
    // 加端口參數啓動爲父進程
    // 加套接字句柄參數和進程句柄參數爲子進程
    // 不加參數顯示用法
 
    if (argc == 2)
    {
        int port = StrToIntA(argv[1]);
 
        if (port < 0 || port > 65535)
        {
            cerr<<"端口錯誤:"<<port<<endl;
            return 0;
        }
 
        // 在端口port處啓動echo服務器
        Serve((USHORT)port);
    }
    else if (argc == 3)
    {
        SOCKET sock = StrToIntA(argv[1]);
        HANDLE hParentProcess = (HANDLE)StrToIntA(argv[2]);
 
        // 針對具體tcp鏈接套接字和父進程句柄開始echo工做
        Work(sock, hParentProcess);
    }
    else
    {
        cout<<"加端口參數啓動爲父進程"<<endl
            <<"加套接字句柄參數和進程句柄參數爲子進程"<<endl
            <<"不加參數顯示用法"<<endl;
    }
 
    return 0;
}

 

 

 

 

同時多進程服務端也是有缺點的,每建立一個進程就表明大量的運算與內存空間佔用,相互進程數據交換也很麻煩。。。那麼怎麼解決呢,在我後面的博客也許會給出答案-----hhhhhhh

 

 最後說一句啦。本網絡編程入門系列博客是連載學習的,有興趣的能夠看我博客其餘篇。。。。

 

好了今天對網絡編程的學習就到這裏結束了,小飛機我要撤了去吃飯了。,,,不少人大學都很迷茫不知道學點什麼好,,,,,管他的,想那麼多幹嗎,先學了再說,對技術若有偏見,那麼你的領域就侷限於此了---《一專多精》

 

 

 

參考博客:https://blog.csdn.net/du_qi/article/details/52372914

參考博客:https://blog.csdn.net/my3439955/article/details/9749869

參考書籍:《TCP/IP 網絡編程 --尹聖雨》

 

如有興趣交流分享技術,可關注本人公衆號,裏面會不按期的分享各類編程教程,和共享源碼,諸如研究分享關於c/c++,python,前端,後端,opencv,halcon,opengl,機器學習深度學習之類有關於基礎編程,圖像處理和機器視覺開發的知識

相關文章
相關標籤/搜索