并发服务器

  本文摘自《UNIX网络编程 卷1》。

fork和exec函数

  fork函数是Unix/Linux中派生新进程的唯一方法。其定义如下:

#include <unistd.h>
pid_t fork(void);
// 返回:若成功则在子进程中返回0,在父进程中返回子进程ID,若出错则返回-1

  fork函数调用一次,返回两次。它在调用进程(称为父进程)中返回一次,返回值是新派生进程(称为子进程)的进程ID号;在子进程中返回一次,返回值为0。因此,返回值本身告知当前进程是子进程还是父进程。

  fork在子进程返回0而不是父进程的进程ID的原因在于:任何子进程只有一个父进程,而且子进程总是可以通过调用getppid取得父进程的进程ID。相反,父进程可以有很多子进程,而且无法获取各个子进程的进程ID。如果父进程想要跟踪所有子进程的进程ID,那么它必须记录每次调用fork的返回值。

  父进程中调用fork之前打开的所有描述符在fork返回之后由子进程分享。我们将看到网络服务器利用了这个特性:父进程调用accept之后调用fork。所接受的已连接套接字随后就在父进程与子进程共享。通常情况下,子进程接着读写这个已连接套接字,父进程则关闭这个已连接套接字。

  fork有两个典型用法:

  1)一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。这是网络服务器的典型用法。

  2)一个进程想要执行另一个程序。既然创建新进程的唯一办法是调用fork,该进程于是首先调用fork创建一个自身的副本,然后其中一个副本(通常为子进程)调用exec把自身替换成新的程序。这是诸如shell之类程序的典型用法。

  存放在硬盘上的可执行程序文件能够被Unix执行的唯一方法是:由一个现有进程调用六个exec函数中的某一个。(这六个函数中哪一个被调用并不重要,我们往往把它们统称为exec函数。)exec把当前进程映像替换成新的程序文件,而且该新进程通常从main函数开始执行。进程ID并不改变。我们称调用exec的进程为调用进程(calling process),称新执行的程序为新程序(new program)。

  这六个exec函数之间的区别在于:a)待执行的程序文件是由文件名(filename)还是由路径名(pathname)指定;b)新程序的参数是一一列出还是由一个指针数组来引用;c)把调用进程的环境传递给新程序还是给新程序指定新的环境。

并发服务器

  我们之前(第4章前,如图4-11)接触的服务器是一个迭代服务器(iterative server)。对于像时间获取这样的简单服务器来说,这就够了。然而当服务一个客户请求可能花费较长时间时,我们并不希望整个服务器被单个客户长期占用,而是希望同时服务多个客户。Unix/Linux中编写并发服务器程序最简单的办法就是fork一个子进程来服务每个客户。下边程序给出了一个典型的并发服务器程序的轮廓:

 1 pid_t pid;
 2 int listenfd, connfd;
 3 listenfd = Socket(...);
 4     /* fill in sockaddr_in{} with server‘s well-known port */
 5 Bind(listenfd, ...);
 6 Listen(listenfd, LISTENQ);
 7 for( ; ; )
 8 {
 9     connfd = Accept(listenfd, ...);        /* probably blocks */
10     if((pid = Fork()) == 0)
11     {
12         Close(listenfd);                   /* child closes listening socket */
13         doit(connfd);                      /* process the request */
14         Close(connfd);                     /* done with this client */
15         exit(0);                           /* child terminates */
16     }
17     Close(connfd);                         /* parent closes connected socked */
18 }

  当一个连接建立时,accept返回,服务器接着调用fork,然后由子进程服务客户(通过已连接套接字connfd),父进程则等待另一个连接(通过监听套接字listenfd)。既然新的客户由子进程提供服务,父进程就关闭已连接套接字。

  在上边程序中,我们假设由函数doit执行服务客户所需的所有操作。当该函数返回时,我们在该子进程显式地关闭已连接套接字。这一点并非必需,因为下一个语句就是调用exit,而进程终止处理的部分工作就是关闭所有由内核打开的描述符。是否显式调用close只和个人编程风格有关。

  对一个TCP套接字调用close会导致发送一个FIN,随后是正常的TCP连接终止序列。为什么上边程序中父进程对connfd调用close没有终止它与客户的连接呢?为了便于理解,我们必须知道每个文件或套接字都有一个引用计数。引用计数在文件表项中维护(APUE第58-59页),它是当前打开着的引用该文件或套接字的描述符的个数。在上边程序中,socket返回后与listenfd关联的文件表项的引用计数值为1。accept返回后与connfd关联的文件表项的引用计数值也为1。然而fork返回后,这两个描述符就在父进程与子进程间共享(也就是被复制),因此与这两个套接字相关联的文件表项各自的访问计数值均为2。这么一来,当父进程关闭connfd时,它只是把相应的引用计数值从2减为1。这套接字真正的清理和资源释放要等到其引用计数为0时才发生。这也会在子进程关闭connfd时发生。

  我们还可以用图示直观的表现出来。

  首先,图4-14给出了在服务器阻塞于accept调用且来自客户的连接请求到达时客户和服务器的状态:

  

  从accept返回后,我们立即就有图4-15所示状态。连接被内核接受,新的套接字connfd被创建。这是一个已连接套接字,可由此跨连接读写数据:

  

  并发服务器的下一步是调用fork,图4-16给出了从fork返回后的状态:

  

  注意,此时listenfd和connfd这两个描述符都在父进程和子进程之间共享(被复制)。

  再下一步是由父进程关闭已连接套接字,由子进程关闭监听套接字,如图4-17所示:

  

  这是这两个套接字所期望的最终状态。子进程处理与客户的连接,父进程则可以在监听套接字上再次调用accept来处理下一个客户连接。

