TCP预先派生子进程服务程序,accept无上锁保护

时间:2022-03-22 18:26:20

okay,今天是我们linux服务器模型的第二篇—TCP预先派生子进程服务程序,accept无上锁保护。

从字面上理解,就是在启动阶段派生一定数量的子进程,当各个客户连接到达时,这些子进程立即就能为他们服务。注意与我们第一篇的不同,我们第一篇是为每一个客户派生一个子进程,来一个,派生一个。另外值得注意的是,如果某个时刻,客户数量正好等于预先派生的子进程,那么对于下一个客户,依然能够执行三次握手,进行连接,只不过得等待到至少有一个子进程可用。

另外,我们的服务器业务还是与第一篇一样,就是根据客户端发的指令判断返回相应数据。

okay,来看我们的代码:
serv.c:

#include "pub.h"

static  int nchildren;
static pid_t *pids;

void sig_int(int signo);

int main(int argc, char **argv)
{
    int listenfd;
    struct sockaddr_in servaddr;
    int on = 1;
    int i = 0;
    pid_t child_make(int, int);

    if(argc != 3)
    {
        fprintf(stderr,"usage: serv <port> <nchildren>\n"); //这里指定端口与预先派生子进程的数目
        exit(-1);
    }

    if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        perror("serv socket error ");
        exit(-1);
    }

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(atoi(argv[1]));
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    //设置可重用
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0){
        perror("serv bind error ");
        exit(-1);
    }


    if(listen(listenfd, LISTENQ) < 0){
        perror("serv listen error ");
        exit(-1);
    }


    nchildren = atoi(argv[2]);
    pids = calloc(nchildren, sizeof(pid_t));

    for(i = 0;i < nchildren; i++)
        pids[i] = child_make(i ,listenfd);

    Signal(SIGINT, sig_int);

    for( ; ; )  //parent just pause
        pause();        //everything done by children

    exit(0);
}


void sig_int(int signo)
{
    int i = 0;
    for(i = 0; i < nchildren; i++){
        kill(pids[i], SIGTERM);
    }

    while(wait(NULL) > 0);  //wait for all children

    //对于wait
    //如果调用的父进程没有子进程则返回-1,并设置errno

    if(errno != ECHILD)
    {
        perror("sig_int error ");
        exit(-1);
    }

    exit(0);
}

关注这里:

for(i = 0;i < nchildren; i++)
        pids[i] = child_make(i ,listenfd);

Signal(SIGINT, sig_int);

for( ; ; )  //parent just pause
    pause();        //everything done by children

这里的nchildren是从我们的参数中提取出来的,代表预先派生的子进程的数量。对于pids数组,用child_make()函数派生子进程,同时将子进程的PID返回给pids数组的某一项。这里的pids是我们动态分配的数组,用来储存我们的各个子进程的PID。这里的child_make()函数是用来派生子进程,同时包括处理的代码。带会我们来讲解它。

看后面的Signal(SIGINT, sig_int);就是注册SIGINT信号的处理函数。我们将在收到SIGINT信号后,将所有派生的子进程kill.

这与后面的for循环以及pause()函数就是为了让父进程挂起。让所有的处理交由预先派生的子进程。

来看我们的sig_int()信号处理函数:

void sig_int(int signo)
{
    int i = 0;
    for(i = 0; i < nchildren; i++){
        kill(pids[i], SIGTERM);
    }

    while(wait(NULL) > 0);  //wait for all children

    //对于wait
    //如果调用的父进程没有子进程则返回-1,并设置errno

    if(errno != ECHILD)
    {
        perror("sig_int error ");
        exit(-1);
    }

    exit(0);
}

一目了然,就是我们之前说的,kill掉所有派生子进程。通过while(wait(NULL) > 0);回收所有的紫禁城的资源。注意我们注释中说的:对于wait,如果调用的父进程没有子进程则返回-1,并设置errno。这个errno就是ECHILD。这并不算错误。

okay,接下来是真正的关于派生子进程的函数。
来看child_make()函数:

#include "pub.h"


pid_t child_make(int i, int listenfd)
{
    pid_t pid;
    void child_main(int, int);

    if((pid = fork()) > 0)//parent just return pid of child
        return pid;
    //child 

    child_main(i ,listenfd);    //nerver return
}

