代码框架
server.c :
int main()
{
申请 listensocket
while(1)
{
if((connnectfd = accept(...))==0)
{
if((childpid == fork())==0)
{
close(listenfd);
负责通信逻辑
}
else
{
close(connectfd);
}
}
else
{
异常处理
}
}
return 0;
}
关于close
因为子进程会基本上把父进程pcb复制一份,所以所有的标准输入、输出、错误,还有虚拟地址空间和文件描述符表都会复制一份,所以这个时候对应的相文件描述符的引用计数会加1。
这也就意味着如果父进程在fork完不调用close的话,connectfd 会一直存在于文件描述符表,首先它会浪费资源,其次最重要的是服务器端这边逻辑虽然已经处理完,但是并没有真正调用close,这就造成可能对端client已经发了Fin段进入了 FIN_WAIT2状态,但是服务器端一直没有却发FIN端(虽然child 读到了EOF也确实调用了close但是奈何引用计数是2并不能触发连接终止),这就造成了空置的链接会浪费服务器资源。
关于信号处理
首先一个服务器是不能一直阻塞于waitpid 或者 轮询非阻塞于waitpid ,这样都不合适。因为server还需要去接受每一个到来的连接,一直忙于waitpid不太合适。但是不处理僵尸进程又不太合适,所以我们通过SIGCHID异步去处理。
void sig_child(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
if(pid > 0 && (stat & 0x7f)==0)
{
printf("pid %d 's exit code : %d", stat>>8 & 0xff);
}
else if(pid >0 )
{
printf("pid %d sigcode:%d",stat&0x7f);
}
else
{
perror("In Sigchild 's wait");
}
return ;// 这个return 只是为了代表这个函数还是会返回到被中断处
}
wait status变量
EINTR
低速的系统调用被信号会重启,也就是说如果终端I/O或者网络I/O的系统调用被中断了,那么kernel自动重启。但是也并不是所有unix系统都会重启,posix并没有规定必须重启,所以为了移植性,我们可以在用sigaction注册时候的时候,通过 struct sigaction 中的 sa_flags成员变量来设置。
ifdef SA_RESTART
act.sa_flags |= SA_RESTART;
endif
当然也可以通过检查返回值EINTR来重启,大多数Linux的系统调用函数,一般面对EINTR时都应该重启这个系统调用(read/write/open/accept…),因为这个系统调用是被信号中断的,它不是一种错误。所以为了可移植性我们不能够依靠 kernel帮我们默认重启系统调用,我们得自己搞定,所以就好好判断EINTR吧。
其次继续看上面的框架,如果没有正确处理信号中断,kernel也没有自动重启。那么当注册完handler函数后,发送异步中断,这个时候系统调用可能会被中断,那么我们没有正确处理EINTR的话,可能代码逻辑把这个错误当成 Error 或者 Fatal 来处理直接终止程序,那么就坑了。