多进程服务器端
具有代表性的并发服务器端实现模型和发:
1、多进程服务器:通过创建多个进程提供服务。
2、多路复用服务器:通过捆绑并统一管理I/O对象提供服务。
3、多线程服务器:通过生成与客户端等量的线程提供服务。
一、进程概念及应用
进程:“占用内存空间的正在运行的程序”。
从操作系统的角度看,进程是程序流的基本单位,若创建多个进程,则操作系统将同时运行。有时一个程序运行过程中也会产生对个进程。接下来要创建的多进程服务器就是其中的代表。编写服务器端前,先了解一下通过程序创建进程的方法。
1、进程ID
无论进程是如何创建的,所有进程都会从操作系统分配到ID,此ID称为“进程ID”,其值是大于2的整数,1要分配给操作系统启动后的首个进程。
通过命令ps au 查看进程信息
2、通过调用fork函数创建进程
创建进程的方法有很多,下面介绍用于创建多进程服务器端的fork函数
#include <unistd.h> /* Clone the calling process, creating an exact copy. Return -1 for errors, 0 to the new process, and the process ID of the new process to the old process. */ extern __pid_t fork (void) __THROWNL;
成功时返回进程ID,失败时返回-1。for函数将创建调用的进程副本,复制正在运行的,调用fork函数的进程。两个进程都将执行fork函数调用后的语句。但由于通过同一个进程、复制相同的内存空间,之后的程序流要根据fork函数的返回值加以区分。
父进程:fork函数返回子进程ID
子进程:fork函数返回0
以时间代码为例
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int gval = 10; 5 int main(int argc, char *argv[]) { 6 pid_t pid; 7 int lval = 20; 8 gval ++, lval += 5; 9 10 pid = fork(); 11 if(pid == 0) 12 gval += 2, lval += 2; 13 else 14 gval -= 2, lval -= 2; 15 if(pid == 0) 16 printf("Child Proc: [%d, %d] \n",gval, lval); 17 else 18 printf("Parent Proc: [%d, %d] \n",gval, lval); 19 return 0; 20 }
运行结果如下:
两个进程将独立运行第十行之后的代码。第十行之前gval和lval的值分别是11和25。
二、进程和僵尸进程
文件操作中,关闭文件和打开文件同等重要。同样,在进程中,销毁进程和创建进程也同等重要,如果为认真对待进程销毁,将有可能变成僵尸进程。
1、僵尸进程
进程完成工作后(执行完main函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源,这中状态下的进程称作“僵尸进程”,这也是给系统带来负担的原因之一。
2、产生僵尸进程的原因
利用如下两个示例展示调用fork函数产生子进程的终止方法:
①传递参数并调用exit函数
②main函数中执行return语句并返回值
想exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统,而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。此僵尸进程该怎么被销毁呢?
“应该向创建子进程的父进程传递子进程的exit参数值或return语句的返回值。”
操作系统不会主动把这些值传递给父进程,只有父进程主动发起请求(调用函数)时,操作系统才会传递该值。如果父进程未主动要求获取子进程的介绍状态值,操作系统将一直保持,并让子进程长时间处于僵尸进程状态。
下面代码将创建僵尸进程:
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main(int agrc, char* argv[]) { 5 pid_t pid = fork(); 6 if(pid == 0) { 7 puts("Hi, I am a child process"); 8 } else { 9 printf("Child Process ID :%d \n", pid); 10 sleep(30); 11 } 12 if(pid == 0) 13 puts("End child process"); 14 else 15 puts("End parent process"); 16 return 0; 17 }
在程序运行后未结束时,可以查看创建的子进程是否被销毁
在运行程序后将创建进程ID为11616的子进程,通过查看进程状态可以知道PID为11616的进程状态为僵尸进程(Z+)。
3、销毁僵尸进程 1:利用wait函数
为了销毁子进程,父进程需要主动请求获取子进程的结束状态值。共2中,其中之一就是调用如下函数:
#include <sys/wait.h> /* Wait for a child to die. When one does, put its status in *STAT_LOC and return its process ID. For errors, return (pid_t) -1. This function is a cancellation point and therefore not marked with __THROW. */ extern __pid_t wait (int *__stat_loc);
成功时返回终止的子进程ID,失败时返回-1。调用此函数,然后有子进程终止,返回值将保存在该函数的参数所指向内存空间,但函数参数执行的单元中还包含其他信息
WIFEXITED子进程正常终止时返回“真” true
WEXITSTATUS返回子进程的返回值
下面的代码将不会产生僵尸进程
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/wait.h> 5 6 int main(int argc, char* argv[]) { 7 int status; 8 pid_t pid = fork(); 9 10 if(pid == 0) return 3; 11 else { 12 printf("Child PID : %d \n", pid); 13 pid = fork(); 14 if(pid == 0) exit(7); 15 else { 16 printf("Child PID: %d \n", pid); 17 wait(&status); 18 if(WIFEXITED(status)) 19 printf("Child send one: %d \n", WEXITSTATUS(status)); 20 21 wait(&status); 22 if(WIFEXITED(status)) { 23 printf("Child send two: %d \n", WEXITSTATUS(status)); 24 } 25 sleep(30); 26 } 27 } 28 return 0; 29 }
通过WIFEXITED(status) 判断是否正常终止,通过WEXITSTATUS(status)获取子进程的返回值。
调用wait函数时,如果没有已终止的子进程,那么程序将阻塞直到有子进程终止,因此需谨慎调用该函数。
4、销毁僵尸进程 2:利用waitpid函数
wait函数会引起程序阻塞,还可以考虑调用waitpid函数,这是防止僵尸进程的第二种方法,也是防止阻塞的方法。
#include <sys/wait.h> /* Wait for a child matching PID to die. If PID is greater than 0, match any process whose process ID is PID. If PID is (pid_t) -1, match any process. If PID is (pid_t) 0, match any process with the same process group as the current process. If PID is less than -1, match any process whose process group is the absolute value of PID. If the WNOHANG bit is set in OPTIONS, and that child is not already dead, return (pid_t) 0. If successful, return PID and store the dead child's status in STAT_LOC. Return (pid_t) -1 for errors. If the WUNTRACED bit is set in OPTIONS, return status for stopped children; otherwise don't. This function is a cancellation point and therefore not marked with __THROW. */ extern __pid_t waitpid (__pid_t __pid, int *__stat_loc, int __options);
成功时返回终止的子进程ID(或0),失败时返回-1.
__pid:等待终止的目标子进程ID,若传递-1,则与wait函数相同,可以等待任意子进程终止。
__stat_loc:与wait函数的__stat_loc参数具有相同的含义。
__options:传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/wait.h> 4 5 int main(int argc, char* argv[]) { 6 int status; 7 pid_t pid = fork(); 8 if(pid == 0) { 9 sleep(15); 10 return 24; 11 } else { 12 while (!waitpid(-1, &status, WNOHANG)) { 13 sleep(3); 14 puts("sleep 3sec."); 15 } 16 if(WIFEXITED(status)) 17 printf("Child send %d \n", WEXITSTATUS(status)); 18 } 19 return 0; 20 }
可以看到第14行执行了五次,这也证明了waitpid函数并未阻塞。
三、信号处理
知道了进程的创建和销毁,但还有一个问题没解决,子进程何时终止,调用waitpid函数后要无休止地等待吗?
子进程终止的识别主体是操作系统,因此,若操作系统能把如下信息告诉正忙与工作的父进程,将有助于构建高效的程序
“嘿,父进程! 你创建的子进程已经终止了!”
此时父进程将暂时放下手头的工作,处理子进程终止的相关事宜。为了实现这个方法,引入了信号处理机制。此处的“信号”是在特定事件发生时由操作系统向进程发送的消息。
1、信号与signal函数
#include <signal.h> /* Type of a signal handler. */ typedef void (*__sighandler_t) (int); /* Set the handler for the signal SIG to HANDLER, returning the old handler, or SIG_ERR on error. By default `signal' has the BSD semantic. */ extern __sighandler_t signal (int __sig, __sighandler_t __handler)
sig:特殊情况消息
__handler:特殊情况下将要调用的函数的地址值
特效情况消息有很多中
/* We define here all the signal names listed in POSIX (1003.1-2008); as of 1003.1-2013, no additional signals have been added by POSIX. We also define here signal names that historically exist in every real-world POSIX variant (e.g. SIGWINCH). Signals in the 1-15 range are defined with their historical numbers. For other signals, we use the BSD numbers. There are two unallocated signal numbers in the 1-31 range: 7 and 29. Signal number 0 is reserved for use as kill(pid, 0), to test whether a process exists without sending it a signal. */ /* ISO C99 signals. */ #define SIGINT 2 /* Interactive attention signal. */ #define SIGILL 4 /* Illegal instruction. */ #define SIGABRT 6 /* Abnormal termination. */ #define SIGFPE 8 /* Erroneous arithmetic operation. */ #define SIGSEGV 11 /* Invalid access to storage. */ #define SIGTERM 15 /* Termination request. */ /* Historical signals specified by POSIX. */ #define SIGHUP 1 /* Hangup. */ #define SIGQUIT 3 /* Quit. */ #define SIGTRAP 5 /* Trace/breakpoint trap. */ #define SIGKILL 9 /* Killed. */ #define SIGBUS 10 /* Bus error. */ #define SIGSYS 12 /* Bad system call. */ #define SIGPIPE 13 /* Broken pipe. */ #define SIGALRM 14 /* Alarm clock. */ /* New(er) POSIX signals (1003.1-2008, 1003.1-2013). */ #define SIGURG 16 /* Urgent data is available at a socket. */ #define SIGSTOP 17 /* Stop, unblockable. */ #define SIGTSTP 18 /* Keyboard stop. */ #define SIGCONT 19 /* Continue. */ #define SIGCHLD 20 /* Child terminated or stopped. */ #define SIGTTIN 21 /* Background read from control terminal. */ #define SIGTTOU 22 /* Background write to control terminal. */ #define SIGPOLL 23 /* Pollable event occurred (System V). */ #define SIGXCPU 24 /* CPU time limit exceeded. */ #define SIGXFSZ 25 /* File size limit exceeded. */ #define SIGVTALRM 26 /* Virtual timer expired. */ #define SIGPROF 27 /* Profiling timer expired. */ #define SIGUSR1 30 /* User-defined signal 1. */ #define SIGUSR2 31 /* User-defined signal 2. */ /* Nonstandard signals found in all modern POSIX systems (including both BSD and Linux). */ #define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
SIGALRM:已到通过调用alarm函数注册的时间。
SIGINT:输出CTRL+C
SIGCHLD:子进程终止
下面两种就是信号注册过程,注册好信号后,发送信号时,操作系统将调用该信号对应的函数。
signal(SIGALRM, timeout);
signal(SIGINT, keycontrol);
使用alarm函数验证,
#include <unistd.h> /* Schedule an alarm. In SECONDS seconds, the process will get a SIGALRM. If SECONDS is zero, any currently scheduled alarm will be cancelled. The function returns the number of seconds remaining until the last alarm scheduled would have signaled, or zero if there wasn't one. There is no return value to indicate an error, but you can set `errno' to 0 and check its value after calling `alarm', and this might tell you. The signal may come late due to processor scheduling. */ extern unsigned int alarm (unsigned int __seconds) __THROW;
返回0或者以秒为单位的距SIGALRM信号发送所剩事件。
在第一个等待中我按了CTRL+C,而且在等待过程中实际没有睡眠100s,在产生信号时,为了调用信号处理器,将唤醒由于调用sleep函数而进入阻塞状态的进程。而进程一旦被唤醒,就不会再进入睡眠状态了。
2、利用siganction函数进行信号处理
signal函数在UNIX系列的不同操作系统中可能存在区别,但sigaction函数完全相同。
#include <signal.h> /* Get and/or set the action for signal SIG. */ extern int sigaction (int __sig, const struct sigaction *__restrict __act, struct sigaction *__restrict __oact) __THROW;
sigaction结构体如下:
typedef void (*__sighandler_t) (int); struct sigaction { /* Signal handler. */ #if defined __USE_POSIX199309 || defined __USE_XOPEN_EXTENDED union { /* Used if SA_SIGINFO is not set. */ __sighandler_t sa_handler; /* Used if SA_SIGINFO is set. */ void (*sa_sigaction) (int, siginfo_t *, void *); } __sigaction_handler; # define sa_handler __sigaction_handler.sa_handler # define sa_sigaction __sigaction_handler.sa_sigaction #else __sighandler_t sa_handler; #endif /* Additional set of signals to be blocked. */ __sigset_t sa_mask; /* Special flags. */ int sa_flags; /* Restore handler. */ void (*sa_restorer) (void); };
sa_handler成员保存信号处理函数的指针值,sa_mask和sa_flags的所有位均初始化为0即可。可以使用sigemptyset函数将sa_mask成员的所有位初始化为0
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <signal.h> 4 5 void timeout(int sig) { 6 if(sig == SIGALRM) 7 puts("Time out!"); 8 alarm(2); 9 } 10 int main(int argc, char *argv[]) { 11 int i; 12 struct sigaction act; 13 act.sa_handler = timeout; 14 sigemptyset(&act.sa_mask); 15 act.sa_flags = 0; 16 sigaction(SIGALRM, &act, 0); 17 alarm(2); 18 for(i = 0; i < 3; i ++) { 19 puts("wait..."); 20 sleep(100); 21 } 22 return 0; 23 }
3、利用信号处理技术消灭僵尸进程
子进程在终止时会产生SIGCHLD信号,通过这点,利用信号处理机制就能消灭僵尸进程了。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 #include <sys/wait.h> 6 7 void read_childproc(int sig) { 8 int status; 9 pid_t id = waitpid(-1, &status, WNOHANG); 10 if(WIFEXITED(status)) { 11 printf("Removed proc id: %d \n",id); 12 printf("Child send: %d \n",WEXITSTATUS(status)); 13 } 14 } 15 16 int main(int argc, char *argv[]) { 17 pid_t pid; 18 struct sigaction act; 19 act.sa_handler = read_childproc; 20 sigemptyset(&act.sa_mask); 21 act.sa_flags = 0; 22 sigaction(SIGCHLD, &act, 0); 23 24 pid = fork(); 25 if(pid == 0) { 26 puts("Hi! I'm child process"); 27 sleep(10); 28 return 12; 29 } else { 30 printf("Child proc id: %d \n", pid); 31 pid = fork(); 32 if(pid == 0) { 33 puts("Hi! I'm child process"); 34 sleep(9); //如果睡眠十秒的话有可能只消灭一个僵尸进程,然后两个进程同学调用一个函数 (猜的!!!) 35 exit(24); 36 } else{ 37 int i; 38 printf("Child proc id: %d \n", pid); 39 for(i = 0; i < 5; i ++) { 40 puts("wait..."); 41 sleep(5); 42 } 43 } 44 } 45 return 0; 46 }
可以看出,子进程并未变成僵尸进程。
四、基于多任务的并发服务器
1、基于进程的并发服务器模型
并发服务器模型使其可以同时多个客户端提供服务。
第一阶段:回声服务器(父进程)通过调用accept函数受理连接请求。
第二阶段:此时获取的套接字文件描述符创建并传递给子进程。
第三阶段:子进程利用传递来的文件描述符提供服务。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <signal.h> 6 #include <sys/socket.h> 7 #include <sys/wait.h> 8 #include <arpa/inet.h> 9 10 #define BUF_SIZE 30 11 void error_handling(const char *message); 12 void read_childproc(int sig); 13 14 int main(int argc, char *argv[]) { 15 int serv_sock, clnt_sock; 16 struct sockaddr_in serv_adr, clnt_adr; 17 18 pid_t pid; 19 struct sigaction act; 20 socklen_t addr_sz; 21 int str_len,state; 22 char buf[BUF_SIZE]; 23 if(argc != 2) { 24 printf("Usage: %s <port> \n",argv[0]); 25 exit(1); 26 } 27 28 act.sa_handler = read_childproc; 29 sigemptyset(&act.sa_mask); 30 act.sa_flags = 0; 31 state = sigaction(SIGCHLD, &act, 0); 32 serv_sock = socket(AF_INET, SOCK_STREAM, 0); 33 memset(&serv_adr, 0, sizeof(serv_adr)); 34 serv_adr.sin_family = AF_INET; 35 serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 36 serv_adr.sin_port = htons(atoi(argv[1])); 37 38 if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) 39 error_handling("bin() error"); 40 41 if(listen(serv_sock, 5) == -1) 42 error_handling("listen() error"); 43 44 while (1) { 45 addr_sz = sizeof(clnt_adr); 46 clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &addr_sz); 47 if(clnt_sock == -1) 48 continue; 49 else 50 puts("new client connected..."); 51 pid = fork(); 52 if(pid == -1) { 53 close(clnt_sock); 54 continue; 55 } 56 if(pid == 0) { 57 close(serv_sock); 58 while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0) 59 write(clnt_sock, buf, str_len); 60 close(clnt_sock); 61 puts("client disconnected"); 62 return 0; 63 } else 64 close(clnt_sock); 65 } 66 close(serv_sock); 67 return 0; 68 } 69 void read_childproc(int sig) { 70 pid_t pid; 71 int status; 72 pid = waitpid(-1, &status, WNOHANG); 73 printf("remove proc id: %d \n", pid); 74 } 75 76 void error_handling(const char *message) { 77 fputs(message, stderr); 78 fputc('\n', stderr); 79 exit(1); 80 }
五、分割TCP的I/O程序
1、分割TCP的I/O程序的优点
向服务器端传输数据,并等待服务器端回复。无条件等待,直到接收服务器端的回声数据后,才能传输下一批数据。
使用两个进程,一个用来接收数据,一个用来发送数据。可以提高频繁交换数据的程序性能。
1 #include <stdlib.h> 2 #include <stdio.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <sys/socket.h> 6 #include <arpa/inet.h> 7 8 #define BUF_SIZE 30 9 void error_handling(const char *message); 10 void read_routine(int sock, char *buf); 11 void write_routine(int sock, char *buf); 12 13 int main(int argc, char *argv[]) { 14 int sock; 15 pid_t pid; 16 char buf[BUF_SIZE]; 17 struct sockaddr_in serv_adr; 18 if(argc != 3) { 19 printf("Usage : %s <IP> <port> \n", argv[0]); 20 exit(1); 21 } 22 23 sock = socket(AF_INET, SOCK_STREAM, 0); 24 memset(&serv_adr, 0, sizeof(serv_adr)); 25 serv_adr.sin_family = AF_INET; 26 serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 27 serv_adr.sin_port = htons(atoi(argv[2])); 28 29 if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) 30 error_handling("connect() error"); 31 32 pid = fork(); 33 if(pid == 0) 34 write_routine(sock, buf); 35 else 36 read_routine(sock, buf); 37 close(sock); 38 return 0; 39 } 40 41 void read_routine(int sock, char *buf) { 42 while (1) { 43 int str_len = read(sock, buf, BUF_SIZE); 44 if(str_len == 0) return; 45 46 buf[str_len] = 0; 47 printf("Message from server : %s ", buf); 48 } 49 } 50 51 void write_routine(int sock, char *buf) { 52 while (1) { 53 fgets(buf, BUF_SIZE, stdin); 54 if(!strcmp(buf, "q\n") || !strcmp(buf, "Q\n")) { 55 shutdown(sock, SHUT_WR); 56 return; 57 } 58 write(sock, buf, strlen(buf)); 59 } 60 } 61 62 void error_handling(const char *message) { 63 fputs(message, stderr); 64 fputc('\n', stderr); 65 exit(1); 66 }