网络编程——多进程服务器端

参考

  1. 《TCP/IP网络编程》

多进程服务器端

具有代表性的并发服务器端实现模型和方法:

  1. 多进程服务器:通过创建多个进程提供服务
  2. 多路复用服务器:通过捆绑并同一管理I/O对象提供服务
  3. 多线程服务器:通过生成与客户端等量的线程提供服务

多进程服务器不适合在Windows平台下

进程

进程的定义:占用内存空间的正在运行的程序。

进程ID

所有进程都会从操作系统分配到ID。此ID称为进程ID,其值为大于2的整数。1要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户进程无法得到ID值1

fork函数

fork函数将创建调用的进程副本。即并非完全不同的程序创建进程,而是复制正在运行的、调用fork函数的进程。另外,两个进程都将执行fork函数调用后的语句。但因为通过同一进程、复制相同的内存空间,之后的程序需要根据fork函数的返回值加以区分。即利用fork函数的如下特点区分程序执行流程:

  1. 父进程:fork函数返回子进程ID
  2. 子进程:fork函数返回0
#include <unistd.h>

pid_t fork(void);

成功时返回进程ID,失败时返回-1

示例
#include <stdio.h>
#include <unistd.h>

int gval = 0;
int main(int argc, char* argv[])
{
    pid_t pid;
    int lval = 20;
    gval++;
    lval++;

    pid = fork();
    if (pid == 0)                    // 子进程
    {
        gval += 2;
        lval += 2;
    }
    else                             // 父进程
    {
        gval -= 2;
        lval -= 2;
    }

    if (pid == 0)
    {
        printf("Child Proc: [%d, %d] \n", gval, lval);
    }
    else
    {
        printf("Parent Proc: [%d, %d] \n", gval, lval);
    }
    return 0;
}

僵尸进程

进程完成工作后(执行完main函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作僵尸进程

产生僵尸进程的原因

调用fork函数产生子进程的终止方式:

  1. 传递参数并调用exit函数
  2. main函数中执行return语句并返回值

向exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生子进程的父进程。处在这种状态下的进程就是僵尸进程。所以要销毁僵尸进程,应该向创建子进程的父进程传递子进程的exit参数或return语句的返回值

但操作系统不会主动把这些值传递给父进程,只有父进程主动发起请求(函数调用)时,操作系统才会传递该值。而如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态

创建僵尸进程示例
#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;
}
后台处理

后台处理是指将控制台窗口中的指令放到后台运行的方式。如果以如下方式运行上述示例,则程序将在后台运行(&将触发后台处理):

root@my_linux:/tcpip# ./zombie &

如果采用这种方式运行程序,即可在同一控制台输入其他命令,无需另外打开新控制台

销毁僵尸进程方式1:利用wait函数
#include <sys/wait.h>

pid_t wait(int* statloc);

成功时返回终止的子进程ID,失败时返回-1

调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit函数的参数值,main函数的return返回值)将保存到该函数的参数所指内存空间。但函数参数指向的单元中还包含其他信息,因此需要通过下列宏进行分离:

  1. WIFEXITED:子进程正常终止时返回true
  2. WEXITSTATUS:返回子进程的返回值

所以,向wait函数传递变量status的地址时,调用wait函数后应编写:

if (WIFEXIED(status))              // 是否正常终止
{
    puts("Normal termination!");
    printf("Child pass num: %d", WEXITSTATUS(status));
}
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;
    }
    else
    {
        printf("Child PID: %d \n", pid);
        pid = fork();
        if (pid == 0)
        {
            exit(7);
        }
        else
        {
            printf("Child PID: %d \n", pid);
            wait(&status);
            if (WIFEXITED(status))
            {
                printf("Child send one: %d \n", WEXITSTATUS(status));
            }
            
            wait(&status);
            if (WIFEXITED(status))
            {
                printf("Child send two: %d \n", WEXITSTATUS(status));
            }
            sleep(30);
        }
    }
    return 0;
}

调用wait函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此需谨慎调用该函数

销毁僵尸进程2:使用waitpid函数

调用waitpid函数时,程序不会阻塞

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int* statloc, int options);

成功时返回终止的子进程ID(或0),失败时返回-1
(1)pid
等待终止的目标子进程的ID,若传递-1,则与wait函数相同,可以等待任意子进程终止

(2)statloc
与wait函数的statloc参数具有相同含义

(3)options
传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数

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
    {
        while (!waitpid(-1, &status, WNOHANG))
        {
            sleep(3);
            puts("sleep 3sec.");
        }

        if (WIFEXITED(status))
        {
            printf("Child send %d \n", WEXITSTATUS(status));
        }
    }
    return 0;
}

信号与signal函数

