TCP/IP网络编程笔记-ch10.多进程服务器端

文章目录

  • 函数
    • 创建进程
    • 销毁僵尸进程1 wait
    • 销毁僵尸进程2 waitpid
    • 注册信号signal函数
    • alram函数
    • 信号处理函数sigaction
  • 知识点
  • 进程概念及应用
    • 并发服务器端的实现方法
    • 进程
      • **进程**
      • **进程ID**
      • "通过fork函数创建进程"
    • 进程和僵尸进程
      • 产生僵尸进程的原因
      • 销毁僵尸进程1:利用wait函数
      • 销毁僵尸进程2:利用waitpid函数
    • 信号处理
      • 向操作系统求助
      • 信号与signal函数
    • 基于多任务的并发服务器
      • 基于进程的并发服务器模型
      • 通过fork函数复制文件描述符
    • 分隔TCP的I/O程序
      • 分隔I/O程序的优点
  • 实例

函数

创建进程

#include

pid_t fork(void);
//成功返回进程ID,失败返回-1

销毁僵尸进程1 wait

#include 

pid_t wait(int * statloc);
//成功返回终止的子进程ID,失败返回-1

调用此函数时,若已有子进程终止,子进程终止时传递返回值(exit函数的参数值、main函数的return返回值)将保存到该函数的参数所指内存空间。
函数参数指向单元中还含有其他信息,通过下列宏进行分离。

WIFEXITED:子进程正常终止时返回true
WEXITSTATUS:子进程返回值

使用:

int status;
pid=fork();
if(pid==0)
{
    exit(7);
}

wait(&status);
if(WIFEXITED(status))
{
    printf("Child send one:&d \n",WEXITSTATUS(status));
}

注意:

当调用wait函数时,如果没有已终止的子进程,那程序将阻塞(Blocking)直到有子进程终止。

销毁僵尸进程2 waitpid

#include

@param pid:等待终止的目标子进程的ID,若传递-1,则同wait函数,可等待任意子进程终止。
@param statloc:与wait函数的statloc参数具有相同含义
@param options:传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数
pid_t waitpid(pid_t pid,int * statloc,int options);
//成功返回终止的子进程ID(或0),失败返回-1

使用:

int status;
pid_t pid=fork();

if(pid==0)
{
    sleep(15);
    return 24;
}
else
{
    while(!waitpid(-1,&status,WNOHANG))//waitpid不阻塞,即使没有终止子进程也不影响程序执行
    {
        sleep(3);
        puts("sleep 3sec");
    }
}

调用waitpid函数时,程序不会阻塞。

注册信号signal函数

#include

@param signo:特殊情况信息
@param func:为特殊情况下将调用的函数的地址值(指针)。发生第一个参数代表的情况时,调用第二个参数所指的信息。
void(*signal(int signo,void (*func)(int)))(int);

signo取值和情况: 
    SIGALRM:    已到通过调用alarm函数注册的时间
    SIGINT:     输入CTRL+C
    SIGCHLD:    子进程终止
//为在产生信号时调用,返回之前注册的函数指针

例如:

已到通过alaram函数注册时间,请调用timeout函数:
signal(SIGALRM,timeout);

输入CTRL+C时调用keycontrol函数:
signal(SIGINT,keycontrol);

alram函数

#include

unsigned int alarm(unsigned int seconds);
//返回0或以秒为单位的距SIGALRM信号发生所剩时间

调用该函数后,seconds秒后将产生SIGALRM信号,若向该函数传递0,则之前对SIGALRM信号的预约将取消。如果通过该函数预约信号后未指定该信号对应处理参数,则(通过调用signal函数)终止进程,不进行任何处理。

信号处理函数sigaction

它类似于signal函数且能完全代替后者。它的稳定是因为:
signal函数在UNIX系列不同操作系统中可能存在区别,但sigaction函数完全相同

#include

@param signo:与signal函数相同,传递信号信息
@param act:对应于第一个参数的信号处理函数(信号处理器)信息
@param oldact:通过此参数获取之前注册的信号处理函数指针,若不需要则传递0
int sigaction(int signo,const struct sigaction * act,struct sigaction * oldact);
//成功返回0,失败返回-1

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

sa_handler成员保存信号处理函数的指针值(地址值)。sa_mask和sa_flags的所有位均初始化为0即可。
使用例:

void timeout(int sig)//信号函数定义
{
    if(sig==SIGALRM)//如果传递信号类型为SIGALRM
    
    else
}

struct sigaction act;
act.sa_handler=timeout;//信号函数名
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGALRM,&act,0);//注册信号处理函数
alarm(2);//2s后发出SIGALRM信号

知识点

进程概念及应用

按之前学习到的内容,我们可构建按序向第一个客户端到一百个客户端提供服务的服务器端。如果每个客户端的平均服务时间为0.5秒,则第100个客户端会对服务器产生相当大的不满。
为了解决这种问题,需要使用多进程服务器端。

并发服务器端的实现方法

具有代表性的并发服务器端实现模型和方法

多进程服务器:通过创建多个进程提供服务(windows不支持)
多路复用服务器:通过捆绑并统一管理I/O对象提供服务
多线程服务器:通过生成与客户端等量的线程提供服务

进程

进程

定义:

占用内存空间的正在运行的程序。

网上下载游戏安装到硬盘中,此时游戏是程序而非进程,当游戏进入运行状态时,开始运行程序,游戏被加载到主内存并进入运行状态,此时才可称为进程。同时运行多个该游戏,则会生成相当数量的进程,也会占用相应进程数的内存空间。

进程是程序流的基本单位,若创建多个进程,则操作系统将同时运行。有时一个程序运行过程中也会产生多个进程。多进程服务器即其中的代表。

进程ID

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

