实现服务器端并发的方法
多进程服务器不适应于Windows,适用于linux平台
cpu核数<进程数时,将启用分时机制
进程ID从2开始,1被分配给OS启动后的首个进程,用于协助OS
ps au 查看正在进程的详细信息
#include
pid_t fork(void);
//成功时返回进程ID,失败时返回-1
fork创建调用它的进程的副本,被创建的副本将从调用fork()语句的后一句开始执行(子进程当前的fork()语句返回值规定为0)
父子进程相当于完全独立的两个进程,互不影响,只是共享同样的代码,只是注意fork的返回值
#include
#include
#include
#include
int main(void){
pid_t pid ;
pid = fork();
if(pid == 0) printf("this is child process\n");
else printf("this is parent process\n");
return 0;
}
产生原因:fork产生的子进程结束后,并未被完全销毁,会留下一个称为僵尸进程(Zombie)的数据结构。这是因为子进程的返回值和状态信息等不能主动传给父进程,因此只能一直存在直到这些值被接收;
僵尸进程几乎不占内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集。
父进程如果未回收僵尸进程,那么父进程结束后,僵尸进程也会被init进程自动回收
#include
#include
#include
#include
int main(void){
pid_t pid ;
pid = fork();
if(pid == 0) printf("this is child process\n");
else{
sleep(30);//在父进程结束之前的这30s内,可以观察到僵尸进程,结尾带有
printf("this is parent process\n");
}
return 0;
}
以后台的方式运行,这样在输入ps au时不用新开终端
root@localhost# ./test &
wait函数
#include
pid_t wait(int* statloc);
//成功时返回子进程ID,失败时返回-1
因为返回值中还包含其他信息,所以需要用下列宏进行分离
WIFEXITED 子迸程正常终止时返回“真”(true)
WEXITSTATUS 返回子进程的返回值
有多少fork,多少子进程,就要多少次wait,如果wait次数多了,将陷入阻塞状态
#include
#include
#include
#include
int main(void){
pid_t pid = fork();
int status;
if(pid == 0) return 3;
else{
pid = fork();
if(pid == 0) return 7;
else{
wait(&status);
if(WIFEXITED(status)) printf("child return one: %d\n", WEXITSTATUS(status));
wait(&status);
if(WIFEXITED(status)) printf("child return one: %d\n", WEXITSTATUS(status));
sleep(30);//这时观察不到僵尸进程了
}
}
return 0;
}
waitpid函数
#include
pid_t waitpid(pid_t pid, int *statloc, int options);
//成功时返回终止的子进程ID(或0),失败时返回-1
参数
pid 等待终止的目标子进程的ID ,若传递-1,则与wait函数相同,可以等待任意子进程终止
options 传递头文件sys/wait.h中声明的常量WNOHANG ,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数
#include
#include
#include
#include
#include
int main(void){
pid_t pid = fork();
int status;
if(pid == 0){
sleep(15);
return 3;
}else{
while(!waitpid(-1, &status, WNOHANG)){
sleep(3);
puts("sleep 3sec.");
}
if(WIFEXITED(status)) printf("child return one: %d\n", WEXITSTATUS(status));
}
return 0;
}
可以看到即便等不到僵尸进程,waitpid也不会阻塞;
while(!wait(&status)){
sleep(3);
puts("sleep 3sec.");
}
问题:父进程不能一直while循环waitpid等待子进程的结束
解决:子进程终止的识别主体是操作系统,因此引入信号处理(Signal Handling)机制
#include
void (*signal(int signo, void (*func)(int)))(int);
//为了在产生信号时调用, 返回之前注册的函数指针
signo表示特殊事件,func是发生该特殊事件要调用的函数。signo特殊事件如下
#include
unsigned int alarm(unsigned int seconds);
//返回0或以秒为单位的距SIGALRM信号发生所剩时间
//调用该函数,seconds秒之后将产生一个SIGALRM信号
#include
#include
#include
#include
void timeout(int sig){
if(sig == SIGALRM) puts("Time out!");
alarm(2);//2秒后再产生一个SIGALRM信号
}
void keycontrol(int sig){
if(sig == SIGINT) puts("CTRL+C pressed");
}
int main(void){
int i;
signal(SIGALRM, timeout);//此时不会执行timeout,signal只是注册而已
signal(SIGINT, keycontrol);//此时不会执行keycontrol,只进行注册
alarm(2);//2秒后产生一个SIGALRM信号
for(i=0; i<3; ++i){
puts("wait...");
sleep(100);
}
/*
结果说明:
1. signal只是向OS注册,不会执行注册的函数,因此运行来到main里面的alarm
2. alarm(2)表示2秒后产生一个SIGALRM信号,在这2秒内,程序继续允许,来到for循环
3. for循环立即输出第一次"wait...",并进入sleep阻塞状态
4. 2秒后,之前的alarm将产生SIGALRM信号,OS捕捉到这一信号后,唤醒进程来调用对应的注册函数,即终止sleep(),因此很快就执行完毕了
*/
return 0;
}
产生信号时,为了调用信号处理器,OS将唤醒主进程,即便未达到sleep()规定的时间
如果使用ctrl+c使用SIGINT的注册函数,也会唤醒进程
可以替换signal,signal 函数在UNIX 系列的不同操作系统中可能存在区别,但sigaction函数完全相同。
#include
int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact);
//成功时返回0,失败时返回-1
act 对应于第一个参数的信号处理函数( 信号处理器) 信息
oldact 通过此参数获取之前注册的信号处理函数指针,若不需要则传递0
struct sigaction{
void (*sa_handler)(int);//对应信号的处理函数
sigset_t sa_mask;//初始化为0,后续再讲
int sa_flags;//初始化为0,后续再讲
}
在CentOS 7环境下vscode中,使用struct sigaction act报错,但报错不影响运行成功,原理未知
#include
//#include
#include
#include
void timeout(int sig){
if(sig == SIGALRM) puts("Time out!");
alarm(2);//2秒后再产生一个SIGALRM信号
}
int main(void){
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;
}
结果和signal一致
int main(void){
pid_t pid = fork();
int status;
if(pid == 0) printf("this is child process\n");
else{
printf("this is parent process\n");
sleep(5);//在这5秒内能观察到僵尸进程
while(!waitpid(-1, &status, WNOHANG));
sleep(10);//此时僵尸进程已被回收
}
return 0;
}
int main(void){
pid_t pid = fork();
int status;
if(pid == 0){
sleep(15);
return 3;
}else{
while(!waitpid(-1, &status, WNOHANG)){
sleep(3);
puts("sleep 3sec.");
}//为了得到子进程的返回值,尽管waitpid不用阻塞,也要循环使其阻塞等待
if(WIFEXITED(status)) printf("child return one: %d\n", WEXITSTATUS(status));
}
return 0;
}
#include
#include
#include
#include
#include
void sig_wait(int sig){
int status;
pid_t pid;
if(sig == SIGCHLD) printf("child process terminates\n");
pid = waitpid(-1, &status, 0);
if(WIFEXITED(status)) printf("child %d return one: %d\n", pid, WEXITSTATUS(status));
}
int main(void){
int status, i;
//注册函数
struct sigaction act;
act.sa_handler = sig_wait;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, 0);
//第1个子进程
pid_t pid = fork();
if(pid == 0){
//第1个子进程运行代码
sleep(5);
return 3;
}else{
//第2个子进程
pid = fork();
if(pid == 0){
//第2个子进程运行代码
sleep(3);
return 7;
}else{
for(i=0; i<5; ++i){
printf("wait...\n");
sleep(100);//在第3秒时返回子进程2,在第5秒时返回子进程1; 当第3次循环时,因为无人唤醒,将阻塞100s
}
}
}
return 0;
}
要求:输入Ctrl+C时询问是否确定退出程序,输入Y则终止程序
#define SIZE 1024
void sig_ctrlc(int sig){
char c;
if(sig == SIGINT)
printf("signal SIGINT\n");
puts("do you want to exit from process?[Y]");
c = getchar();
getchar();//清除掉输入缓冲中的换行符
if(c == 'Y') exit(1);//必须用exit才能退出
else{
printf("returned!\n");
return;
}
}
int main(int argc, char* argv[]){
struct sigaction act;
pid_t pid;
act.sa_handler = sig_ctrlc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
while(1){
sleep(3);
printf("this is main process\n");
}
return 0;
}
每当有客户端请求连接时,服务器端就创建一个子进程来提供服务
查看和杀死端口号
netstat -tunlp|greo 端口号
kill -9 进程ID
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SIZE 1024
void error_handling(char* message){
printf("%s\n", message);
exit(1);
}
void sig_wait(int sig){
pid_t pid;
int status;
pid = waitpid(-1, &status, 0);
printf("******* enter signal SIGCHLD handler ********\n");
if(WIFEXITED(status))
printf("child process %d returned %d\n", pid, WEXITSTATUS(status));
printf("******* leave signal handler ********\n");
}
int main(int argc, char* argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
int clnt_addr_size;
struct sigaction act;
pid_t pid;
int i, read_len;
char buf[SIZE];
//1. socket
if((serv_sock = socket(PF_INET, SOCK_STREAM, 0)) == -1)
error_handling("socker() error");
//2. bind
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[1]));
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
//2.2 设置SO_REUSEADDR避免bind() error
//int option = 1;
//setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));
//3. listen
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
//4. sigaction注册子进程终止处理
act.sa_handler = sig_wait;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, 0);
for(i=0; i<5; ++i){//设置连接数为5,实际可以使用while(1)
//5. accept
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(clnt_sock == -1){
printf("accept() error\n");
continue;
}
//6. fork创建子进程
pid = fork();
if(pid == -1){
close(clnt_sock);
continue;
}
if(pid == 0){
//7.1 进入子进程程序,read和write
close(serv_sock);//子进程也要关闭一次服务器端和客户端的套接字
while(read_len = read(clnt_sock, buf, SIZE)){
if(read_len == -1)
error_handling("read() error");
buf[read_len] = '\0';
printf("server received from client %d is: %s, len=%d\n", clnt_sock, buf, read_len);
write(clnt_sock, buf, read_len);
}
close(clnt_sock);
return 0;
}else{
//7.2 进入父进程程序
close(clnt_sock);//套接字会被复制一份吗?
}
}
close(serv_sock);
return 0;
}
问题:accept() error
每一个子进程结束后,都会出现一个accept error(书上源代码也是如此),原因未明。
#include
#include
#include
#include
#include
#include
#define SIZE 1024
void error_handling(char* message){
printf("%s\n", message);
exit(1);
}
int main(int argc, char* argv[]){
int clnt_sock;
struct sockaddr_in serv_addr;
char buf[SIZE] = "Saturday";
int read_len;
//1. socket
if((clnt_sock = socket(PF_INET, SOCK_STREAM, 0)) == -1)
error_handling("socker() error");
//2. 绑定服务器地址
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[1]));
serv_addr.sin_addr.s_addr = inet_addr(argv[2]);
//3. connect
if(connect(clnt_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error");
//4. read和write
int i;
for(i=0; i<5; ++i){
sleep(2);//每隔2秒发送一次
write(clnt_sock, buf, strlen(buf));
read_len = read(clnt_sock, buf, SIZE);
if(read_len == -1)
error_handling("read() error");
buf[read_len] = '\0';
printf("received from server is: %s, len=%d\n", buf, read_len);
}
close(clnt_sock);
return 0;
}
可以创建一个echo_mpclient2.c,改变buf和sleep的值,更好地观察多进程
使用fork()函数复制进程时,将文件描述符也复制一遍,但并没有复制套接字
父进程和子进程的文件描述符指向同样的通信双方套接字,因此只有在父进程和子进程中都close一遍套接字,才能真正销毁套接字
在前面的程序中,客户端都是阻塞等待服务器端返回数据。在多进程中,可以通过分隔I/O来避免阻塞等待
close和shutdown在多进程中的使用:
close是计数,只有所有进程都close才能关闭套接字;shutdown是直接关闭,只要有一个进程调用,就可以关闭某个方向的套接字
#include
#include
#include
#include
#include
#include
#include
#include
#define SIZE 1024
void write_routine(int sock, char* buf){
while(1){
fgets(buf, SIZE, stdin);
if(!strcmp(buf, "q\n") || !strcmp(buf, "Q\n")) {//strcmp相同返回0
shutdown(sock, SHUT_WR);
return;
}
write(sock, buf, strlen(buf));
}
}
void read_routine(int sock, char* buf){
while(1){
int read_len = read(sock, buf, SIZE);
if(read_len == 0)
return;
buf[read_len] = 0;
printf("received from server is: %s, len=%d\n", buf, read_len);
}
}
int main(int argc, char* argv[]){
int clnt_sock;
struct sockaddr_in clnt_addr, serv_addr;
pid_t pid;
char buf[SIZE];
int read_len;
//1. socket
clnt_sock = socket(PF_INET, SOCK_STREAM, 0);
//2. sock_addr
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[1]));
serv_addr.sin_addr.s_addr = inet_addr(argv[2]);
//3. connect
connect(clnt_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//4. fork
pid = fork();
//5. 子进程write数据
if(pid == 0)
write_routine(clnt_sock, buf);
//6. 父进程read数据
else
read_routine(clnt_sock, buf);
close(clnt_sock);
return 0;
}
运行结果同前面一样
进程拥有独立的空间,哪怕是父子进程也无法直接通信
管道并非是进程的资源,而是和套接字一样属于操作系统,因此不能被fork复制
#include
int pipe(int filedes[2]);
//成功时返回0,失败时返回-1
filedes[0] 通过管道接收数据时使用的文件描述符,即管道出口
filedes[1] 通过管道传输数据时使用的文件描述符,即管道入口。
#include
#include
#include
#define BUF_SIZE 1024
int main(void){
int fds[2];
char str[] = "pipe communication!";
char buf[BUF_SIZE];
pid_t pid;
pipe(fds);
pid = fork();
if(pid == 0){
//子进程
write(fds[1], str, sizeof(str));
}else{
//父进程
read(fds[0], buf, BUF_SIZE);
puts(buf);
}
return 0;
}
#include
#include
#include
#define BUF_SIZE 1024
int main(void){
int fds[2];
char str1[] = "Who are you?";
char str2[] = "Thank you for you message";
char buf[BUF_SIZE];
pid_t pid;
pipe(fds);
pid = fork();
if(pid == 0){
//子进程
write(fds[1], str1, sizeof(str1));
sleep(2);//如果不加这一句,子进程会先读取;导致父进程read阻塞
read(fds[0], buf, BUF_SIZE);
printf("child process received: %s\n", buf);
}else{
//父进程
read(fds[0], buf, BUF_SIZE);
printf("father process received: %s\n", buf);
write(fds[1], str2, sizeof(str2));
sleep(3);//注意父进程不能先比子进程结束,不然导致孤儿进程
}
return 0;
}
问题:在先进先出原则下 ,谁先读取谁就可以获得数据,如上述代码,如子进程先读取,则父进程read阻塞
#include
#include
#include
#define BUF_SIZE 1024
int main(void){
int fds1[2], fds2[2];
char str1[] = "Who are you?";
char str2[] = "Thank you for you message";
char buf[BUF_SIZE];
pid_t pid;
pipe(fds1);//fds1是从子进程到父进程
pipe(fds2);//fds2是从父进程到子进程
pid = fork();
if(pid == 0){
//子进程
write(fds1[1], str1, sizeof(str1));
read(fds2[0], buf, BUF_SIZE);
printf("child process received: %s\n", buf);
}else{
//父进程
read(fds1[0], buf, BUF_SIZE);
printf("father process received: %s\n", buf);
write(fds2[1], str2, sizeof(str2));
sleep(3);//注意父进程不能先比子进程结束,不然导致孤儿进程
}
return 0;
}
要求:用一个新的进程,接受从客户端传来的数据,并保存在文件中
int fds[2];
pipe(fds);
pid = fork();//创建子进程用来保存各个客户端传来的数据
//用于保存数据的子进程
if(pid == 0){
FILE* fp = fopen("ehomsg.txt", "wt");//w表示以二进制写入,wt表示以文本格式写入
char msgbuf[SIZE];
int i, read_len;
for(i=0; i<10; ++i){
read_len = read(fds[0], msgbuf, SIZE);
printf("saved %s\n", msgbuf);
fwrite((void *)msgbuf, 1, read_len, fp);
}
fclose(fp);
return 0;
}
//父进程就像之前那样正常工作,只是增加向管道传送数据的步骤
for(i=0; i<5; ++i){
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(clnt_sock == -1){
printf("accept() error\n");
continue;
}
pid = fork();
if(pid == 0){
close(serv_sock);
while(read_len = read(clnt_sock, buf, SIZE)){
if(read_len == -1)
error_handling("read() error");
buf[read_len] = '\0';
printf("server received from client %d is: %s, len=%d\n", clnt_sock, buf, read_len);
write(clnt_sock, buf, read_len);
//增加:将buf传给管道
write(fds[1], buf, read_len);
}
close(clnt_sock);
return 0;
}else{
close(clnt_sock);
}
}
close(serv_sock);