信号处理机制

实现一个简单的回射程序,客户端输入一行文字,服务器接收并发送这段文字。

//客户端头文件
#ifndef UNP_H
#define UNP_H
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
#include<iostream>

#define MAXLINE 100
#define SERV_PORT 9877
using namespace std;
ssize_t Writen(int fd, const char *vptr, size_t n);
ssize_t Readline(int fd, char *vptr, size_t maxlen);
int Socket(int family,int type,int protocol)
{
    int n;
    if((n=socket(family,type,protocol))<0)
        cout<<"socket error";
    return n;
}
int Connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen)
{
    int n;
    if((n=connect(sockfd,servaddr,addrlen))<0)
        cout<<"connect error";
    return n;

}
void str_cli(FILE* fp,int sockfd)
{
    char sendline[MAXLINE],recvline[MAXLINE];
    while(fgets(sendline,MAXLINE,fp)!=nullptr)
    {
        Writen(sockfd,sendline,strlen(sendline));
        if(Readline(sockfd,recvline,MAXLINE)==0)
            cout<<"read error"<<endl;
        fputs(recvline,stdout);
    }
}
ssize_t Writen(int fd,const char *vptr,size_t n)
{
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
    ptr=vptr;
    nleft=n;
    while(nleft>0)
    {
        if((nwritten=write(fd,ptr,nleft))<=0)
        {
            if(nwritten<0&&errno==EINTR)
                nwritten=0;
            else
                return -1;
        }
        nleft-=nwritten;
        ptr+=nwritten;
    }
    return n;
}
ssize_t Readline(int fd,char *vptr,size_t maxlen)
{
    ssize_t n,rc;
    char c,*ptr;
    ptr=vptr;
    for(n=1;n<maxlen;n++)
    {
        again:
        if((rc=read(fd,&c,1))==1)
        {
            *ptr++=c;
            if(c=='\n')
                break;
        }
        else if(rc==0)
        {
            *ptr=0;
            return n-1;
        }
        else
        {
            if(errno==EINTR)
                goto again;
            return -1;
        }
    }
    *ptr=0;
    return n;
}

#endif // UNP_H
//客户端文件
#include"unp.h"
int main(int argc,char** argv)
{
    int sockfd;
    struct sockaddr_in servaddr;
    if(argc!=2)
        cout<<"error";
    sockfd=Socket(AF_INET,SOCK_STREAM,0);
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_port=htons(SERV_PORT);
    inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
    Connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
    str_cli(stdin,sockfd);
    exit(0);
}
//服务器端头文件
#ifndef UNP_H
#define UNP_H
#define LISTRNQ 100
#define SERV_PORT 9877
#define MAXLINE 100
#include<sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<unistd.h>
#include<iostream>
#include<stdio.h>
using namespace std;
int Socket(int family,int type,int protocol)
{
    int n;
    if((n=socket(family,type,protocol))<0)
        cout<<"socket error";
    return n;
}
void Listen(int fd,int backlog)
{
    char *ptr;
    if((ptr=getenv("LISTENQ"))!=nullptr)
        backlog=atoi(ptr);
    if(listen(fd,backlog)<0)
        cout<<"listen error";
}
ssize_t Writen(int fd,char *vptr,size_t n)
{
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
    ptr=vptr;
    nleft=n;
    while(nleft>0)
    {
        if((nwritten=write(fd,ptr,nleft))<=0)
        {
            if(nwritten<0&&errno==EINTR)
                nwritten=0;
            else
                return -1;
        }
        nleft-=nwritten;
        ptr+=nwritten;
    }
    return n;
}
void str_echo(int sockfd)
{
    ssize_t n;
    char buf[MAXLINE];
    again:
    while((n=read(sockfd,(void*)buf,MAXLINE))>0)
        Writen(sockfd,buf,n);
    if(n<0&&errno==EINTR)
        goto again;
    else if(n<0)
        cout<<"read error";
}
#endif // UNP_H
//服务器端文件
#include"unp.h"

