《TCP/IP网络编程》第10章 多进程服务器端

本章所有示例代码>>github

10.1 进程概念及应用

1.       两种类型的服务器端

    按序处理客户端请求,如果每个客户端的平均服务时间为0.5秒,则第100个客户端会对服务器产生相当大的不满。

    所有连接请求的受理时间不超过1秒,但平均服务时间为2~3秒。

2.       并发服务器端的实现

    同时向多个客户端提供服务的并发服务器端,具有代表性的并发服务器端实现模型和方法:

    A、多进程服务器:             通过创建多个进程提供服务(不适用Windows);

    B、多路复用服务器:          通过捆绑并统一管理I/O对象提供服务;

    C、多线程服务器:             通过生成与客户端等量的线程提供服务;

3.       理解进程(Process)

    进程——“占用内存空间正在运行的程序”

    从操作系统的角度看,进程是程序流的基本单位,若创建多个进程,则操作系统将同时运行。有时一个程序运行过程中也会产生多个进程。

    CPU核心数与进程数:拥有2个、4个运算器的CPU分别称作双核(Daul)CPU、4核(Quad)CPU。核的个数与可同时运行的进程数相同。若进程数超过核数,进程将分时使用CPU资源。

4.       进程ID

    无论进程是如何创建的,所有进程都会从操作系统分配到ID。此ID称为“进程ID”,其值为大于2的整数。1要分配给操作系统启动后的首个进程(用于协助操作系统),因此用户进程无法分配ID值1。

    通过ps指令可以查看当前运行的所有进程:

    ps -au

    -au参数列出了所有进程详细信息。

5.       通过调用fork函数创建进程

    创建进程的方法有很多,此处介绍创建多进程服务器端的fork函数。

  #include <unistd.h>
  pid_t fork(void);

    fork函数将创建调用的进程副本。


    也就是说,并非根据完全不同的程序创建进程,而是复制正在运行的、调用fork函数的进程。另外,两个进程都将执行fork函数调用后的语句(准确地说是在fork函数返回后)。因为通过同一个进程、复制相同的内存空间,之后的程序流要根据fork函数的返回值加以区分。

    在父进程中,fork函数返回子进程ID

    在子进程中,fork函数返回0

    “父进程”(Parent Process)指原进程,即调用fork函数的主体。而“子进程”(Child Process)是通过fork函数复制出的进程。

    调用fork函数后,父子进程拥有完全独立的内存空间。

10.2 进程和僵尸进程

    文件操作中,关闭文件和打开文件同等重要。同样,进程销毁和进程创建同等重要。如果未认真对待进程销毁,它们将变成僵尸进程造成困扰。

1.       僵尸(Zombie)进程

    进程完成工作后(执行完main函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作“僵尸进程”。

2.       产生僵尸进程的原因

    “应该向创建子进程的父进程传递子进程的exit参数值或return语句的返回值。”

    如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态。

3.       销毁僵尸进程1:利用wait函数

    为了销毁子进程,父进程应主动请求获取子进程的返回值。

#include <sys/wait.h>
pid_t wait(int *statloc); // 成功返回终止的子进程ID,失败返回-1

    子进程终止时传递的返回值(exit函数的参数值、main函数的return返回值)将保存到该函数的参数所指内存空间。但函数参数所指向的单元中还包含其他信息,因此需要通过以下的宏进行分离:

    WIFEXITED子进程正常终止时返回“真”(true);

    WEXITSTATUS返回子进程的返回值。

    即向wait函数传递变量status的地址时,调用wait函数后应编写如下代码:

if(WIFEXITED(status))
{
    puts(“Normaltermination!”);
    printf(“Childpass num: %d”, WEXITSTATUS(status));
}

    注意:调用wait函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此需谨慎调用该函数。

4.       销毁僵尸进程2:使用waitpid函数

    wait函数会引起程序阻塞,还可以考虑调用waitpid函数。既能防止僵尸进程,又能防止程序阻塞。

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options); // 成功时返回终止的子进程ID(或0),失败时返回-1
    - pid:       等待终止的目标子进程的ID,若传递-1,则与wait函数相同,可以等待任意子进程终止;

    -statloc: 与wait函数的statloc参数相同意义;

    -option传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数;

10.3 信号处理

    父进程往往与子进程一样繁忙,因此不能只调用waitpid函数以等待子进程终止。

