目录
1--并发服务器端
2--进程
2-1--进程的相关概念
2-2--fork()创建进程
2-3--僵尸进程
2-4--wait()和waitpid()销毁僵尸进程
3--信号处理
3-1--signal()函数
3-2--sigaction()函数
3--3--利用信号处理技术消灭僵尸进程
4--基于多任务的并发服务器
5--分割 TCP 的 I/O 程序
1--并发服务器端
并发服务器端主要有以下三类:
① 多进程服务器:通过创建多个进程提供服务;
② 多路复用服务器:通过捆绑并统一管理I/O对象提供服务;
③ 多线程服务器:通过生成与客户端等量的线程提供服务;
2--进程
2-1--进程的相关概念
进程的相关概念:
① 进程的定义如下:占用内存空间的正在运行的程序;
② 从操作系统的角度看,进程是程序流的基本单位,若创建多个进程,则操作系统将同时运行;
③ 对于 CPU 而言,核的个数与可同时运行的进程数相同;若进程数超过核数,进程将分时使用 CPU 资源;
④ 无论进程是如何创建的,所有进程都会从操作系统中分配到进程 ID,其值为大于 2 的整数;
2-2--fork()创建进程
#include <unistd.h>
pid_t fork(void);
// 成功时返回进程 ID,失败时返回 -1
fork() 函数会复制父进程的进程副本给创建的子进程,两个进程都将执行 fork() 函数调用后的语句;具体执行的内容可根据 fork() 函数的返回值进行区分,对于父进程 fork() 函数返回子进程的进程 ID,对于子进程 fork() 函数返回 0;
// gcc fork.c -o fork
// ./fork
#include <stdio.h>
#include <unistd.h>
int gval = 10;
int main(int argc, char *argv[]){
__pid_t pid;
int lval = 20;
gval++, lval += 5;
pid = fork();
if(pid == 0){ // 对于子进程,fork返回0,因此执行以下内容
gval += 2, lval += 2;
}
else{ // 对于父进程,执行以下内容
gval -= 2, lval -= 2;
}
if(pid == 0){
// 对于子进程,复制父进程的进程副本,则最初 gval = 11, lval = 25;
// 执行 += 2 后,gval = 13, lval = 27;
printf("Child Proc: [%d, %d] \n", gval, lval);
}
else{
// 对于父进程,执行 -= 2后,gval = 9, lval = 23;
printf("Parent Proc: [%d %d] \n", gval, lval);
}
return 0;
}
2-3--僵尸进程
一般进程完成工作后都应被立即销毁,但部分进行由于各种原因导致不能及时销毁,就成为了僵尸进程,其会占用系统中的重要资源;
终止僵尸进程的两种方式:① 传递参数给 exit 函数并调用 exit 函数;② main 函数中执行 return 语句并返回值;
向 exit 函数传递的参数值和 main 函数 return 语句的返回值都会传递给操作系统,而操作系统不会立即销毁子进程,直到把这些返回值传递给父进程;这种不会被立即销毁的子进程就是僵尸进程;
此外,操作系统不会主动将返回值传递给父进程;只有父进程主动发起请求时,操作系统才会将子进程的返回值传递给父进程;因此,如果父进程未主动要求获得子进程的结束状态值,操作系统就不会销毁子进程,子进程就一直处于僵尸进程状态;
// gcc zombie.c -o zombie
// ./zombie
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]){
__pid_t pid = fork();
if(pid == 0){
puts("Hi, I am a child process");
}
else{
// 父进程终止时,子进程也会被同时销毁
// 本案例通过延缓父进程的终止时间,来让子进程进入僵尸进程状态
printf("Child Process ID: %d \n", pid);
sleep(30);
}
if(pid == 0){
puts("End child process");
}
else{
puts("End parent process");
}
return 0;
}
通过 ps au 可以观测到在父进程睡眠的时间里,子进程成为了僵尸进程(Z+状态);
2-4--wait()和waitpid()销毁僵尸进程
为了销毁僵尸子进程,父进程必须主动请求获取子进程的返回值;
父进程调用 wait() 函数 和 waitpid() 函数可以主动获取子进程的返回值;
#include <sys/wait.h>
pid_t wait(int* statloc);
// 成功时返回终止的子进程 ID, 失败时返回 -1;
// 子进程的返回值会保存到 statloc 所指的内存空间
// WIFEXITED() 子进程正常终止时返回 true
// WEXITSTATUS() 返回子进程的返回值
父进程调用 wait() 函数时,如果没有已终止的子进程,则父进程的程序将会阻塞,直至有子进程终止来返回值;
// gcc wait.c -o wait
// ./wait
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv[]){
int status;
__pid_t pid = fork();
if(pid == 0){
return 3; // 第一个子进程返回3
}
else{
printf("Child PID: %d \n", pid); // 第一个子进程的 ID
pid = fork(); // 创建第二个子进程
if(pid == 0){
exit(7); // 第二个子进程返回7
}
else{
printf("Child PID : %d \n", pid); // 第二个子进程的 ID
wait(&status); // 主动请求获取子进程的返回值
if(WIFEXITED(status)){
printf("Chile send one: %d \n", WEXITSTATUS(status));
}
wait(&status); // 主动请求获取子进程的返回值
if(WIFEXITED(status)){
printf("Child send two: %d \n", WEXITSTATUS(status));
}
sleep(30); // 这时候父进程选择睡眠,子进程也不会成为僵尸进程
}
}
return 0;
}
wait() 函数会引起程序阻塞,而 waitpid() 函数不会引起阻塞;
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* statloc, int options);
// 成功时返回终止的子进程的ID(或0),失败时返回-1
// pid 表示等待终止的目标子进程的 ID,传递 -1 时与 wait() 相同,即可以等待任意子进程终止
// statloc 存放子进程返回结果的地址空间
// options 设置为 WNOHANG 时,即使没有终止的子进程,父进程也不会进入阻塞状态,而是返回 0 并结束函数
// gcc waitpid.c -o waitpid
// ./waitpid
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[]){
int status;
__pid_t pid = fork();
if(pid == 0){
sleep(15);
return 24;
}
else{
// 没有终止的子进程时,返回0,则一直循环调用waitpid()
// 直到有终止的子进程来跳出循环
while(!waitpid(-1, &status, WNOHANG)){
sleep(3);
puts("sleep 3sec.");
}
if(WIFEXITED(status)){
printf("Child send %d \n", WEXITSTATUS(status));
}
return 0;
}
}
3--信号处理
上述父进程调用 wait() 函数会阻塞,而调用 waitpid() 函数也必须不断调用(因为不知道子进程何时终止),这也同样会影响父进程的工作效率;
通过信号处理机制,可以解决上述问题;信号表示在特定事件发生时由操作系统向进程发送(通知)的消息;
因此可以通过注册信号,当子进程终止时,让操作系统将子进程终止的消息发送给父进程,这时候父进程才请求获取子进程的返回值;
3-1--signal()函数
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
// 第一个参数 signo 表示特殊情况信息
// 第二个参数表示特殊情况发生后,要调用的函数的地址值(指针)
// 常见特殊情况
// 1. SIGALRM 表示已到调用 alarm 函数注册的时间
// 2. SIGINT 表示遇到了 CTRL+C 的情况
// 3. SIGCHLD 表示遇到了子进程终止的情况
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 返回 0 或以秒为单位的距离 SIGALRM 信号发生的所剩时间(即还剩下多长时间就会发生 SIGALRM 信号时间)
// 经过 seconds 秒后会发生 SIGALRM 信号事件
发生信号事件时,将会唤醒由于调用 sleep 函数而进入阻塞状态的进程;即:即使还没到 sleep 函数规定的事件也会被强制唤醒,而进程一旦唤醒后就不会再进入睡眠状态;
// gcc signal.c -o signal
// ./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); // 不会真的睡眠 100s,因为alarm函数会产生SIGALRM信号事件,从而唤醒进程
}
return 0;
}
3-2--sigaction()函数
sigaction() 函数的功能类似于 signal() 函数,但 sigaction() 更稳定;因为 signal() 函数在不同操作系统中可能存在区别,但 sigaction() 在不同 UNIX 系统中完全相同;
#include <signal.h>
int sigaction(int signo, const struct sigaction* act, struct sigaction* oldact);
// 成功时返回0,失败时返回 -1
// signo 用于传递信号信息
// act 对应于 signo 的信号处理函数
// oldact 获取之前注册的信号处理函数的指针,不用时传递0
struct sigaction{
void (*sa_handler)(int); // 信号处理函数的指针
sigset_t sa_mask; // 初始化为0
int sa_flags; // 初始化为0
}
// gcc sigaction.c -o sigaction
// ./sigaction
#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); // 调用sigemptyset()将sa_mask的所有位初始化为0
act.sa_flags = 0; // sa_flags也初始化为0
sigaction(SIGALRM, &act, 0);
alarm(2);
for(int i = 0; i < 3; i++){
puts("wait...");
sleep(100);
}
return 0;
}
3--3--利用信号处理技术消灭僵尸进程
// gcc remove_zombie.c -o remove_zombie
// ./remove_zombie
#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("Remove 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); // 调用sigaction(),当遇到子线程结束的信号时,调用信号处理函数
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);
return 24;
}
else{
int i;
printf("Child proc id: %d \n", pid);
for(int i = 0; i < 5; i++){
puts("wait...");
sleep(5);
}
}
}
return 0;
}
4--基于多任务的并发服务器
每当有客户端请求服务时,回声服务器端都创建子进程以提供服务;
使用 fork() 创建进程时,子进程会复制父进程拥有的所有资源,因此无需额外传递文件描述符;
// gcc echo_mpserv.c -o echo_mpserv
// ./echo_mpserv 9190
#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){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void read_childproc(int sig){
__pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("remove proc id: %d \n", pid);
}
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); // 创建 tcp socket
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...");
}
// 每当有客户端请求服务时,clnt_sock 不为 -1
// 因此会执行 fork() 函数创建子进程,并由子进程向客户端提供服务
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("client disconnected...");
return 0;
}
else{ // 父进程运行区域
// 因为客户端的文件描述符已经复制到子进程中
// 由子进程处理客户端的内容,因此父进程需要销毁客户端的文件描述符
close(clnt_sock);
}
}
close(serv_sock);
return 0;
}
5--分割 TCP 的 I/O 程序
客户端通过 fork() 创建子进程,将 I/O 分割;客户端的父进程负责接收数据,额外创建的子进程负责发送数据;文章来源:https://www.toymoban.com/news/detail-701752.html
分割后,不同进程分别负责输入和输出,因此客户端是否从服务器端接收完数据都可以进行传输;文章来源地址https://www.toymoban.com/news/detail-701752.html
// gcc echo_mpclient.c -o echo_mpclient
// ./echo_mpclient 127.0.0.1 9190
#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){
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); // 调用 shutdown 函数向服务器端传递 EOF
return;
}
write(sock, buf, strlen(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); // 创建 tcp socket
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(); // 创建子进程实现 I/O 分离
if(pid == 0){ // 子进程写数据到服务器端
write_routine(sock, buf);
}
else{ // 父进程从服务器端中读取数据
read_routine(sock, buf);
}
close(sock);
return 0;
}
到了这里,关于《TCP/IP网络编程》阅读笔记--并发多进程服务端的使用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!