多进程服务端

并发服务器端的实现方法

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

fork函数创建进程

1
2
3
4
#include <unistd.h>

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

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

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

僵尸进程及产生原因

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

调用fork函数产生子进程的终止方式:

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

向exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。将子进程变成僵尸进程的正是操作系统。

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

销毁僵尸进程

wait函数

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

1
2
3
4
#include <sys/wait.h>

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

调用wait函数时,如果没有已终止的子进程,那么程序将阻塞直到有子进程终止,需要谨慎使用。

waitpid函数

为了防止阻塞,可以使用waitpid函数。

1
2
3
4
5
6
7
#include <sys/wait.h>

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

信号与signal函数

1
2
3
4
#include <signal.h>

// 为了在产生信号时调用,返回之前注册的函数指针
void (*signal(int signo, void (*func)(int)))(int);

第一个参数signo为特殊信息,第二个参数void (*func)(int)为无特殊情况下将要调用的函数指针。

可以在signal函数中注册的部分特殊情况:

  • SIGALRM:已通过调用alarm函数注册的时间。
  • SIGINT:输入CTRL+C。
  • SIGCHLD:子进程终止。
1
2
3
4
#include <unistd.h>

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

利用sigaction函数进行信号处理

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <signal.h>

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

struct sigaction {
void (*sa_handler)(int); // 保存信号处理函数指针
sigset_t sa_mask;
int sa_flags;
};

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

每当有客户端请求服务时,回声服务器端都创建子进程以提供服务。

与之前的回声服务器端的区别所在:

第一阶段:回声服务器端(父进程)调用accept函数受理连接请求。

第二阶段:此时获取的套接字文件描述符创建并传递给子进程。

第三阶段:子进程利用传递来的文件描述符提供服务。

通过管道实现进程间通信

为了完成进程间通信,需要创建管道。管道并非属于进程的资源,而是和套接字一样,属于套作系统。所以,两个进程通过操作系统提供的内存空间进行通信。

1
2
3
4
5
6
#include <unistd.h>

// filedes[0] 通过管道接收数据时使用的文件描述符,即管道入口
// filedes[1] 通过管道传输数据时使用的文件描述符,即管道出口
// 成功返回0,失败返回-1
int pipe(int filedes[2]);

对于使用单个管道实现互相通信需要注意进程间的收发消息顺序,对于进管道的数据,先使用read函数的进程会先获得数据。为了避免这种不确定性,可以创建两个管道,各自负责不同的数据流动。

I/O复用

select函数的功能和调用顺序

使用select函数时可以将多个文件描述符集中到一起统一监视。

  • 是否存在套接字接收数据?
  • 无需阻塞传输数据的套接字有哪些?
  • 那些套接字发生了异常?

select函数的调用方法和顺序如下:

设置文件描述符

监视文件描述符可以视为监视套接字。首先需要将要监视的文件描述符集中到一起,同时按照监视项进行区分。使用fd_set数组变量执行此操作。该数组是存有0和1的位数组。如果该为设置为1,则表示该文件描述符是监视对象。

由于fd_set以位为操作,直接操作会繁琐,所以在库中定义了几个宏:

  • FD_ZERO(fd_set* fdset); 将fd_set变量的所有位初始化为0。
  • FD_SET(int fd, fd_set* fdset); 在参数fdset指向的变量中注册文件描述符fd的信息。
  • FD_CLR(int fd, fd_set* fdset); 在参数fdset指向的变量中清除文件描述符fd的信息。
  • FD_ISSET(int fd, fd_set* fdset); 若参数fdset指向的变量中包含文件描述符fd的信息,则返回“真”。

设置检查(监视)范围及超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/select.h>
#include <sys/time.h>

// maxfd 监视对象文件描述符数量
// readset 将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量,并传递其地址值
// writeset 将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量,并传递其地址值
// exceptset 将所有关注“是否发生异常”的文件描述符注册到fd_set型变量,并传递其地址值
// timeout 调用select函数后,为了防止陷入无限阻塞的状态,传递超时信息
// 成功返回大于0的值,失败返回-1
int select(int maxfd, fd_set* readset, fd_set* writeset,
fd_set* exceptset, const struct timeval* timeout);

// timeval结构体定义
struct timeval {
long tv_sec; // seconds
long tv_usec; // microseconds
};

多播与广播

多播方式的数据传输是基于UDP完成的。采用多播时,可以同时向多个主机传递数据。

广播只能向同一网络中的主机传输数据。

多播的数据传输方式及流量方面的优点

优点如下:

  • 多播服务器端针对特定多播组,只发送1次数据。
  • 即使只发送1次数据,但该组内的所有客户端都会接收到数据。
  • 多播组数可在IP地址范围内任意增加。
  • 加入特定组即可接受发往该多播组的数据。

多播组是D类IP地址(224.0.0.0 ~ 239.255.255.255)。

广播的理解及实现方法

广播也是基于UPD完成的。根据传输数据时使用的IP地址的形式,分为以下两种:

  • 直接广播。
  • 本地广播。

直接广播的IP地址除了网络中的地址外,其余主机地址全部设置为1。例如,希望向网络地址192.12.34中的所有主机传输数据时,可以向192.12.34.255传输。

本地广播中使用的IP地址限定为255.255.255.255。例如,192.32.24网络中的主机向255.255.255.255传输数据时,数据将传输到192.32.24网络中的所有主机。