文章目录
至于什么是多进程,这里不展开叙述,可以去查阅相关技术资料,在本章中使用到的技术有:多线程编程以及信号处理,如果对这些技术不太熟悉可以自行查看相关资料。这里只介绍几个操作函数
一、相关操作函数
1. 创建进程函数
#include<unistd.h>
pid_t fork(void);
//他的返回值得从不同角度看,假如从父进程看,他的返回值为子进程ID;从子进程看,返回0;失败返回-1;
2. 防止出现僵尸进程的函数
#include<sys/wait.h>
pid_t wait(int *statloc);
/*调用成功返回终止的子进程ID,失败返回-1;
*/
如果父进程调用这个函数时已经有子进程终止,那么子进程终止时传递的返回值(包括exit()函数的参数以及return 返回值)将保存到该函数的参数所指向的内存空间。但是这个内存空间不仅仅有返回值参数,还应该包括其他信息,其他信息保存在各种宏当中,以下是两个比较典型的宏。
WIFEXITED:子进程正常终止时返回“真”(true)
WEXITSTATUS:返回子进程的返回值
//也就是说,向wait函数传递变量status地址时,调用wait函数后应该执行以下代码
if(WIFEXITED(status))
{
puts("Normal termination!");
printf("Child pass num:%d", WEXITSTATUS(status))
}
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int* statloc, int options);
//pid: 等待终止的目标子进程的ID,若传递-1,则该函数功能和wait函数一样等待任何子进程的终结。
//statloc:与wait函数的statloc参数具有一样的意义。
//options: 传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞的状态,而是直接返回0并退出函数。
3. 信号处理函数
进程: 你好,操作系统!,如果我之前创建的子进程终止的话,就帮我调用zombie_handle函数。
操作系统:好的!如果你的子进程终止,我会帮你调用zombie_handle函数,但是之前你得先把该函数要执行的语句编好!
以上对话形象体现出“信号注册”过程,被注册的函数称为信号注册函数
#include<signal.h>
void (*signal(int singo, void(*func)(int)))(int);//函数指针
//signal: 函数名
//int signo ,void(*func)(int): 信号注册函数,参数类型为int,返回void型函数指针
以上函数看着复杂其实我们只要记住,只要两个参数,前面一个参数用于标记特殊情况的种类,另外一个参数代表不同特殊情况对应的处理函数(即信号注册函数)。
下面是不同特殊情况对应的宏
SIGALRM: 已通过调用alarm函数注册的时间
SIGINT: 键盘输入了CTRL+C组合键
SIGCHLD: 子进程终止
#include<signal.h>
int sigaction(int signo,const struct sigaction *act, struct sigaction* oldact);
//成功返回0,失败返回-1
/*
signo :传递信号信息
act :对应于第一个参数的信号处理函数(信号处理器)信息
oldact: 通过此参数获取之前注册的信号处理函数指针,若不需要则传递0
*/
声明并初始化sigaction结构体变量以调用上述函数,该结构体定义如下:
struct sigaction
{
void(*sa_handler)(int); //保存信号处理函数的指针
sigset_t sa_mask; //信号选项参数一般设置成0
int sa_flags; //信号特性参数一般设置成0
}
signal函数与sigction对比,更加常用的是后者,因为更加稳定,而且后者能够完全替代前者,signal函数在UNIX系列的不同操作系统可能存在不同,但是sigaction函数完全相同。读者完全可以只考虑sigaction,不需要去考虑signal。
二、基于多任务的并发服务器
1. 基于进程的并发服务器模型
基于多进程的并发服务器模型肯定脱离不了fork()函数,因此我们可以采用该函数进行编写服务器端。该模型如下图所示:
每当客户端有请求连接服务(连接请求)时,回声服务器端都创建一个子进程进行处理并提供服务,为了完成这些任务,需要经过以下过程,这是与之前的回声服务器端的区别所在。
第一阶段:回声服务器端(父进程)通过调用accept函数受理连接请求。
第二阶段:此时获取的套接字文件描述符创建并传递给子进程。
第三阶段:子进程利用传递来的文件描述符提供服务。
2. 代码实现
#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 30
void error_handling(char *message);
void read_childproc(int sig);
int main(int argc, char const *argv[])
{
int serv_sock,clnt_sock;
struct sockaddr_in serv_adr,clnt_adr;
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);
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");
while(1)
{
adr_sz=sizeof(clnt_sock);
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==-1)
{
close(clnt_sock);
continue;
}
/*
*子进程,负责处理与客户端之间数据交流
*/
if(pid==0) //子进程运行区域
{
//这里关闭父进程创建的套接字,具体原因是因为子进程进行复制的时候也会复制套接字描述符,而子 进程不需要父进程创建的套接字,因此进行关闭。这在第四小节会详细阐述。
close(serv_sock);
while((str_len=read(clnt_sock,buf,BUF_SIZE))!=0)write(clnt_sock,buf,str_len);
//处理完毕,关闭套接字,方便之后的子进程继续与其他客户端进行交互
close(clnt_sock);
puts("client disconnected.....");
return 0;
}
//以下是父进程执行区域
else{
close(clnt_sock);
}
}
close(serv_sock);
return 0;
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid=waitpid(-1,&status,WNOHANG);
printf("remove pro id:%d\n",pid);
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
至于客户端可以选择之前随便一个客户端程序就可以进行对接,这里给出第四章的客户端程序
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc,char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if(argc!=3)
{
printf("Usage:%s<IP><port> \n",argv[0]);
exit(1);
}
sock=socket(PF_INET,SOCK_STREAM,0);
if(sock==-1)error_handling("socket() error");
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
error_handling("connect() error");
else{
puts("Connected.....");
}
while(1)
{
fputs("Input message(Q to quit):",stdout);
fgets(message,BUF_SIZE,stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
{
printf("testing......\n");
break;
}
write(sock,message,strlen(message));
str_len=read(sock,message,BUF_SIZE-1);
message[str_len]=0;
printf("Message from server:%s",message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
3. 通过fork函数复制文件描述符
记得上面服务端程序中子进程关闭父进程创建的套接字描述符,此时我们需要理解一个概念:套接字不等于套接字文件描述符。在Linux中源文件只有一份但是可以拥有多个软链接,当多个软链接被删除之后,源文件也会被自动覆盖消失。同样的,一个套接字可以拥有至少一个以上的套接字文件描述符。只有将所有套接字文件描述符撤销之后,套接字才能正常关闭。
在父进程当中我们之前讲过服务端与客户单进行交互需要创建两个套接字,一个由程序员自己创建,一个是由函数自动帮我们创建,此时,父进程拥有两个套接字的套接字文件描述符,因此父进程在创建在子进程时候给子进程继承了两个套接字描述符。如若子进程不关闭不使用的套接字文件描述符,退出的时候可能就无法关闭套接字。因此在创建子进程的时候需要关闭无关的套接字文件描述符
三、基于多进程的客户端读写分离
我们注意到,在上述的基于多进程的服务器端回声测试当中,客户端采用write()、read()函数实现与服务端的数据交换,这两个读写函数属于阻塞型函数,也就是假如缓冲区如果没有数据可读,那么调用read()函数无法读取数据而进入阻塞状态,而假如此时还有大量数据没有发送给服务端,因此会造成严重的网络延迟。为了解决这个问题,我们是否能够也能用多进程来使得读写端实现分离,父子进程负责读或者写。
1. 代码实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
//接收数据
void read_routine(int sock,char *buf);
//发送数据
void write_routine(int sock,char* buf);
int main(int argc, char const *argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if(argc!=3)
{
printf("Usage : %s <IP> <port> \n",argv[0]);
exit(1);
}
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=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)error_handling("connect error");
pid=fork();
//子进程执行区域
if(pid==0)
{
write_routine(sock,buf);
//思考以下这里为什么不调用close函数关闭sock ?
//close(sock)
}
//父进程执行区域
else{
read_routine(sock,buf);
}
close(sock);
return 0;
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
void read_routine(int sock,char* buf)
{
while(1)
{
int str_len=read(sock,buf,BUF_SIZE);
if(str_len==0)
{
return ;
}
buf[str_len]=0;
printf("Message from server :%s\n",buf);
}
}
void write_routine(int sock,char* buf)
{
while(1)
{
fgets(buf,BUFSIZ,stdin);
if((!strcmp(buf,"q\n"))||(!strcmp(buf,"Q\n")))
{
shutdown(sock,SHUT_WR);
return ;
}
write(sock,buf,strlen(buf));
}
}
注意到,write_routine()函数当中调用shutdown 函数向服务器传递EOF。当然在第46行调用的close(sock)可以向服务端发送EOF(即停止数据传输),但是子进程也复制了一份套接字描述符(即服务端父进程、服务端与子进程之间都有连接),因此一次调用close(sock)不够,还需要在子进程里面调用shutdown函数来优雅地断开与服务端的连接,因此需要在子进程里面向服务器端传递EOF ()。shutdown函数的使用我们在如何优雅断开TCP连接里面有详细阐述。