父进程不能只调用waitpid函数以等待子进程终止,当子进程结束时通过操作系统通知父进程将更高效。于是引入信号处理(Signal Handling)机制。此处的“信号”是在特定事件发生时由操作系统向进程发生的消息

通过注册信号,当进程发现自己的子进程结束时,请求操作系统调用特定函数。这一功能可以通过signal函数实现,其在产生信号时调用,返回之前注册的函数指针

#include <signal.h>

void (*signal(int signo, void (*func)(int)))(int);

第一个参数为特殊情况信息,第二个参数为特殊情况下佳宁要调用的函数的地址值。发生第一个参数代表的情况时,调用第二个参数所指的函数

在signal函数中注册的部分特殊情况和对应常数:

  1. SIGALRM:已到通过调用alarm函数注册的时间
  2. SIGINT:输入CTRL+C
  3. SIGCHLD: 子进程终止

例如,完成“子进程终止则调用mychild函数”的请求:

signal(SIGCHLD, mychild);
signal函数示例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

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(int argc, char* argv[])
{
    int i;
    signal(SIGALRM, timeout);
    signal(SIGINT, keycontrol);
    alarm(2);             // 预约2秒后发送SIGALRM信号

    for (i = 0; i < 3; i++)
    {
        puts("wait...");
        sleep(100);
    }
    return 0;
}

其中,alarm函数的介绍如下:

#include <unistd.h>

unsigned int alarm(unsigned int senconds);

返回0或以秒为单位的距SIGALRM信号发生所剩时间。如果调用该函数的同时向它传递一个正整数参数,相应时间后将产生SIGALRM信号。若向该函数传递0,则之前对SIGALRM信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用signal函数)终止进程,不做任何处理

调用信号处理函数的主体的确是操作系统,但进程处于睡眠状态时无法调用函数。因此,产生信号时,为了调用信号处理器,将唤醒由于调用sleep函数而进入阻塞状态的进程,而且,进程一旦被唤醒就不会再进入睡眠状态。即使还未到sleep函数中规定的时间也是如此。所以,上述示例运行不到10秒就会结束

sigaction函数

sigaction函数类似于signal函数,而且完全可以代替后者,也更稳定,因为signal函数在UNIX系列的不同操作系统中可能存在区别,但sigaction函数完全相同

#include <signal.h>

int sigaction(int signo, const struct sigaction* act, struct sigaction* oldact);

成功时返回0,失败时返回-1
(1)signo
与signal函数相同,传递信号信息
(2)act
对应于第一个参数的信号处理函数(信号处理器)信息
(3)oldact
通过此参数获取之前注册的信号处理函数指针,若不需要则传递0

sigaction结构体定义如下:

struct sigaction
{
	void (*sa_handler)(int);
	sigset_t sa_mask;
	int sa_flags;
};

其中,sa_handler保存信号处理函数的指针值。sa_mask和sa_flags的所有位初始化为0即可,这两个成员用于指定信号相关的选项和特性

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);
    act.sa_flags = 0;
    sigaction(SIGALRM, &act, 0);

    alarm(2);

    for (i = 0; i < 3; i++)
    {
        puts("wait...");
        sleep(100);
    }
    return 0;
}
利用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);    // 调用waitpid函数,子进程将正常终止
    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;                        // 注册SIGCHLD信号对应的处理器
    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;
}

基于多任务的并发服务器

扩展之前的回声服务器端,使其可以同时向多个客户端提供服务。每当有客户端请求服务时,回声服务器端都创建子进程以提供服务。为了完成这些任务,需要经过如下过程:

  1. 第一阶段:回声服务器端(父进程)通过调用accept函数受理连接请求
  2. 第二阶段:此时获取的套接字文件描述符创建并传递给子进程
  3. 第三阶段:子进程利用传递来的文件描述符提供服务
实现并发服务器
#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("client 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);
}

注意:调用fork函数时复制父进程的所有资源。但套接字并非进程所有,从严格意义上说,套接字属于操作系统,只是进程拥有代表相应套接字的文件描述符。调用fork函数后,2个文件描述符指向同一套接字,只有2个文件描述符都终止(销毁)后,才能销毁套接字。因此,调用fork函数后,要将无关的套接字文件描述符关掉

分割TCP的I/O程序

对于已实现的回声客户端,传输数据后需要等待服务器端返回的数据,因为程序代码中重复调用了read和write函数。现在可以创建多个进程,分割数据的收发过程

客户端的父进程负责接收数据,额外创建子进程负责发送数据。分割后,不同进程分别负责输入和输出,这样,无论客户端是否从服务器端接收完数据都可以进程传输

#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 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);             // 向服务器端传递EOF
            return;
        }
        write(sock, buf, strlen(buf));
    }
}

void error_handling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值