int main(int argc,char** argv)
{
    int listenfd,connfd;
    pid_t childpid;
    socklen_t clilen;
    struct sockaddr_in cliaddr,servaddr;
    listenfd=Socket(AF_INET,SOCK_STREAM,0);
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
    servaddr.sin_port=htons(SERV_PORT);
    bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
    Listen(listenfd,LISTRNQ);
    for(;;)
    {
        clilen=sizeof(cliaddr);
        connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
        if((childpid=fork())==0)
        {
            close(listenfd);
            str_echo(connfd);
            exit(0);
        }
    }
    close(connfd);
}

服务器启动后,调用socket,bind,listen,accept并且阻塞于accept调用等待客户。
客户启动时,调用socket,connect,connect引起TCP三路握手的过程。当三路握手完成后,客户中的connect和服务器的accept均返回,连接于是建立
客户调用str_cli函数,将函数阻塞于fgets调用,等待输入文本
当服务器accept返回时,调用fork,再由子进程调用str_echo。这个函数调用readline,readline调用read,read等待客户程序送入一行文本。
服务器父进程再次调用accept并阻塞,等待下一个客户。

客户接收到三路握手的第二个分节时,connect返回,而服务器要直到接收到三路握手的第三个分节才返回。

使用netstat -a可以查看套接字监听状态
这里写图片描述
我们可以看到服务器端口号为9877,服务器子进程的套接字状态为ESTABLISHED,
55668对应客户进程的本地端口号。

当我们在终端输入ctrl+D(EOF)时,客户端进入TIME_WAIT状态,服务器等待另一个客户连接。这个过程的步骤是:
1 当我们输入EOF时,fgets返回一个空指针,str_cli函数返回到main函数,main调用exit终止。
2 进程终止的处理是关闭打开的描述符,所以TCP客户发送一个FIN给服务器,服务器TCP则以ACK响应。服务器处于CLOSE_WAIT状态,客户套接字处于FIN_WAIT_2状态。
3 当服务器TCP接收到FIN时,服务器子进程阻塞于readline调用,于是readline返回0。这导致str_echo函数返回服务器子进程的main函数,服务器子进程通过exit来终止。
4 服务器子进程中打开的所有描述符关闭,服务器发送FIN分节,客户接收并发送ACK分节,连接终止。客户套接字进入TIME_WAIT状态。
5 当服务器子进程终止时,给父进程发送一个SIGCHLD信号。因为我们没有对这个信号进行处理,所以子进程进入僵尸状态,使用ps -t 查看STAT为Z,我们必须清除僵尸进程。

我们希望当客户正常终止时通过函数处理这个信号,清除掉僵尸进程,所以就要使用POSIX信号处理机制。

POSIX信号处理

**信号就是告知某个进程发生了某个事件,信号通常是异步的,也就是进程预先不知道信号什么时候发生。
信号可以由一个进程发送给另一个进程,也可以由内核发给进程。**

这个SIGCHLD信号就是由内核在子进程终止时发送给它的父进程的信号。

每个信号都有一个处置,我们通过调用sigaction函数来设定一个信号的处置。有三种处理方法:
1 我们可以提供一个信号处理函数,只要有信号发生它就被调用。有两种信号不能被这样的函数处理SIGKILL和SIGSTOP。函数原型:
void handler(int signo);
2 我们可以把某个信号的处置设置为SIG_IGN来忽略它。SIGKILL和SIGSTOP这两个信号不能被忽略。
3 可以把某个信号的处置设置为SIG_DFL 来启用它的默认处置。默认处置通常是收到信号后终止进程,其中某些信号还在当前工作目录产生一个进程的核心映像。

对于大多数的信号,调用sigaction函数并指定信号发生所调用的函数就是捕获信号所需做的全部工作。SIGIO,SIGPOLL和SIGURG这些信号还需要捕获它们的进程做些额外的工作。

signal函数

建立信号处置的posix方法就是调用sigaction函数。还有一个简单的方法是调用signal函数,因为signal函数比posix出现的时间早,所以需要定义一个signal函数,包括sigaction函数。

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;
#endif
}
else
{
#ifdef SA_RESTART
act.sa_flags|=SA_RESTART;
#endif
}
if(sigaction(signo,&act,&oact)<0)
return(SIG_ERR);
return(oact.sa_handler);
}