同样很简洁,fork返回如果是大于0,便是子进程的PID,返回即可。留给上面我们讲的pids数组保存。如果是子进程,就进入child_main()函数,这是我们真正进行与客户端连接的函数。
来看一下child_main()函数:

#include "pub.h"


void child_main(int i, int listenfd)
{
    int connfd;
    void child_handle(int, int);
    struct sockaddr_in cliaddr;
    socklen_t len;
    char *buff;

    for( ; ; ){
        len = sizeof(cliaddr);
        if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len)) < 0){
            if(errno == EINTR){
                continue;
            }else{
                perror("the num %d child_main accept error ");
                continue;
            }
        }

        buff = inet_ntoa(cliaddr.sin_addr);
        fprintf(stdout,"%s has been handled\n",buff);
        fflush(stdout);

        child_handle(i, connfd);

        fprintf(stdout,"%s has left\n",buff);
        fflush(stdout);

        close(connfd);
    }

    close(listenfd);
}

不是很难,就是与我们第一篇中的连接差不多。通过一个for循环,等待accept返回,一旦返回并且无错,就先打印某某has been handled,进入child_handle()函数,这是我们处理子进程的业务代码函数,待会再讲。

每当一个子进程处理完业务,就答应某某has left,并关闭connfd,注意我们这个循环并不会退出,永远待命。

再看我们的child_handle()函数:

#include "pub.h"

void child_handle(int i,int sockfd)
{
    char buff[MAXLINE];
    int n;
    time_t mytime;
    for( ; ; ){

        if((n = read(sockfd, buff, MAXLINE)) <= 0){
            if(n < 0 && errno == EINTR)
                continue;
            else if(n == 0){
                //断开连接,交由child_main处理
                break;
            }else{
                perror("child num %d child_handle read error ");
                break;
            }
        }

        //比较前几个字符串,是不是GETTIME,是则返回时间,否则返回 Gettime Command Error
        if((strncmp(buff,"GETTIME", 7) == 0) || 
            (strncmp(buff,"gettime", 7) == 0) )
        {
            mytime = time(NULL);
            snprintf(buff, sizeof(buff), "%s", ctime(&mytime));
            writen(sockfd, buff, strlen(buff));//这里最好用writen(自定义)
        }
        else
        {//不是的话,就返回 Gettime Command Error
            snprintf(buff, sizeof(buff), "Gettime Command Error ");
            writen(sockfd, buff, strlen(buff));
        }


    }


}

这段业务代码与我们之前第一篇的do_child()函数基本一样。比较前几个字符串,是不是GETTIME或者gettime,是则返回时间,否则返回 Gettime Command Error。

其中还包含writen函数,我把它贴出来,就是第一篇中的writen函数,就不讲解了;

#include "pub.h"


int writen(int sockfd,const char *buff, int n)
{
    int nleft = n;
    int ncount = 0;

    const char *ptr = buff;

    while(nleft > 0)
    {
        if((ncount = write(sockfd, ptr, nleft)) <= 0)
        {
            if(errno == EINTR)
                ncount = 0; //call again
            else
                return -1;
        }
        nleft -= ncount;
        ptr += ncount;
    }

    return n - nleft;
}

另外,还有我们的信号处理函数Signal()函数,我也贴出来。

/* include signal */
#include "pub.h"

Sigfunc *
signal(int signo, Sigfunc *func)
{
    struct sigaction    act, oact;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;   /* SunOS 4.x */
#endif
    } else {
#ifdef SA_RESTART
        act.sa_flags |= SA_RESTART;     /* SVR4, 44BSD */
#endif
    }
    if (sigaction(signo, &act, &oact) < 0)
        return(SIG_ERR);
    return(oact.sa_handler);
}
/* end signal */

Sigfunc *
Signal(int signo, Sigfunc *func)    /* for our signal() function */
{
    Sigfunc *sigfunc;

    if ( (sigfunc = signal(signo, func)) == SIG_ERR)
    {
        perror("signal error");
        exit(1);
    }
    return(sigfunc);
}

TCP预先派生子进程服务程序,accept无上锁保护

这是一张可以概括我们今天的服务器模型的图片,注意这里我们的每一个子进程都有listenfd,每个子进程都是通过这个listenfd来进行accept,得到客户端连接。同时,这个listenfd描述符只是一个这个FILE结构体的下标而已。当前有那么多的引用计数。

okay,这就是我们今天的linux服务器模型第二篇–TCP预先派生子进程服务程序,accept无上锁保护。你明白了吗?