在寫多進程編程之前,我們先了解一下fork系統調用。
fork系統調用
①Linux內核在啓動的最後階段會創建init進程來執行序/sbin/init,該進程是系統運行的第一個進程,進程號爲 1,稱爲Linux 系統的初始化進程,該進程會創建其他子進程來啓動不同寫系統服務,而每個服務又可能創建不同的子進程來執行不同的程序。Linux 中維護着一個數據結構叫做 進程表,保存當前加載在內存中的所有進程的有關信息,其中包括進程的 PID(Process ID)、進程的狀態、命令字符串等,操作系統通過進程的 PID 對它們進行管理,這些 PID 是進程表的索引。
②Linux下有兩個基本的系統調用可以用於創建子進程:fork()和vfork()。在我們編程的過程中,一個函數調用只有一次返回(return),但由於fork()系統調用會創建一個新的進程,這時會有兩次返回。一次返回是給父進程,其返回值是子進程的PID(Process ID),第二次返回是給子進程,其返回值爲0。所以我們在調用fork()後,需要通過其返回值來判斷當前的代碼是在父進程還是子進程運行,如果返回值是0說明現在是子進程在運行,如果返回值>0說明是父進程在運行,而如果返回值<0的話,說明fork()系統調用出錯。fork 函數調用失敗的原因主要有兩個:
- 系統中已經有太多的進 程;
- 該實際用戶 ID 的進程總數超過了系統限制
③每個子進程只有一個父進程,並且每個進程都可以通過getpid()獲取自己的進程PID,也可以通過getppid()獲取父進程的PID,這樣在fork()時返回0給子進程是可取的。一個進程可以創建多個子進程,這樣對於父進程而言,他並沒有一個API函數可以獲取其子進程的進程ID,所以父進程在通過fork()創建子進程的時候,必須通過返回值的形式告訴父進程其創建的子進程PID。這也是fork()系統調用兩次返回值設計的原因。
④fork()系統調用會創建一個新的子進程,這個子進程是父進程的一個副本。這也意味着,系統在創建新的子進程成功後,會將父進程的文本段、數據段、堆棧都複製一份給子進程,但子進程有自己獨立的空間,子進程對這些內存的修改並不會影響父進程空間的相應內存。這時系統中出現兩個基本完全相同的進程(父、子進程),這兩個進程執行沒有固定的先後順序,哪個進程先執行要看系統的進程調度策略。如果需要確保讓父進程或子進程先執行,則需要程序員在代碼中通過進程間通信的機制來自己實現。我們可以看一下下面的圖
網絡socket多進程編程之服務器代碼示例
在Linux下多進程編程之後,我們就可以使用多進程編程寫socket服務器了其流程圖和代碼如下:
/*********************************************************************************
* Copyright: (C) 2020 makun<[email protected]>
* All rights reserved.
*
* Filename: djc_socket_server.c
* Description: This file fork_socket_server
*
* Version: 1.0.0(2020年02月28日)
* Author: makun <[email protected]>
* ChangeLog: 1, Release initial version on "2020年02月28日 13時59分51秒"
*
********************************************************************************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <getopt.h>
#include <stdlib.h>
#define MSG_STR "Hello lingyun iot stduio client\n"
void print_usage(char *progname)
{
printf("%s usage: \n", progname);
printf("-p(--port): sepcify server listen port.\n");
printf("-h(--Help): print this help information.\n");
return ;
}
int main (int argc, char **argv)
{
int sockfd = -1;
int clifd;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t len;
int port = 0;
int ch;
int rv = -1;
int on = 1;
pid_t pid;
struct option opts[] = {
{"port", required_argument, NULL, 'p'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
while( (ch=getopt_long(argc, argv, "p:h", opts, NULL)) != -1 )
{
switch(ch)
{
case 'p':
port=atoi(optarg);
break;
case 'h':
print_usage(argv[0]);
return 0;
}
}
if( !port )
{
print_usage(argv[0]);
return 0;
}
sockfd=socket(AF_INET,SOCK_STREAM ,0);
if( sockfd < 0)
{
printf("create socket failure:%s\n",strerror(errno));
return -1;
}
printf("create socket successfuly: %d\n", sockfd);
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
rv = bind(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
if(rv < 0)
{
printf("socket[%d] bind to port[%d] failure:%s\n",sockfd,port,strerror(errno));
return -2;
}
listen(sockfd,13);
printf("start to listen on port [%d]\n", port);
while(1)
{
printf("start accept new client incoming..\n");
clifd=accept(sockfd,(struct sockaddr *)&cliaddr,&len);
if(clifd < 0)
{
printf("accept client failure:%s\n", strerror(errno));
continue;
}
printf("accept new clienr[%s:%d] successfully\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
pid=fork();
if( pid < 0 )
{
printf("fork creat child pricess failure:%s\n",strerror(errno));
continue;
}
else if( pid > 0)
{
close(clifd);
continue;
}
else if( pid ==0 )
{
char buf[1024];
close(sockfd);
printf("Chid process start to commuicate with socket client..\n");
memset(buf , 0, sizeof(buf));
rv=read(clifd,buf,sizeof(buf));
if(rv < 0)
{
printf("Read data from client sockfd[%d] failure:%s\n",clifd,strerror(errno));
close(clifd);
exit(0);
}
else if( rv == 0)
{
printf("socket[%d]get disconnected\n", clifd);
close(clifd);
exit(0);
}
else if( rv > 0)
{
printf("read %d bytss data from server:%s\n",rv,buf);
}
rv=write(clifd, MSG_STR, strlen(MSG_STR));
if(rv < 0)
{
printf("Write to client by sockfd[%d] failure: %s\n", sockfd,
strerror(errno));
close(clifd);
exit(0);
}
sleep(1);
printf("close client socket[%d] and child process exit\n", clifd);
close(clifd);
exit(0);
}
}
close(sockfd);
return 0;
}
linux下編程結果如下:
結果分析:在該程序中,父進程accept()接收到新的連接後,就調用fork()系統調用來創建子進程來處理與客戶端的通信。因爲子進程會繼承父進程處於listen狀態的socket 文件描述符(sockfd),也會繼承父進程accept()返回的客戶端socket 文件描述符(clifd),但子進程只處理與客戶端的通信,這時他會將父進程的listen的文件描述符sockfd關閉;同樣父進程只處理監聽的事件,所以會將clifd關閉。此時父子進程同時運行完成不同的任務,子進程只負責跟已經建立的客戶端通信,而父進程只用來監聽到來的socket客戶端連接。所以當有新的客戶端到來時,父進程就有機會來處理新的客戶連接請求了,同時每來一個客戶端都會創建一個子進程爲其服務。