Sigfunc是由typedef定义的一个函数指针类型,函数原型为:

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

typedef定义为:

typedef void Sigfunc(int);

所以函数定义为:

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

signaction函数原型:

int sigaction(int signo,const struct sigaction *restrict act,struct sigaction *restrict oact);

函数的第二个和第三个参数是指向sigaction结构的指针,该结构原型为:

struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flag;
void (*sa_sigaction)(int,siginfo_t*,void *);
};

sa_handler指向信号处理函数,
sa_mask指定在信号处理函数执行期间需要被屏蔽的信号,当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再发生。
sa_flags成员用于指定信号处理的行为可以选择的参数为:
SA_RESTART 使被信号打断的系统调用自动重新发起
SA_NOCLDSTOP 使父进程在它的子进程暂停或继续运行时不会收到SIGCHLD信号。
SA_NOCLDWAIT 使父进程在它的子进程退出时不会收到SIGCHLD信号,这时子进程如果退出也不会成为僵尸进程
SA_NODEFFER 使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
SA_RESETHAND:信号处理之后重新设置为默认的处理方式
SA_SIGINFO 使用sa_sigaction成员而不是sa_handler作为信号处理函数
有关signal函数和sigaction函数可以参考这个博客
SIGNAL

sigmptyset函数将信号集初始化为空,这里不需要设置阻塞信号。
SIGALRM在进行阻塞式系统调用时,为避免进程陷入无限循环,能够通过安装SIGALRM信号设置定时器;
更多有关SIGALRM信号参考这个博客:
SIGALRM
SA_INTERRUPT//此信号中断的系统调用将不会重启。
SA_RESTART//由相应信号中断的系统调用将由内核自动重启。
在这个例子中,如果被捕获的信号不是SIGALRM且SA_RESTART有定义,那么我们就设置这个标志。如果SIGALRM信号被安装,那么就会自动处理中断的系统调用。

POSIX信号处理:
1 一旦安装了信号处理函数,它便一直安装着
2 在一个信号处理函数运行期间,正被递交的信号是阻塞的,安装处理函数时在传递给sigaction函数的sa_mask信号集中指定的任何额外信号也被阻塞。
3 如果一个信号在被阻塞期间产生了一次或多次,那么该信号被解阻塞之后通常只递交一次

处理SIGCHLD信号

设置僵死状态的目的是维护子进程的信息,以便父进程在以后某个时刻获取。

处理僵死进程
为了不让僵死进程占用内核空间,我们每fork子进程都得wait它们,以防它们变成僵死进程。

//SIGCHLD处理函数该函数在fork子程序之前完成。
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid=wait(&stat);
prinf("child %d terminated\n",pid);
return;
}

在System V和Unix 98标准下,如果一个进程把SIGCHLD设定为SIG_IGN,那么这个进程就不会变成僵死进程。但仅适用于这两个标准下,处理SIGCHLD信号的可移植方法就是调用wait或waitpid

在例子中当SIGCHLD信号递交时,父进程阻塞于accept调用。sig_chld函数执行,其wait调取到子进程的PID和终止状态。
子进程终止返回的SIGCHLD信号由父进程捕获,捕获信号的这段时间正是父进程accept阻塞等待新连接的时间,所以捕获信号会使accept中断。accept中断返回一个EINTER错误,如果父进程不处理这个错误,那么父进程将中止。
在编写捕获信号的网络程序时,我们必须认清被中断的系统调用并且处理它们
有的系统会自动重启被中断的系统调用,标准c函数库中提供的signal函数不会使内核自动重启被中断的系统调用。

处理被中断的系统调用
当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTER错误,有些内核自动重启某些被中断的系统调用。为了便于移植,当我们编写捕获信号的程序时,我们必须对慢系统调用返回EINTER有所准备。
如果accept返回EINTER那么我们可以使用continue函数

