目录
2.1 signal函数(旧版本,只适用于旧版本UNIX系统)
2.2 sigaction函数(新版本,所有UNIX操作系统都一样,更稳定)
1. 进程的概念及应用
1.1 什么是进程?
进程:占用内存空间的正在运行的程序。
从操作系统的角度看,进程是程序流的基本单位。若创建多个进程,则操作系统将同时运行。有时一个程序运行过程中也会产生多个进程,多进程服务器端就是其中的代表。
1.2 创建进程
1.2.1 进程ID
每个进程都会从操作系统中分配到一个ID,这个ID就是“进程ID”,进程ID的值为大于2的整数,因为1要分配给操作系统启动后的首个进程(协助操作系统的进程),因此用户无法得到ID为1的进程。
1.2.2 fork函数(创建进程)
#include<unistd.h>
pid_t fork();
成功返回进程ID,失败返回-1
fork函数: 将创建基于当前运行的,调用fork函数的进程的副本(内存空间内容完全相同且互相独立)。此进程为父进程,其副本为子进程。两个进程都将执行fork函数调用后的语句(准确来说,就是从这个fork函数的返回值开始),之后的程序流中,通过fork函数的返回值来区分,当前执行的是子进程还是父进程。fork函数只会复制属于进程的资源,而非操作系统的资源。
如图:
从复制发生点开始,父进程的所有变量的值,在复制发生点处时,是什么值,子进程也就会是什么值,之后两个进程之间的变量值互不影响,如图所示,最终结果:
1.2.3 僵尸进程(为什么要进行进程销毁)
如果在子进程创建和执行了销毁后,父进程没有主动要求获取子进程的结束状态值,那么子进程就会成为僵尸进程,此时的进程只是一个空壳只保留了一些pid,退出状态,运行时间等数据,而其内存空间已经在return和exit的时候释放了,这就是僵尸进程。
1.2.4 wait函数(销毁进程,阻塞)
#include<sys/wait.h>
pid_t wait(int* statloc); //statloc指向的内存空间,存放有子进程的exit参数值或return语句的返回值等其它信息
成功返回终止的子进程ID,失败返回-1
statloc指向的内存空间,不仅仅存放有子进程的exit参数值或return语句的返回值,还有其它信息,需要通过如下宏进行分离:
WIFEXITED(int statue);
子进程正常终止返回1(真)。
WEXITSTATUS(int statue);
返回子进程的返回值。
所以,使用wait函数的标准流程:
int status;
wait(&status);
if(WIFEXITED(status)) //如果正常终止
{
std::cout<<"子进程正常终止!"<<std::endl;
std::cout<<"子进程返回值:"<<WEXITSTATUS(status)<<std::endl;
}
注意:调用wait函数时,如果没有子进程要终止,那么程序将阻塞住,直到有任意一个子进程终止。
1.2.5 waitpid函数(销毁进程,无阻塞)
#include<sys/wait.h>
pid_t waitpid(
pid_t pid, //如果为-1,那么和wait函数一样,等待任意一个子进程终止
int* statloc, //同wait函数一样
int options //传递头文件中声明的常量WNOHANG,表示即使没有子进程终止,
//也不会阻塞程序执行,只是返回0并退出函数
);
成功返回终止的子进程ID(或0),失败返回-1
注意:返回0的情况是没有子进程终止。
1.3 进程间的通信
进程间的通信就是让两个进程间可以交换数据。
1.3.1 通过管道(PIPE)实现进程间的通信
管道与套接字一样是属于操作系统的,而非进程资源,所以在执行fork时,不会被复制。
创建管道的函数:
#include<unistd.h>
int pipe(int filedes[2]);
//filedes[0]:通过管道接收数据时使用的文件描述符,即管道出口
//filedes[1]:通过管道传输数据时使用的文件描述符,即管道入口
成功返回0,失败返回-1
父进程调用该函数创建管道时,会同时获取对应出入口的文件描述符,此时父进程可以读写同一管道。
两进程间管道的双向传输模型:
代码:
#include<iostream>
#include<unistd.h>
int main()
{
int fds[2];
char buf[1024]={"who are you"};
char str[1024]={"Thank you for your message"};
pipe(fds);
pid_t pid=fork();
if(pid==0)
{
write(fds[1],buf,sizeof(buf));
sleep(2);//(1)
char readbuf[1024];
read(fds[0],readbuf,sizeof(readbuf));
std::cout<<"父进程传来的信息:"<<readbuf<<std::endl;
}
else
{
char readbuf[1024];
read(fds[0],readbuf,sizeof(readbuf));
std::cout<<"子进程传来的信息:"<<readbuf<<std::endl;
write(fds[1],str,sizeof(str));
sleep(3);//(2)
}
return 0;
}
由以上问题可知,当只有一个管道时,要进行两个进程间的双向通信,就需要控制好运行的先后顺序,这很复杂甚至因为系统的不同,不大可能实现,所以,我们该怎么进行双向通信?
答:创建2个管道。2个管道分别负责不同的数据流。
修改代码:
#include<iostream>
#include<unistd.h>
int main()
{
int fds_write[2];
int fds_read[2];
char buf[1024]={"who are you"};
char str[1024]={"Thank you for your message"};
pipe(fds_write);
pipe(fds_read);
pid_t pid=fork();
if(pid==0)
{
write(fds_write[1],buf,sizeof(buf)); //子进程向父进程写,管道入口
char readbuf[1024];
read(fds_read[0],readbuf,sizeof(readbuf)); //子进程从父进程读,管道出口
std::cout<<"父进程传来的信息:"<<readbuf<<std::endl;
}
else
{
char readbuf[1024];
read(fds_write[0],readbuf,sizeof(readbuf)); //父进程就从子进程读,管道出口
std::cout<<"子进程传来的信息:"<<readbuf<<std::endl;
write(fds_read[1],str,sizeof(str)); //父进程向子进程写,管道入口
//sleep(3);//写不写都可以
}
return 0;
}
2. 信号处理
将信号处理函数注册给操作系统调用,然后将信号与信号处理函数绑定,接着当触发信号时,操作系统会调用响应的信号处理函数。这种形式有点类似于Qt的connect函数
2.1 signal函数(旧版本,只适用于旧版本UNIX系统)
#include<signal.h>
void (*signal(
int signo,
void (*func)(int)
))(int);
为了在产生信号时调用,返回之前注册的函数指针
signo参数:是要调用的特殊情况信息,也就是信号。
常量 | 含义 |
SIGALRM | 当alarm函数调用的时间到了后,就调用信号处理函数。 注意: 1.如果seconds为0,那么就取消之前注册的信号。 2.如果没有指定该信号对应的处理函数,那么当alarm函数接收后将终止进程。 |
SIGINT | 输出CTRL+C,就调用信号处理函数。 |
SIGCHLD | 子进程终止,就调用信号处理函数。 |
func参数:是一个int型参数返回值为void型的函数指针,也就是信号处理函数。其中的int参数会传入注册时的signo参数。
注意:当信号发生时,会唤醒由于调用sleep函数而进入阻塞状态的进程。(因为:操作系统无法调用进入睡眠状态的进程的函数,所以只能先唤醒这个进程,再调用。)
2.2 sigaction函数(新版本,所有UNIX操作系统都一样,更稳定)
#include<signal.h>
int sigaction(
int signo, //与signal函数相同,传递信号信息
const struct sigaction* act, //对应于第一个参数的信号处理函数(信号处理器)信息
struct sigaction* oldact //通过此参数获取之前注册的信号处理函数指针,不需要则传0
);
成功返回0,失败返回-1
struct sigaction
{
void (*sa_handler)(int); //保存信号处理函数的指针
sigset_t sa_mast; //用于指定信号相关的选项和特性,可以用sigemptyset(&sa_mast)函数来初始化为0。
int sa_flags; //用于指定信号相关的选项和特性,目前初始化为0即可
}
这个函数还有很多可以深究的地方,目前用到的已经可以写一个多进程服务器端了,后续的后面补上。
3. 实现简单的多进程服务端
3.1 代码
#include<iostream>
#include<sys/socket.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<cstring>
#define MAX_SIZE 1024
void childprocesshandle(int sig)
{
std::cout<<"here"<<std::endl;
if(sig==SIGCHLD)
{
int statue;
int childPid=waitpid(-1,&statue,WNOHANG);
if(childPid==0)
{
std::cout<<"没有要结束的子进程"<<std::endl;
return;
}
if(WIFEXITED(statue)) //子进程正常返回
{
std::cout<<"结束的子进程ID:"<<childPid<<std::endl;
std::cout<<"子进程返回的值:"<<WEXITSTATUS(statue)<<std::endl;
}
else
{
std::cout<<"子进程非正常退出"<<std::endl;
}
}
}
int main()
{
int socketfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
if(socketfd==-1)
{
std::cout<<"socket fail!"<<std::endl;
return 0;
}
//这里是复习前面所学的多种套接字选项所写
int sendbufsize;
socklen_t sendlent=sizeof(sendbufsize);
getsockopt(socketfd,SOL_SOCKET,SO_SNDBUF,(void*)&sendbufsize,&sendlent);
std::cout<<"输出缓冲区大小:"<<sendbufsize<<std::endl;
int rcvbufsize;
socklen_t rcvlent=sizeof(rcvbufsize);
getsockopt(socketfd,SOL_SOCKET,SO_RCVBUF,(void*)&rcvbufsize,&rcvlent);
std::cout<<"输入缓冲区大小:"<<rcvbufsize<<std::endl;
int NoTimewait=true;
socklen_t NoTimelen=sizeof(NoTimewait);
setsockopt(socketfd,SOL_SOCKET,SO_REUSEADDR,(const void*)&NoTimewait,NoTimelen);
bool Check=false;
socklen_t timewaitsize=sizeof(Check);
getsockopt(socketfd,SOL_SOCKET,SO_REUSEADDR,(void*)&Check,&timewaitsize);
std::cout<<"Time-wait状态:"<<Check<<std::endl;
sockaddr_in sockAddr;
memset(&sockAddr,0,sizeof(sockAddr));
sockAddr.sin_family=AF_INET;
sockAddr.sin_addr.s_addr=htonl(INADDR_ANY);
sockAddr.sin_port=htons(9130); //固定端口
if(-1==bind(socketfd,(sockaddr*)&sockAddr,sizeof(sockAddr)))
{
std::cout<<"bind fail!"<<std::endl;
}
if(-1==listen(socketfd,5))
{
std::cout<<"listen fail!"<<std::endl;
}
struct sigaction sigstruct;
sigemptyset(&sigstruct.sa_mask);
sigstruct.sa_handler=childprocesshandle;
sigstruct.sa_flags=0;
if(-1==sigaction(SIGCHLD,&sigstruct,0))
{
std::cout<<"sigaction fail!"<<std::endl;
}
while(1)
{
sockaddr_in clientAddr;
memset(&clientAddr,0,sizeof(clientAddr));
socklen_t clientAddrLen=sizeof(clientAddr);
int clientfd=accept(socketfd,(sockaddr*)&clientAddr,&clientAddrLen);
if(clientfd==-1)
{
continue;
}
pid_t pid=fork(); //(1)
if(pid==-1)
{
std::cout<<"fork fail!"<<std::endl;
continue;
}
else if(pid==0)
{
close(socketfd);
std::cout<<"客户端的IP地址:"<<inet_ntoa(clientAddr.sin_addr)<<std::endl;
char readbuf[MAX_SIZE];
int bufsize;
while((bufsize=read(clientfd,readbuf,MAX_SIZE))!=0)
{
std::cout<<"客户端发来的信息:"<<readbuf<<std::endl;
write(clientfd,readbuf,bufsize);
}
close(clientfd);
std::cout<<"here1"<<std::endl;
return 12;
}
else
close(clientfd);
}
close(socketfd);
}
3.2 fork函数复制文件操作符
答:套接字是属于操作系统的,严格意义上来说,没有复制,只是进程会拥有相应套接字的文件描述符。
如图所示,当父进程和子进程都有一个文件描述符指向操作系统的套接字的时候,只有当2个文件描述符都终止后,才能完全销毁套接字。如果是如图的状态:
套接字不会被销毁,所以,在调用fork函数后,要将无关的套接字文件描述符关掉。
3.3 多进程服务器端的缺点
创建进程需要付出极大的代价,大量的运算和内存空间,由于每个进程都拥有独立的内存空间,所以在相互间的数据交换也要求采用相对复杂的方法(IPC属于相对复杂的通信方法,如管道这种方式)。所以如果每有一个客户端就创建一个进程,这是很耗费资源的。
4. 分割TCP的I/O程序(实现多进程客户端的一种模型)
其实就是将客户端的I/O分离开来,父进程负责读,子进程负责写。如图:
答:以回声服务器端和客户端为例,回声客户端在发送完数据后,只能等待服务器端那边将数据传回来之后,才能再次发送,而将I/O分离开来,则无需等待,可多次发送,如图,左边是没分割I/O的回声客户端,右边是分割的。
综上,I/O分割有如下优点:1. 程序实现更简单 2.可以提高频繁交换数据的程序性能。