1.       向操作系统求助

    若操作系统能把子进程终止信息高速忙于工作的父进程,将有助于构建高效的程序。

    为实现该想法,引入信号处理(SignalHandling)机制,此处的信号是在特定事件发生时由操作系统向进程发送的消息。

2.       信号与signal函数

    信号注册函数:

#include <signal.h>
void (*signal(int signo, void (*fun(int)))(int); // 为了在产生信号时调用,返回之前注册的函数指针

  • 函数名:      signal
  • 参数:         int signo、void(*func)(int)
  • 返回类型:  返回void型函数指针

    调用上述函数时,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针)。(发生第一个参数代表的情况时,调用第二个参数所指的函数)

    部分特殊情况:

  • SIGALARM:   已到通过调用alarm函数注册的时间;
  • SIGINT:          输入CTRL+C;
  • SIGCHLD:      子进程终止;

    alarm函数:

#include <unistd.h>
unsigned int alarm(unsigned int seconds); // 返回0或以秒为单位的距signal信号发生所剩时间

    如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生SIGALARM信号。

    “发生信号时将唤醒由于调用sleep函数而进入阻塞状态的进程”

    产生信号时,为了调用信号处理器,将唤醒由于调用sleep函数而进入阻塞状态的进程。而且,进程一旦被唤醒,就不会再进入睡眠状态。即使还未到sleep函数中规定的时间也是如此。

    实际上,现在很少使用signal函数编写程序,它只是为了保持对旧程序的兼容。

3.       利用sigaction函数进行信号处理

    “signal函数在UNIX系统的不同操作系统中可能存在区别,但sigaction函数完全相同”

#include<signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact); //成功返回0,失败返回-1

       -signo:   与signal函数相同,传递信号信息;

       -act:       对应于第一个参数的信号处理函数(信号处理器)信息;

       -oldact:  通过此参数获取之前注册的信号处理函数指针,若不需要则传递0;

    sigaction结构体:

struct sigaction
{
    void(*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
};

4.       利用信号处理技术消灭僵尸进程

    子信号终止时将产生SIGCHLD信号。

10.4 基于多任务的并发服务器

1.       基于进程的并发服务器模型

    每当有客户端请求服务(连接请求)时,服务器端都创建子进程以提供服务

  • 第一阶段:服务器端(父进程)通过调用accept函数受理连接请求;
  • 第二阶段:此时获取的套接字文件描述符创建并传递给子进程;
  • 第三阶段:子进程利用传递来的文件描述符提供服务;

2.       实现并发服务器

if(pid == 0)
{
    // 由子进程向客户端提供回声服务
    close(serv_sock);   //可关闭服务器套接字,是因为服务器套接字文件描述符同样也传递到子进程
    while((str_len =read(data_sock, buf, BUF_SIZE)) != 0)
        write(data_sock, buf, str_len);
        close(data_sock);
        puts("clientdisconnected...");
        return 0;
}
else
{
    // 由accept函数创建的套接字文件描述符已经复制给了子进程
    close(data_sock);
}

3.       通过fork函数复制文件描述符

    通过fork函数复制文件描述符。父进程将2个套接字(一个是服务器端套接字,另一个是与客户端连接的套接字)文件描述符复制给子进程

    只复制文件描述符,并未复制套接字。套接字并非进程所有——从严格意义上说,套接字属于操作系统——只是进程拥有代表相应套接字的文件描述符。

    1个套接字中存在2个文件描述符,只有2个文件描述符都终止(销毁)后,才能销毁套接字。调用fork函数后,要将无关的套接字文件描述符关掉。

10.5 分割TCP的I/O程序

    回声客户端I/O分割模型如下图。客户端的父进程负责接收数据,额外创建的子进程负责发送数据。分割后,不同进程分别负责输入和输出,这样,无论客户端是否从服务器端接受完数据都可以进行传输。

    其实,按照这种实现方式,程序的实现更加简单。父进程中只需编写接收数据的代码,子进程只需编写发送数据的代码,所以会简化。实际上,在1个进程内同时实现数据收发逻辑需要考虑更多细节。

    其实,回声客户端并不用分割I/O(仅作示例)。

    分割I/O程序的一个优点是,可以提高频繁交换数据的程序性能。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值