for(;;)
{
clien=sizeof(cliaddr);
if((connfd=accept(listenfd,(SA*)&cliaddr,&clilen)<0)
{
if(errno==EINTR)
continue;
else
err_sys("accept error");
}

重启被中断的系统调用。对于accept,read,write,select,open之类的函数是有效的,但是不能重启connect,当connect被一个信号中断而不重启时,我们必须调用select来等待连接完成。

在这个程序中,我们调用wait来处理已终止的子进程。
有两个函数可以处理这些子进程,wait和waitpid,这两个函数返回已终止的子进程的进程id号和通过statloc指针指向的一个进程终止状态。
我们调用三个宏来检查终止状态,并辨别子进程是正常终止,由某个信号杀死还是仅仅由作业控制停止。
有些宏可以获取子进程的退出状态,杀死子进程的信号值或停止子进程的作业控制信号值。
如果调用wait的进程没有已经终止的子进程,那么wait将一直阻塞到现有的第一个子进程终止为止。waitpid可以解决阻塞
waitpid可以指定想要等待进程的id,值为-1时表示等待第一个终止的子进程。
函数原型:

#include<sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);
//成功返回进程ID,出错返回0或-1

pid_t 表示返回的进程id,statloc指针指向终止的状态。
options表示可以指定的选项,最常用的选项是WNOHANG,它告诉内核在没有已终止的子进程时不要阻塞。
wait函数没有指定等待的进程ID当服务器使用fork派生出多个子进程时,如果使用wait将只会处理第一个阻塞的子进程,所以为了处理这个问题要使用waitpid函数所以说信号处理函数要改为:

//函数正确处理accept返回的EINTR,并建立一个给所有已终止子进程调用waitpid信号处理函数。
void sig_chld(int signo)
{
pid_t pid;
int stat;
while((pid=waitpid(-1,&stat,WNOHANG))>0)
printf("child %d terminated\n",pid);
return;
}

总结

总结一下在网络编程中可能遇到的问题:
1 当fork子进程时,必须捕获SIGCHLD信号
2 当捕获信号时,必须处理被中断的系统调用
3 SIGCHLD的信号处理函数应该使用waitpid函数以免留下僵死进程。

总结TCP服务器处理
1 使用socket函数创建监听套接字这里我们创建一个包裹函数其中包括了socket返回错误时的处理
2 使用bzero初始化套接字地址结构
3 我们需要把传送过去的IP地址和端口号由主机字节序转换为网络字节序,这里服务器使用的是通配地址INADDR_ANY,它告诉内核去选择ip地址,端口号使用的是13(发送时间日期端口号)
4 使用bind将套接字地址结构绑定到监听套接字上(客户端也可以使用bind)
5 使用listen函数将一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求,套接字从CLOSE转换为LISTEN状态。第二个套接字规定了内核应该为相应套接字排队的最大连接个数(指的是TCP三路握手没完成和已经完成的总数)。这里使用的是环境变量定义的LISTNQ指定的一个较大的值。
6 接下来就开始处理传送过来的对端协议
7 需要新建一个套接字地址结构,通过accept返回对端的协议地址,返回的内容写入到新建的套接字地址结构中(可以使用inet_ntop函数将传送过来的ip地址的32位二进制格式转换为点分十进制格式,打印出来,对于端口号可以使用ntohs函数),accept函数返回一个已连接套接字(内核会为每个与服务器连接的客户创建一个已连接套接字,当服务器完成对客户的服务时,已连接套接字关闭,与监听套接字最大的区别就是监听套接字在整个服务器生命期都存在.服务器对客户的操作建立在已连接套接字上。)
8 使用fork创建子进程,创建子进程需要关闭监听子进程的监听套接字,对已连接套接字进行处理,并使用exit(0)返回。并且在父进程中只保留监听套接字,关闭已连接套接字,父进程的作用只是监听新到的连接,子进程对客户提供服务
9 客户端终止时发送FIN导致子进程发出SIGCHLD信号传递给父进程,父进程调用sigaction函数或者signal函数处理信号。信号处理函数应该放在父进程accept阻塞之前。当父进程accept阻塞时,如果信号传送过来父进程处理,那么会导致accept被中断,返回EINTR,所以需要对EINTR处理,处理机制是使用continue语句。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值