一、进程的概念以及应用(上)
利用之前学习到的内容,我们可以构建按序向第一个客户端到第一百个客户端提供服
务的服务器端。当然,第一个客户端不会抱怨服务器端,但如果每个客户端的平均服务时间
为0.5 秒,则第100 个客户端会对服务器端产生相当大的不满。
两种类型的服务端
如果真正为客户端着想,应提高客户端满意度平均标准。如果有下面这种类型的服务器
端,应该感到满意了吧???
"第一个连接请求的受理时间为0 秒,第50 个连接请求的受理时间为50 秒,第100 个
连接请求的受理时间为100 秒!但只要受理,服务只需1 秒钟。"
如果排在前面的请求数能用一只手数清,客户端当然会对服务器端感到满意。但只要超
过这个数,客户端就会开始抱怨。还不如用下面这种方式提供服务。
"所有连接请求的受理时间不超过1 秒,但平均服务时间为2~3 秒。"
并发服务器的实现方法:
即使有可能延长服务时间,也有必要改进服务器端,使其同时向所有发起请求的客户端
提供服务,以提高平均满意度。
而且,网络程序中数据通信时间比CPU 运算时间占比更大,因此,向多个客户端提供
服务是一种有效利用CPU 的方式。接下来讨论同时向多个客户端提供服务的并发服务器端。
下面列出的是具有代表性的并发服务器端实现模型和方法。
1 多进程服务器∶通过创建多个进程提供服务。
2 多路复用服务器∶通过捆绑并统一管理I/O 对象提供服务。
3 多线程服务器∶通过生成与客户端等量的线程提供服务。
先讲解第一种方法∶多进程服务器。这种方法不适合在Windows 平台下(Windows 不支持)。
理解进程:
接下来了解多进程服务器实现的重点内容——进程,其定义如下∶
"占用内存空间的正在运行的程序"
假如同学们从网上下载了《植物大战僵尸游戏》并安装到硬盘。此时的游戏并非进程,
而是程序。因为游戏并未进入运行状态。接着开始运行程序。此时游戏被加载到主内存并进
入运行状态,这时才可称为进程。如果同时运行多个植物大战僵尸游戏程序,则会生成相应
数量的进程,也会占用相应进程数的内存空间。
再举个例子。进行文档相关操作,这时应打开文档编辑软件。如果工作的同时还想听音
乐,应打开酷狗播放器。另外,为了与朋友聊天,再打开微信软件。此时共创建3 个进程。
从操作系统的角度看,进程是程序流的基本单位,若创建多个进程,则操作系统将同时运行。
有时一个程序运行过程中也会产生多个进程。接下来要创建的多进程服务器就是其中的代
表。编写服务器端前,先了解一下通过程序创建进程的方法。
二、进程的概念以及应用(下)
CPU 核的个数与进程数
拥有2 个运算设备的CPU 称作双核CPU,拥有4 个运算器的CPU 称作4 核CPU。也就
是说,1 个CPU 中可能包含多个运算设备(核)。核的个数与可同时运行的进程数相同。相
反,若进程数超过核数,进程将分时使用CPU 资源。但因为CPU 运转速度极快,我们会感
到所有进程同时运行。当然,核数越多,这种感觉越明显。
进程ID
无论进程是如何创建的,所有进程都会从操作系统分配到ID。此ID 称为"进程ID",其
值为大于2 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用
户进程无法得到ID 值1。
通过ps au 指令可以查看当前运行的所有进程。特别需要注意的是,该命令同时可以列
出PID(进程ID)。通过指定a 和u 参数列出了所有进程详细信息。
通过调用fork 函数创建进程
创建进程的方法很多,此处只介绍用于创建多进程服务器端的fork 函数。
#include <unistd.h>
pid_t fork(void);
→成功时返回进程ID,失败时返回-1。
fork 函数将创建调用的进程副本。也就是说,并非根据完全不同的程序创建进程,
而是复制正在运行的、调用fork 函数的进程。另外,两个进程都将执行fork 函数调用
后的语句(准确地说是在fork 函数返回后)。但因为通过同一个进程、复制相同的内
存空间,之后的程序流要根据fork 函数的返回值加以区分。即利用fork 函数的如下特
点区分程序执行流程。
父进程∶fork 函数返回子进程ID。
子进程∶fork 函数返回0。
此处"父进程"(Parent Process)指原进程,即调用fork 函数的主体,而"子进程"(Child
Process)是通过父进程调用fork 函数复制出的进程。接下来讲解调用fork 函数后的程
序运行流程,如下图所示。
三、进程和僵尸进程
文件操作中,关闭文件和打开文件同等重要。同样,进程销毁也和进程创建同等重要。
如果未认真对待进程销毁,它们将变成僵尸进程困扰大家。
进程的世界同样如此。进程完成工作后(执行完main 函数中的程序后)应被销毁,但
有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作"僵尸进程",
这也是给系统带来负担的原因之一。
产生僵尸进程的原因
首先利用如下两个示例展示调用fork 函数产生子进程的终止方式。
1 传递参数并调用exit 函数。
2 main 函数中执行retun 语句并返回值。
向exit 函数传递的参数值和main 函数的returm 语句返回的值都会传递给操作系统。而
操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的
进程就是僵尸进程。也就是说,将子进程变成僵尸进程的正是操作系统。既然如此,此僵尸
进程何时被销毁呢?
"应该向创建子进程的父进程传递子进程的exit 参数值或return 语句的返回值。"
如何向父进程传递这些值呢?操作系统不会主动把这些值传递给父进程。只有父进程主
动发起请求(函数调用)时,操作系统才会传递该值。换言之,如果父进程未主动要求获得
子进程的结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态。也就是
说,父母要负责收回自己生的孩子
销毁僵尸进程1∶利用wait 函数
为了销毁子进程,父进程应主动请求获取子进程的返回值。比较常用的共有2 种,其中
之一就是调用wait 函数。
#include<sys/wait.h>
pid_t wait(int* statloc);
成功时返回终止的子进程ID,失败时返回-1。
调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit 函数的参数
值、main 函数的return 返回值)将保存到该函数的参数所指内存空间。但函数参数指向的
单元中还包含其他信息,因此需要通过下列宏进行分离。
1. WIFEXITED 子进程正常终止时返回"真"(true)。
2. WEXITSTATUS 返回子进程的返回值。
也就是说,向wait 函数传递变量status 的地址时
调用wait 函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此需谨慎调用该函数。
销毁僵尸进程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 参数具有相同含义。
●options 传递头文件sys/wait.h 中声明的常量WNOHANG,即使没有终止的子进程也
不会进入阻塞状态,而是返回0 并退出函数。
优点:调用waitpid 函数时,程序不会阻塞。
四、信号处理和signal函数
我们已经知道了进程创建及销毁方法,但还有一个问题没解决。
"子进程究竟何时终止?调用waipid 函数后要无休止地等待吗?"
父进程往往与子进程一样繁忙,因此不能只调用waitpid 函数以等待子进程终止。
方法:
向操作系统求助
子进程终止的识别主体是操作系统,因此,若操作系统能把如下信息告诉正忙于工作的父进
程,将有助于构建高效的程序。
"嘿,父进程!你创建的子进程终止了!"
此时父进程将暂时放下工作,处理子进程终止相关事宜。这就是信号处理(Signal Handling)
机制。此处的"信号"是在特定事件发生时由操作系统向进程发送的消息。另外,为了响应该
消息,执行与消息相关的自定义操作的过程称为"信号处理"。
我们想象一下如下场景:
进程∶"嘿,操作系统!如果我之前创建的子进程终止,就帮我调用zombie_handler 函数。"
操作系统∶"好的!如果你的子进程终止,我会帮你调用zombie_handler 函数,你先把该
函数要执行的语句编好!"
上述场景中进程所讲的相当于"注册信号"过程,即进程发现自己的子进程结束时,请求
操作系统调用特定函数。该请求通过signal 函数调用完成(因此称signal 为信号注册函数)。
#include<signal.h>
void(*signal(int signo, void(*func)(int))(int);
等价于<=>
typedef void(*signal_handler)(int);
signal_handler signal(int signo,signal_handler func);
→为了在产生信号时调用,返回之前注册的函数指针。
函数名∶signal
参数∶int signo, void(* func)(int)
返回类型∶参数为int 型,返回void 型函数指针。
调用上述函数时,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函
数的地址值(指针)。发生第一个参数代表的情况时,调用第二个参数所指的函数。下面给
出可以在signal 函数中注册的部分特殊情况和对应的常数。
SIGALRM∶已到通过调用alarm 函数注册的时间。
SIGINT∶输入CTRL+C。
SIGCHLD∶子进程终止。(英文为child)
具体说明:
编写调用signal 函数的语句完成如下请求
1、"子进程终止则调用mychild 函数。"
代码:signal(SIGCHLD, mychild);
此时mychild 函数的参数应为int,返回值类型应为void。对应signal 函数的第二个参数。
另外,常数SIGCHLD 表示子进程终止的情况,应成为signal 函数的第一个参数。
2、"已到通过alarm 函数注册的时间,请调用timeout 函数。"
3、"输入CTRL+C 时调用keycontrol 函数。"
代表这2 种情况的常数分别为SIGALRM 和SIGINT,因此按如下方式调用signal 函数。
2、signal(SIGALRM, timeout);
3、signal(SIGINT, keycontrol);
以上就是信号注册过程。注册好信号后,发生注册信号时(注册的情况发生时),操作
系统将调用该信号对应的函数。
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
→返回0 或以秒为单位的距SIGALRM 信号发生所剩时间。
如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生
SIGALRM 信号。若向该函数传递0,则之前对SIGALRM 信号的预约将取消。如果通过该函数
预约信号后未指定该信号对应的处理函数,则(通过调用signal 函数)终止进程,不做任何
处理。
五、基于多任务的并发服务器
#include<sys/socket.h>
#include<arpa/inet.h>//网络地址的一些处理函数
#include<sys/wait.h>//处理子进程避免成为僵尸进程
#include<unistd.h>
void client85() {int client = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(servaddr)); //清零 防止意外servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");servaddr.sin_port = htons(31117);int ret = connect(client, (struct sockaddr*)&servaddr, sizeof(servaddr));if (ret == 0) {printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);char buffer[256] = "hello world";printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);ssize_t ret = write(client, buffer, sizeof(buffer));//发给服务器printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);if (ret <= 0){fputs("write failed!\n", stdout);//close(client);std::cout << "client done!" << std::endl;return;}memset(buffer, 0, sizeof(buffer));printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);ssize_t ret2 = read(client, buffer,sizeof(buffer));if (ret2 <= 0){printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);fputs("read failed!\n", stdout);close(client);std::cout << "client done!" << std::endl;return;}std::cout << "from server:" << buffer<<std::endl;}//close(client);std::cout << "client done!" << std::endl;
}
void hand_childProc(int sig) {pid_t pid;int status = 0;pid = waitpid(-1, &status, WNOHANG);//检测是否有子进程结束printf("%s(%d):%s removed sub proc:%d\r\n", __FILE__, __LINE__, __FUNCTION__, pid);
}
void server85() {int serv_sock;struct sigaction act;act.sa_flags = 0;act.sa_handler = hand_childProc;sigaction(SIGCHLD, &act, 0);struct sockaddr_in serv_adr, client_adr;memset(&serv_adr, 0, sizeof(serv_adr));//memset(&client_adr, 0, sizeof(client_adr));serv_adr.sin_family = AF_INET;serv_adr.sin_addr.s_addr = INADDR_ANY;serv_adr.sin_port = htons(31117);printf("%s(%d):%s client is closed!\n", __FILE__, __LINE__, __FUNCTION__);serv_sock = socket(PF_INET, SOCK_STREAM, 0);printf("%s(%d):%s client is closed!\n", __FILE__, __LINE__, __FUNCTION__);if (bind(serv_sock, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) {handle_error("bind failed!");}if (listen(serv_sock, 5) == -1) {handle_error("listen failed!");}int count = 0;while (1) {socklen_t size = sizeof(client_adr);int client = accept(serv_sock, (sockaddr*)&client_adr, &size);//阻塞等待客户端连接printf("%s(%d):%s client is closed!\n", __FILE__, __LINE__, __FUNCTION__);char buffer[256] = "";if (client >= 0) {count++;pid_t pid = fork();if (pid == 0) {//客户端close(serv_sock);memset(buffer, 0, sizeof(buffer));ssize_t length = 0;while ((length = read(client, buffer, sizeof(buffer))) > 0) {write(client, buffer, length);}close(client);printf("%s(%d):%s client is closed!\n", __FILE__, __LINE__, __FUNCTION__);return;}if (pid < 0) {close(client);printf("%s(%d):%s fork is failed!\n", __FILE__, __LINE__, __FUNCTION__);break;}close(client);}else {handle_error("accept failed!\n");}if (count >= 5)break;}close(serv_sock);return;}
void lession85() {pid_t pid = fork();if (pid == 0) {//子进程//启动服务器server85();}else if (pid > 0) {//主//启动客户端printf("%s(%d):%s wait server invoking!\n", __FILE__, __LINE__, __FUNCTION__);sleep(1);for (int i = 0; i < 5; i++) {pid = fork();if (pid > 0) {continue;}else {//子进程//启动客户端client85();break;}}}
}