观察Linux中正在运行的进程:

ps au
指定a u 参数列出了所有进程详细信息

TCP/IP网络编程笔记-ch10.多进程服务器端_第1张图片

“通过fork函数创建进程”

#include

pid_t fork(void);
//成功返回进程ID,失败返回-1

fork函数创建调用的进程副本,复制正在运行的、调用fork函数的进程,两个进程都将执行fork函数调用后的语句。因为通过同一进程复制相同的内存空间,之后的程序要根据fork函数的返回值加以区分。

区分:

父进程:fork函数返回子进程ID
子进程:fork函数返回0

父进程(Parent Process)指苑锦城,即调用fork函数的主体,而"子进程"(Child Process)是通过父进程调用fork函数时复制出的进程。
TCP/IP网络编程笔记-ch10.多进程服务器端_第2张图片

进程和僵尸进程

进程销毁和进程创建同等重要。若没有认真对待进程销毁,它们将变成"僵尸进程"来占用系统中的重要资源,这是给系统带来负担的原因之一。

产生僵尸进程的原因

下两个示例为调用fork函数产生子进程的终止方式:

传递参数并调用exit函数
main函数中执行return语句并返回值

向exit函数传递的参数值和main函数的return语句产生值都会传递给操作系统,而操作系统不会销毁子进程,直到保存着把这些值传递给产生该子进程的父进程
处于这种状态下的进程即为僵尸进程。将子进程变为僵尸进程的正是操作系统。而销毁僵尸进程的方法也显然易见:

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

所以父进程必须负责收回自己产生的子进程。

后台处理:

将控制台窗口中指令放在后台运行。如:
./zombie & 
则程序在后台运行(&触发)

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

销毁僵尸进程2:利用waitpid函数

信号处理

现在已经解决了进程创建和销毁的问题,但还有个问题:

子进程何时终止?waitpid后要无休止地等待吗?

向操作系统求助

子进程终止的识别主题是操作系统,我们引入**信号处理(Signal Handing)**机制,“信号"是在特定事件发生时由操作系统向进程发送的信息。为响应该消息,执行与消息相关的自定义操作的过程称为"处理"或"信号处理”。

信号与signal函数

signal函数见函数区域

信号注册后,发生注册信号时,操作系统将调用该信号对应函数。

alarm函数见函数区域

sigaction函数见函数区域.

可以利用信号处理技术来消灭僵尸进程:

在信号处理函数中放入waitpid函数
void read_childproc(int sig)
{
    int status;
    pid_t id=waitpid(-1,&status,WNOHANG);
}

sigaction(SIGCHLD,&act,0);

基于多任务的并发服务器

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

之前的回声服务器端只能向一个客户端提供服务,接下来对其进行扩展,使其能同时向多个客户端提供服务。
TCP/IP网络编程笔记-ch10.多进程服务器端_第3张图片

如上图所示,每当有客户端请求服务时,回声服务器端都创建子进程提供服务。请求服务的客户端若有5个,则创建5个子进程提供服务。
为完成这些任务,需以下过程:

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

子进程传递套接字描述符的方法:
子进程会复制父进程拥有的所有资源,不需要另外经过传递文件描述符的过程。

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

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

文件描述符的复制:
套接字属于操作系统,进程拥有代表相应套接字的文件描述符,复制该文件描述符。(复制套接字后,统一端口对应多个套接字)

注意:
一个套接字存在2个文件描述符时,只有2个文件描述符都终止(销毁)后,才能销毁套接字。

分隔TCP的I/O程序

分隔I/O程序的优点

我们已经实现的回声客户端的数据回声方式如下:

向服务器端传输数据,并等待服务器回复。无条件等待,知道接收完服务器端的回声数据后,才能传输下面的数据。

传输数据后需等待服务器端返回的数据,因为在程序代码中重复调用了read和write函数,只能这么写的原因之一是:程序在1个进程中运行,现在可创建多个进程,因此可以分隔数据收发过程。

TCP/IP网络编程笔记-ch10.多进程服务器端_第4张图片

由上图可知

客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,
这样,无论客户端是否从服务器端接收完数据都可以进程传输。

这样为什么能简化程序实现?

这种实现方式,父进程只需编写接收数据的代码,子进程只需编写发送数据的代码,所以会简化。  

分隔I/O程序的另一个好处是:可以提高频繁交换数据的程序性能。如图:
TCP/IP网络编程笔记-ch10.多进程服务器端_第5张图片

右侧是分隔I/O后的客户端数据传输方式,左侧是分离之前的,分隔后的客户端发送数据不必考虑接受数据的情况,因此可以连续发送数据,
由此提高同一时间内传输数据量。这种差异在网速慢时尤为明显。

实例

fork函数的调用:
TCP/IP网络编程笔记-ch10.多进程服务器端_第6张图片

创建僵尸进程实例:
TCP/IP网络编程笔记-ch10.多进程服务器端_第7张图片

后台处理&实例:
TCP/IP网络编程笔记-ch10.多进程服务器端_第8张图片

wait函数销毁僵尸进程实例:
TCP/IP网络编程笔记-ch10.多进程服务器端_第9张图片

waitpid函数销毁僵尸进程实例:
TCP/IP网络编程笔记-ch10.多进程服务器端_第10张图片

信号处理相关示例:
TCP/IP网络编程笔记-ch10.多进程服务器端_第11张图片

siaction函数:
TCP/IP网络编程笔记-ch10.多进程服务器端_第12张图片

信号处理技术消灭僵尸进程:
TCP/IP网络编程笔记-ch10.多进程服务器端_第13张图片

实现并发服务器:
TCP/IP网络编程笔记-ch10.多进程服务器端_第14张图片

你可能感兴趣的:(网络,tcp/ip,网络协议)