1.引言
如果你还不了解Linux下的Socket编程,可见花几分钟看看这篇文章,很详细,很经典!文章代码如有困难,可以联系博主~~
Linux Socket编程入门——浅显易懂
2.多进程编程
2.1 进程的概述
使用多进程并发服务器时要考虑以下几点:
- 父进程最大文件描述符个数(父进程中需要close关闭、accept返回的新的文件描述符)
- 系统内创建进程个数(与内存大小有关)
- 进程创建过多是否降低整体服务性能(进程调度)
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
一个多进程实例(理解一下多进程):
#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid; //fpid表示fork函数返回的值
int count=0;
fpid=fork(); //建立子进程
if (fpid < 0)
printf("error in fork!");
else if (fpid == 0) {
printf("i am the child process, my process id is %d/n",getpid());
printf("我是爹的儿子/n");//对某些人来说中文看着更直白。
count++;
}
else {
printf("i am the parent process, my process id is %d/n",getpid());
printf("我是孩子他爹/n");
count++;
}
printf("统计结果是: %d/n",count);
return 0;
}
运行结果是:
i am the child process, my process id is 5574
我是爹的儿子
统计结果是: 1
i am the parent process, my process id is 5573
我是孩子他爹
统计结果是: 1
在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)……
为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID(>0) 。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
2.2 僵尸进程
在linux系统中,当用ps命令观察进程的执行状态时,经常看到某些进程的状态栏为defunct,这就是所谓的“僵尸”进程。“僵尸”进程是一个早已死亡的进程,但在进程表(processs table)中仍占了一个位置(slot)。由于进程表的容量是有限的,所以,defunct进程不仅占用系统的内存资源,影响系统的性能,而且如果其数目太多,还会导致系统瘫痪。
当子进程退出的时候,内核会向父进程发送SIGCHLD信号,父进程可以选择忽略这个信号。子进程的退出是个异步事件(子进程可以在父进程运行的任何时刻终止)。 子进程退出时,内核将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态。
如上可知,僵尸进程一旦出现之后,很难自己消亡,会一直存在下去,直至系统重启。虽然僵尸进程几乎不占系统资源,但是,这样下去,数量太多了之后,终究会给系统带来其他的影响。因此,如果一旦见到僵尸进程,我们就要将其杀掉。如何杀掉僵尸进程呢?
- 方法一:杀掉僵尸进程的父进程,这样僵尸进程就变成孤儿进程,从而init进程将代替父进程来接手,负责清除这个孤儿进程。
- 方法二:重启电脑
杀掉的方法其实效果并不好,好的办法是预防僵尸进程的产生。
- 通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核负责回收僵尸进程。但这样就无法获得进程退出状态。
- 通过捕捉SIGCHLD信号,调用waitpid()函数来处理僵尸进程。
2.3 捕捉信号、调用waitpid函数回收
信号的注册、捕捉:
#include <signal.h>
int sigaction(int signum, //捕捉的信号
const struct sigaction *act, //捕捉到信号后的行为
struct sigaction *oldact); //一般为NULL
struct sigaction {
void (*sa_handler)(int); //处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); //一般不会用
sigset_t sa_mask; //在信号处理函数执行过程中,临时屏蔽掉指定信号。如果没有需要,清空该操作就行。
int sa_flags; //默认为0,选择sa_handler指针
//void (*sa_restorer)(void);
};
回收函数:waitpid()函数原型如下:
#include <sys/types.h>/<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
调用一次waitpid(),只能回收一个进程。通常情况下,用循环来回收。
参数:
- pid:
pid == -1: 回收所有的子进程
pid > 0 :某个子进程的pid
pid == 0 :回收当前进程组的所有子进程
pid < -1 :将不在同一进程组的指定pid进程进行回收,也就是等待进程组号为pid绝对值的任何子进程。 - status:这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会退出,是正常推出还是出了什么错误。当然,如果不关心子进程为什么推出的话,也可以传入空指针。
- options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起等待(设置非阻塞)。如果设置为0,就是阻塞。
返回值:
- -1:回收失败,没有子进程
- >0:返回被回收的子进程的pid
- 如果为非阻塞,
=0:子进程处于运行状态
3. 代码实例
PS:这里要注意一下accept()函数的返回值处理,因为如果一个子进程结束、或者其他信号的错误,会导致accept()函数出错。这里主要注意:ECONNABORTED、EINTR这两个返回错误信息。
代码目标:多个客户端去连接一个服务端,服务端将接收到的小写字母转换为大写字母,再传回对应的客户端。
/***
Server端
Author:Liang jie
objective:服务端将客户端输入的小写转换为大写,再传回客户端。
*/
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#define SRV_PORT 10005
#define BUFIZE 4096
//进程的回收函数
void catch_child(int signum){
while(waitpid(0,NULL,WNOHANG)>0); //非阻塞回收
return;
}
int main(int argc,char *argv[] ){
int lfd,cfd;
pid_t pid;
int ret,i;
char buf[BUFIZE];
struct sockaddr_in srv_addr,clt_addr;
socklen_t clt_addr_len;
bzero(&srv_addr,sizeof(srv_addr)); //清空地址结构
srv_addr.sin_family=AF_INET;
srv_addr.sin_port=htons(SRV_PORT);
srv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd<0)
{
printf("socket creation failed\n");
exit(-1);
}
if(bind(lfd,(struct sockaddr *)&srv_addr,sizeof(srv_addr))==-1)
{
printf("Bind error.\n");
exit(-1);
}
if(listen(lfd,128)==-1)
{
printf("Listen error!\n");
exit(-1);
}
printf("Start to listen!\n");
clt_addr_len=sizeof(clt_addr);
while(1)
{
/*accept()函数,这里要说明一下,因为accept函数可能被信号量终止,
也就是说,accept函数可能并没有出错,只是因为其他信号量出了问题。
所以这里增加了对accept函数返回值的处理,以防被后面进程的信号量打断*/
again:
if ((cfd=accept(lfd,(struct sockaddr *)&clt_addr,&clt_addr_len))<0)
{
if((errno==ECONNABORTED) || (errno==EINTR))
goto again;
else{
printf("accept error!\n");
exit(-1);
}
}
//创建子进程
pid=fork();
//如果创建失败
if(pid<0){
printf("fork error");
exit(-1);
}
//如果是子进程
else if(pid==0){
close(lfd);
break;
}
//如果是父进程,则需要回收已经结束的子进程
else{
struct sigaction act;
act.sa_handler=catch_child; //信号响应函数
sigemptyset(&act.sa_mask); //清空该操作
act.sa_flags=0; //选择第一种函数指针 sa_handler
ret=sigaction(SIGCHLD,&act,NULL); //注册信号,需要捕捉的信号:SIGCHLD。act是捕捉到该信号后的反应
if (ret!=0){
printf("error");
exit(-1);
}
close(cfd);
continue;
}
}
//如果是子进程,(写在While循环里面显得太臃肿,所以就把子进程写在外边)
if (pid==0){
for(;;){
ret = read(cfd,buf,sizeof(buf));
if (ret==0){ //检测到客户端关闭
close(cfd);
exit(1);
}
//小写转大写
for(i=0;i<ret;i++)
buf[i]=toupper(buf[i]);
write(cfd,buf,ret); //写回客户端
write(STDOUT_FILENO,buf,ret); //写到屏幕上
printf("\n");
}
}
return 0;
}
Client端和上一篇文章一样,不用改动。
点击,这里是文章地址~~
4.总结
文章从进程的概念开始讲解,到创建子进程,再到创建子进程经常会出现的僵尸进程、如何解决僵尸进程、以及accept()函数的返回值处理。
下一篇文章将会讲解多线程下的socket编程。
文章代码如有困难,可以联系博主~~