信号与 signal 函数
下列进程和操作系统间的对话是帮助大家理解信号处理而编写的, 其中包含了所有信号处理相关内容.
上述对话中进程所讲相当于"注册信号" 过程, 即进程发现自己的子进程结束时, 请求操作系统调用特定函数. 该请求通过如下函数调用完成 (因此称此函数为信号注册函数).
上述函数的返回值类型为函数指针, 因此函数声明有些繁琐. 若各位不太熟悉返回值类型为函数指针的声明, 希望加强学习(不懂函数指针将无法理解后续内容). 现在为了便于讲解, 我将上述函数声明整理如下
调用上述函数时, 第一个参数为特殊情况信息, 第二个参数为特殊情况下将要调用的函数的地址值(指针). 发生第一个参数代表的情况时, 调用第二个参数所指的的函数. 下面给出可以在signal 函数中注册的部分特殊情况对应的常数.
接下来编写调用 signal 函数的语句完成如下请求:
此时 mychild 函数的参数应为int, 返回值类型应为void. 只有这样才能成为signal 函数的第二个参数. 另外, 常数 SIGCHLD 定义了子进程终止的情况, 应成为 signal 函数的第一参数. 也就是说, signal 函数调用语句如下.
接下来编写 signal 函数的调用语句, 分别完成如下2个请求.
代表这2种情况的常数分别为 SIGALRM 和 SIGINT, 因此按如下方式调用 signal 函数.
以上就是信号注册过程. 注册好信号后, 发生注册信号时(注册的情况发生时), 操作系统将调用该信号对应的函数. 下面通过示例验证, 先介绍 alarm 函数.
如果调用该函数的同时向它传递一个正整参数, 相应时间后(以秒为单位)将产生 SIGALRM 信号. 若向该函数传递0, 则之前对 SIGALRM 信号的预约将取消. 如果通过该函数预约信号后未指定该信号对应处理函数, 则(通过调用signal 函数)终止进程, 不做任何处理.
希望引起注意.
接下来给出信号处理相关示例, 希望各位通过该示例彻底掌握之前的内容.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if (sig == SIGALRM)
{
puts("Time out!");
}
alarm(2);
}
void keycontrol(int sig)
{
if (sig == SIGINT)
{
puts("CTRL+C pressed");
}
}
int main(int argc, char *argv[])
{
int i;
signal(SIGALRM, timeout);
signal(SIGINT, keycontrol);
alarm(2);
for (i=0; i<3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
运行结果:
上述是没有任何输入时的运行结果. 下面在运行过程中输入 CTRL+C. 可以看到输出 "CTRL+C pressed " 字符串. 有一点必须说明:
调用函数的主体的确是操作系统, 但进程处于睡眠状态时无法调用函数. 因此, 产生信号时, 为了调用信号处理器, 将唤醒由于调用sleep 函数而进入阻塞状态的进程. 而且, 进程一旦被唤醒, 就不会再进入睡眠状态. 即使还未到sleep 函数中规定时间也是如此. 所以, 上述示例运行不到10秒就会结束, 连续输入 CTRL+C 则有可能1秒都不到.
利用 sigaction 函数进行信号处理
前面所学的内容主以用来防止僵尸进程生成的代码. 但我还想介绍 sigaction 函数, 它类似于 signal 函数, 而且完全可以代替后者, 也更稳定. 之所以稳定, 是因为如下原因:
实际上现在很少使用 signal 函数编写程序, 它只是为了保持对旧程序的兼容. 下面介绍sigaction 函数, 但只讲解可替代 signal 函数的功能, 因为全面介绍给各位带来不必要的负担.
声明并初始化 sigaction 结构体变量以上调用上述函数, 该结构体定义如下.
此结构体的 sa_handler 成员保存处理函数的指针值(地址值). sa_mask 和 sa_flags 的所有位均初始化为0即可. 这2个成员用于指定信号相关的选项和特性, 而我们的目的主要是防止产生僵尸进程, 故省略. 理解这些参数所需参考书将在后面给出.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if (sig == SIGALRM)
{
puts("Time out!");
}
alarm(2);
}
int main(int argc, char *argv[])
{
int i;
struct sigaction act;
act.sa_handler = timeout;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGALRM, &act, 0);
alarm(2);
for (i=0; i<3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
运行结果:
这就是信号处理相关理论, 以此为基础讨论消灭僵尸进程的方法.
利用信号处理技术消灭僵尸进程
我相信各位也可以独立编写消灭僵尸进程的示例. 子进程终止时将产生 SIGCHLD 信号, 知道这一点就很容易完成, 接下来利用 sigaction 函数编写示例.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void read_childproc(int sig)
{
int status;
pid_t id = waitpid(-1, &status, WNOHANG);
if (WIFEXITED(status))
{
printf("Removed proc id: %d \n", id);
printf("Child send: %d \n", WEXITSTATUS(status));
}
}
int main(int argc, char *argv[])
{
pid_t pid;
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, 0);
pid = fork();
if (pid == 0) /* 子进程执行区域 */
{
puts("Hi! I'm child process");
sleep(10);
return 12;
}
else /* 父进程执行区域 */
{
printf("Child proc id: %d \n", pid);
pid = fork();
if (pid == 0) /* 另一子进程执行的区域 */
{
puts("Hi! I'm child process");
sleep(10);
exit(24);
}
else
{
int i;
printf("Child proc id: %d \n", pid);
for (i=0; i<5; i++)
{
puts("wait...");
sleep(5);
}
}
}
return 0;
}
运行结果:
可以看出, 子进程并未变成僵尸进程, 而是正常终止了. 接下来利用进程相关知识编写服务器端.
10.4 基于多任务的并发服务器
我们已做好了利用 fork 函数编写并发服务器的准备, 现在可以开始编写像样的服务器端了.
基于进程的并发服务器模型
之前的回声服务器端每次只能向一个客户端提供服务. 因此, 我们将扩展回声服务器端, 使其可以同时向多个客户端提供服务. 图10-2给出了基于多进程的并发回声服务器端的实现模型.
从图10-2可以看出, 每当有客户端请求服务(连接请求)时, 回声服务器端都创建子进程以提供服务, 请求服务的客户端若有5个, 则将创建5个子进程提供服务. 为了完成这些任务, 需要经过如下过程, 这是于之前的回声服务器端的区别所在.
此处容易引起困惑的是子进程传递传递套接字文件描述的方法. 但各位读完代码后会发现, 这其实没什么大不了的, 因为子进程会复制父进程拥有的所有资源. 实际上根本不用另外经过传递文件描述符的过程.
实现并发服务器
虽然我已经给出了所有的理论说明, 但大家也许还没想出具体的实现方法, 这就是有必要理解具体代码. 下面给出并发服务器端的实现代码. 当然, 程序是基于多进程实现的, 可以结合第4章的回声客户端运行.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usage: %s <port> \n", argv[0]);
exit(1);
}
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
{
error_handling("bind() error");
}
if (listen(serv_sock, 5) == -1)
{
error_handling("listen() error");
}
while(1)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if (clnt_sock == -1)
{
continue;
}
else
{
puts("new client connected...");
}
pid = fork();
if (pid == -1)
{
close(clnt_sock);
continue;
}
if (pid == 0) /* 子进程运行区域 */
{
close(serv_sock);
while((str_len=read(clnt_sock, buf, BUF_SIZE)) != 0)
{
write(clnt_sock, buf, str_len);
}
close(clnt_sock);
puts("clitne disconnected...");
return 0;
}
else
{
close(clnt_sock);
}
}
close(serv_sock);
return 0;
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果:
服务器端:
客户端A:
客户端B:
启动服务器端后, 要创建多个客户端并建立连接. 可以验证服务器端同时向大多数客户端提供服务, 不, 一定要验证这一点.
通过 fork 函数复制文件描述符
实例 echo_mpserv.c 中给出了通过 fork 函数复制文件描述符的过程. 父进程将2个套接字(一个是服务器端套接字, 另一个是客户端连接的套接字) 文件描述符复制给子进程.
文件描述符的实际复制有多少有些难以理解. 调用 fork 函数时复制父进程的所有资源, 有些人可能认为也会同时复制套接字. 但套接字并非进程所有–从严格意义上说, 套接字属于操作系统–只是进程拥有代表相应套接字的文件描述符. 也不一定非要这样理解, 仅因为如下原因, 复制套接字也并不合理.
示例echo_mpserv.c 中的 fork函数调用过程如图10 - 3 所示. 调用 fork 函数后, 2个文件描述符指向同一套接字.
如图 10-3 所示, 1个套接字中存在2个文件描述符时, 只有2个文件描述符都终止(销毁)后, 才能销毁套机字. 如果维持图中的连接状态, 即使子进程销毁了与客户端连接的套接字文件描述符, 也无法完全销毁套接字(服务器端套接字同样如此). 因此, 调用 fork 函数后, 要求无关的套接字文件描述符关掉, 如图10-4所示.
为了将文件描述符整理成图10-4的形式, 实例echo_mpserv.c 的第60行和第69行 调用了close 函数.
10.5 分割 TCP 的 I/O 程序
各位应该已经理解 fork 函数相关的所有有用的内容. 下面以此为基础, 再讨论客户端中分割I/O程序 (Routine) 的方法. 内容非常简单, 大家不必有负担.
分割 I/O 程序的优点
我们已实现的回声客户端的数据回声方式如下:
传输数据后需要等待服务器端返回的数据, 因为程序代码中重复调用了 read 和 write 函数, 只能这么写的原因之一是, 程序在一个进程中运行. 但现在可以创建多个进程, 因此可以分割数据收发过程. 默认的分割模型如图10-5所示.
从图10-5可以看出, 客户端的父进程负责接收数据, 额外创建的子进程负责发送数据. 分割后, 不同进程分别负责输入和输出, 这样, 无论客户端是否从服务器接收完数据都可以进行传输.
选择这种实现方式的原因有很多, 但最重要的的一定是, 程序的实现更加简单. 也许有人质疑: 既然多产生1个进程, 怎么能简化程序实现呢? 其实, 按照这种实现方式, 父进程中需编写接收数据的代码, 子进程中需要发送的数据的代码, 所以会简化. 实际上, 在1个进程内同时实现收发逻辑需要考虑更多细节, 程序越复杂, 这种区别越明显, 它也是公认的优点.
分割I/O 程序的另一个好处, 可以提高频率交换数据的程序性能, 如图10-6所示.
如图10-6左测演示的是之前的的回声客户端数据交换方式, 右侧演示的是分割I/O后的客户端数据传递方式. 服务器端相同, 不同的是客户端区域. 分割I/O 后客户端发送的数据时不必考虑接收数据的情况, 因此可以连续发送数据, 由此提高同一时间内传输的数据量. 这种差异在网速较差时尤为明显.
回声客户端的I/O 程序分割
我们已经知道I/O 程序分割的意义, 接下来通过实际代码进行实现, 分割的对象是回声客户端. 下列回声客户端可以结合之前的回声服务器端 echo_mpserv.c 运行.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
int main(int argc, char *argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
{
error_handling("connect() error");
}
pid = fork();
if (pid == 0)
{
write_routine(sock, buf);
}
else
{
read_routine(sock, buf);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void read_routine(int sock, char *buf)
{
while(1)
{
int str_len = read(sock, buf, BUF_SIZE);
if (str_len == 0)
{
return;
}
buf[str_len] = 0;
printf("Message from server: %s", buf);
}
}
void write_routine(int sock, char *buf)
{
while(1)
{
fgets(buf, BUF_SIZE, stdin);
if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR);
return;
}
write(sock, buf, strlen(buf));
}
}
运行结果跟普通回声服务器/客户端相同, 故省略. 只是上述示例分割了I/O, 为了简化输出过程,不会输出如下字符串:
无论是否接收到消息, 每次通过键盘输入字符串时都会输出上述字符串, 可能造成输出混乱. 基于多任务的服务器端实现方法讲解到此结束.
结语:
你可以下面这个网站下载这本书<TCP/IP网络编程>
https://www.jiumodiary.com/
时间: 2020-06-03