时间: 2024-10-12 10:42:00

并发服务器的相关文章

Unix C语言编写基于IO多路复用的小型并发服务器

背景介绍 如果服务器要同时处理网络上的套接字连接请求和本地的标准输入命令请求,那么如果我们使用accept来接受连接请求,则无法处理标准输入请求;类似地,如果在read中等待一个输入请求,则无法处理网络连接的请求. 所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作.但 select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而还

TCP并发服务器,每个客户一个子进程

在阅读完<unix 网络编程:卷一>之后,感觉作者真是unix下编程的大师级的人物.而对于我个人而言,每次阅读完一本技术书籍之后,一定还是得自己重新再写一遍程序(换点内容),复习书本中的内容(大致结构,或者说思想,相同),否则,你很难做到真的理解并掌握的地步. Okay,今天我带来的是服务器模型中的第一种,也是最基本最常用的一种模型–TCP并发服务器,每个客户一个子进程. 先简单介绍一下:TCP并发服务器,每个客户一个子进程,也就是说并发服务器调用fork派生一个子进程来处理每个子进程,使得服

UNIX网络编程卷1 服务器程序设计范式1 并发服务器,为每个客户请求fork一个进程

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.传统并发服务器调用 fork 派生一个子进程来处理每个客户 2.传统并发服务器的问题在于为每个客户现场 fork 一个子进程比较耗费 CPU 时间. /* include serv01 */ #include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; void

TCP/IP 网络编程 (抄书笔记 3) -- 僵尸进程和多任务并发服务器

TCP/IP 网络编程 (抄书笔记 3) – 僵尸进程和多任务并发服务器 TCP/IP 网络编程 (抄书笔记 3) – 僵尸进程和多任务并发服务器 Table of Contents 僵尸进程的产生 避免僵尸进程 信号 多任务的并发服务器 僵尸进程的产生 子进程先退出, 父进程没有退出 ==> 僵尸进程 父进程先退出, 子进程没有退出 ==> 子进程被 0 号进程回收, 不会产生僵尸进程 pid_t pid = fork(); if (pid == 0) { // child printf(&

1高并发服务器:多进程服务器

 1多进程并发服务器 使用多进程并发服务器时要考虑以下几点: A.父最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符) B.系统内创建进程个数(和内存大小相关) C.进程创建过多是否降低整体服务性能(进程调度) 2.案例说明 server.c,代码如下: #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #i

2高并发服务器:多线程服务器

 1多进程并发服务器 在使用线程模型开发服务器时需要考虑以下问题: A 调整进程最大文件描述符上限 B 线程如有共享数据,考虑线程同步 C 服务于客户端线程退出时,退出处理 D 2.案例说明 server.c,代码如下: /* server.c */ #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <pthr

TCP并发服务器(五)——每个客户一个线程

TCP并发服务器(五)——每个客户一个线程 1.说明 前面4个版本都是关于进程的,可以将进程改为线程来实现. 这个最简单的版本也快于前面的所有预先派生进程的版本. 2.代码 #include "unpthread.h" void sig_int(int signo) { DPRINTF("sig_int()\n"); void pr_cpu_time(void); pr_cpu_time(); exit(0); } void *doit(void *arg) { v

TCP并发服务器(一)——每个客户一个子进程

TCP并发服务器(一)——每个客户一个子进程 1.说明 这是最传统的并发服务器,对于每一个客户请求fork一个子进程.问题在于每次fork一个子进程比较耗费时间,下面会讲预创建进程. 程序代码基于UNP的库. 程序在使用进程的模式下是最慢的. 2.代码 #include "unp.h" int main(int argc, char *argv[]) { int listenfd; socklen_t addrlen; if (argc = 2) { listenfd = Tcp_li

初步谈谈 C# 多线程、异步编程与并发服务器

多线程与异步编程可以达到避免调用线程异步阻塞作用,但是两者还是有点不同. 多线程与异步编程的异同: 1.线程是cpu 调度资源和分配的基本单位,本质上是进程中的一段并发执行的代码. 2.线程编程的思维符合正常人的思维习惯,线程中的处理程序依然是顺序执行,所以编程起来比较方便,但是缺点也是明显的,多线程的使用会造成多线程之间的上下文切换带来系统花销,并且共享变量之间也是会造成死锁的问题. 3.因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变

linux网络编程-----&gt;高并发---&gt;多进程并发服务器

在做网络服务的时候并发服务端程序的编写必不可少.前端客户端应用程序是否稳定一部分取决于客户端自身,而更多的取决于服务器是否相应时间够迅速,够稳定. 常见的linux并发服务器模型: 多进程并发服务器 多线程并发服务器 select多路I/O转接服务器 poll多路I/O转接服务器 epool多路I/O转接服务器. 本次主要讨论多线程并发服务器模型: 使用多进程并发服务器时要考虑以下几点: 父进程最大文件描述个数(父进程中需要close关闭accpet返回的新文件描述符) 系统内创建进程个数(与内