服务器按处理方式可以分为迭代服务器和并发服务器两类。平常用C写的简单Socket客户端服务器通信,服务器每次只能处理一个客户的请求,它实现简单但效率很低,通常这种服务器被称为迭代服务器。 然而在实际应用中,不可能让一个服务器长时间地为一个客户服务,而需要其具有同时处理 多个客户请求的能力,这种同时可以处理多个客户请求的服务器称为并发服务器,其效率很 高却实现复杂。在实际应用中,并发服务器应用的最广泛。
linux有3种实现并发服务器的方式:多进程并发服务器,多线程并发服务器,IO复用,先来看多进程并发服务器的实现。
在创建新进程时,要进行资源拷贝。Linux 有三种资源拷贝的方式:
下面介绍创建新进程的两个函数:fork()和 vfork()。
其中,fork 用于普通进程的创建,采用的是 Copy on Write 方式;而 vfork 使用完全共享的创建,新老进程共享同样的资源,完全没有拷贝。
● fork函数原型如下:
#include
pid_t fork (void);
函数调用失败会返回-1。fork 函数调用失败的原因主要有两个:
而如果调用成功,该函数调用会在父子进程中分别返回一次。在调用进程也就是父进程中,它的返回值是新派生的子进程的 ID 号,而在子进程中它的返回值为 0。因此可以通过返回值来区别当前进程是子进程还是父进程。
为什么在 fork 的子进程中返回的是 0,而不是父进程 id 呢?
原因在于:没有子进程都只 有一个父进程,它可以通过调用 getppid 函数来得到父进程的 ID,而对于父进程,它有很多 个子进程,他没有办法通过一个函数得到各子进程的ID。如果父进程想跟踪所有子进程的ID, 它必须记住 fork 的返回值。
● vfork函数原型如下:
#include
pid_t vfork (void);
vfork 是完全共享的创建,新老进程共享同样的资源,完全没有拷贝。当使用 vfork()创 建新进程时,父进程将被暂时阻塞,而子进程则可以借用父进程的地址空间运行。这个奇特 状态将持续直到子进程要么退出,要么调用 execve(),至此父进程才继续执行。
可以通过下面的程序来比较 fork 和 vfork 的不同。
#include
#include
int main(void)
{
pid_t pid;
int status;
if ((pid = vfork()) == 0)
{
sleep(2);
printf("child running.\n");
printf("child sleeping.\n");
sleep(5);
printf("child dead.\n");
exit(0);
}
else if ( pid > 0)
{
printf("parent running .\n");
printf("parent exit\n");
exit(0); 21.
}
else
{
printf("fork error.\n");
exit(0);
}
}
程序运行结果如下:
child running.
child sleeping.
child dead.
parent running .
parent exit
如果将 vfork 函数换成 fork 函数,该程序运行的结果如下:
parent running .
parent exit
[root@localhost test]#
child running.
child sleeping.
child dead.
fork 调用后,父进程和子进程继续执行 fork 函数后的指令,是父进程先执行还是子进程 先执行是不确定的,这取决于系统内核所使用的调度算法。
而在网络编程中,父进程中调用 fork 之前打开的所有套接字描述符在函数 fork 返回之后都是共享。如果父、子进程同时对同一个描述符进行操作, 而且没有任何形式的同步,那么它们的输出就会相互混合。
fork函数在并发服务器中的应用:
父、子进程各自执行不同的程序段,这是非常典型的网络服务器。父进程等待客户 的服务请求。当这种请求到达时,父进程调用 fork 函数,产生一个子进程,由子进程对该请求作处理。父进程则继续等待下一个客户的服务请求。并且这种情况下,在 fork 函数之后,父、子进程需要关闭各自不使用的描述符,即父进程将不需要的 已连接描述符关闭,而子进程关闭不需要的监听描述符。这么做的原因有3个:
我们在socket编程中调用 close 关闭已连接描述符时,其实只是将访问计数值减 1。而描述符只在访 问计数为 0 时才真正关闭。所以为了正确的关闭连接,当调用 fork 函数后父进程将不需要的 已连接描述符关闭,而子进程关闭不需要的监听描述符。
好了,有了上面的知识,我们现在可以总结出编写多进程并发服务器的基本思路:
服务器端代码示例(来源于网络):
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 1113
#define LISTENQ 32
#define MAXLINE 1024
/***连接处理函数***/
void str_echo(int fd);
int
main(int argc, char *argv[]){
int listenfd,connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
if((listenfd = socket(AF_INET, SOCK_STREAM,0))==-1){
fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
exit(1);
}
/* 服务器端填充 sockaddr结构*/
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl (INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
/* 绑定listenfd描述符 */
if(bind(listenfd,(struct sockaddr*)(&servaddr),sizeof(struct sockaddr))==-1){
fprintf(stderr,"Bind error:%s\n\a",strerror(errno));
exit(1);
}
/* 监听listenfd描述符*/
if(listen(listenfd,5)==-1){
fprintf(stderr,"Listen error:%s\n\a",strerror(errno));
exit(1);
}
for ( ; ; ) {
clilen = sizeof(cliaddr);
/* 服务器阻塞,直到客户程序建立连接 */
if((connfd=accept(listenfd,(struct sockaddr*)(&cliaddr),&clilen))==-1){
fprintf(stderr,"Accept error:%s\n\a",strerror(errno));
exit(1);
}
//有客户端建立了连接后
if ( (childpid = fork()) == 0) { /*子进程*/
close(listenfd); /* 关闭监听套接字*/
str_echo(connfd); /*处理该客户端的请求*/
exit (0);
}
close(connfd);/*父进程关闭连接套接字,继续等待其他连接的到来*/
}
}
void str_echo(int sockfd){
ssize_t n;
char buf[MAXLINE];
again:
while ( (n = read(sockfd, buf, MAXLINE)) > 0)
write(sockfd, buf, n);
if (n < 0 && errno == EINTR)//被中断,重入
goto again;
else if (n < 0){//出错
fprintf(stderr,"read error:%s\n\a",strerror(errno));
exit(1);
}
}
传统的网络服务器程序大都在新的连接到达时,fork一个子进程来处理。虽然这种模式很多年使用得很好,但fork有一些问题:
下一篇文章介绍基于多线程的并发服务器编程。
进程终止存在两种可能:父进程先于子进程终止;子进程先于父进程终止。
当子进程正常或异常终止时,系统内核向其父进程发送 SIGCHLD 信号,默认情况下, 父进程忽略该信号,或者提供一个该信号发生时即被调用的函数。
父进程可以通过调用 wait()或 waitpid()函数,获得子进程的终止信息。
#include
pid_t wait(int *statloc);
参数 statloc 返回子进程的终止状态(一个整数)。当调用该函数时,如果有一个子进程 已经终止,则该函数立即返回,并释放子进程所有资源,返回值是终止子进程的 ID 号。如果当前没有终止的子进程,但有正在执行的子进程,则 wait 将阻塞直到有子进程终止时才返 回。如果当前既没有终止的子进程,也没有正在执行的子进程,则返回错误-1。
函数 waitpid 对等待哪个进程终止及是否采用阻塞操作方式方面给了更多的控制。
#include
waitpid(pid_t pid ,int *statloc, int option);
当参数 pid 等于-1 而 option 等于 0 时,该函数等同于 wait()函数。 参数 pid 指定了父进程要求知道哪些子进程的状态,当 pid 取-1 时,要求知道任何一个子进程的终止状态。当 pid 取值大于 0 时,要求知道进程号为 pid 的子进程的终止状态。当 pid 取值小于-1 时,要求知道进程组号为 pid 的绝对值的子进程的终止状态。
参数 option 让用户指定附加选项。最常用的选项是 WNO_HANG,它通知内核在没有已 终止子进程时不要阻塞。
当前有终止的子进程时,返回值为子进程的 ID 号,同时参数 statloc 返回子进程的终止 状态。否则返回值为-1。
和wait较大的不同是waitpid可以循环调用,等待所有任意进程结束,而wait只有一次机会。