一、服务器并发访问的问题
1. 服务器按处理方式可以分为迭代服务器和并发服务器两类。
平常用C写的简单Socket客户端服务器通信,服务器每次只能处理一个客户的请求(会阻塞在accpet()处),它实现简单但效率很低,通常这种服务器被称为迭代服务器。然而在实际应用中,不可能让一个服务器长时间 地为一个客户服务,而需要其具有同时处理多个客户请求的能力,这种同时可以处理多个客户请求的服务器称为并发服务器,其 效率很高却实现复杂。在实际应用中,并发服务器应用的广泛。
linux有3种实现并发服务器的方式:
多进程并发服务器,多线程并发服务器,IO复用,先来看多进程并发服务器的实现。
迭代服务器:
二、多进程编程
什么是一个进程?在操作系统原理使用这样的术语来描述的:正在运行的程序及其占用的资源(CPU、内存、系统资源等)叫做进 程。站在程序员的角度来看,我们使用vim编辑生成的C文件叫做源码,源码给程序员来看的但机器不识别,这时我们需要使用 编译器gcc编译生成CPU可识别的二进制可执行程序并保存在存储介质上,这时编译生成的可执行程序只能叫做程序而不能叫进 程。而一旦我们通过命令(./a.out)开始运行时,那正在运行的这个程序及其占用的资源就叫做进程了。进程这个概念是针对系统 而不是针对用户的,对用户来说,他面对的概念是程序。很显然,一个程序可以执行多次,这也意味着多个进程可以执行同一个 程序。 进程空间内存布局 在深入理解Linux下多进程编程之前,我们首先要了解Linux下进程在运行时的内存布局。Linux 进程内存管理的对象都是虚拟 内存,每个进程先天就有 0-4G 的各自互不干涉的虚拟内存空间,0—3G 是用户空间执行用户自己的代码, 高 1GB 的空间是内核 空间执行 Linu x 系统调用,这里存放在整个内核的代码和所有的内核模块,用户所看到和接触的都是该虚拟地址,并不是实际 的物理内存地址。 Linux下一个进程在内存里有三部分的数据,就是”代码段”、”堆栈段”和”数据段”。其实学过汇编语言 的人一定知道,一般的CPU都有上述三种段寄存器,以方便操作系统的运行。这三个部分是构成一个完整的执行序列的必要的部 分。”代码段”,顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用相同的代码段。”堆栈段”存放的就是子程 序的返回地址、子程序的参数以及程序的局部变量和malloc()动态申请内存的地址。而 数据段则存放程序的全局变量,静态变量及常量的内存空间。
下图是Linux下进程的内存布局:
`1.栈 `
栈内存由编译器在程序编译阶段完成,进程的栈空间位于进程用户空间的顶部并且是向下增长,每个函数的每次调用 都会在栈空间中开辟自己的栈空间,函数参数、局部变量、函数返回地址等都会按照先入者为栈顶的顺序压入函数栈中, 函数返回后该函数的栈空间消失,所以函数中返回局部变量的地址都是非法的。
2. 堆。
堆内存是在程序执行过程中分配的,用于存放进程运行中被动态分配的的变量,大小并不固定,堆位于非初始化数据 段和栈之间,并且使用过程中是向栈空间靠近的。当进程调用 malloc 等函数分配内存时,新分配的内存并不是该函数的 栈帧中,而是被动态添加到堆上,此时堆就向高地址扩张;当利用 free 等函数释放内存时,被释放的内存从堆中被踢 出,堆就会缩减。因为动态分配的内存并不在函数栈帧中,所以即使函数返回这段内存也是不会消失。
`4. 非初始化数据段。`
通常将此段称为 bss 段,用来存放未初始化的全局变量和 static 静态变量。并且在程序开始执行之前, 就是在 main()之前,内核会将此段中的数据初始化为 0 或空指针。
5. 初始化数据段。
用来保已初始化的全局变量和 static 静态变量。
6. 文本段也称代码段
这是可执行文件中由 CPU 执行的机器指令部分。正文段常常是只读的,以防止程序由于意外而修改 其自身的执行。 Linux 内存管理的基本思想就是只有在真正访问一个地址的时候才建立这个地址的物理映射,
Linux C/C++语言的分配方式共有 3 种方式:
`(1)从静态存储区域分配。`
就是数据段的内存分配,这段内存在程序编译阶段就已经分配好,在程序的整个运行期间都存在, 例如全局变量,static 变量。
(2)在栈上创建。
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。 栈内存分配运算内置于处理器的指令集中,效率很高,但是系统栈中分配的内存容量有限,比如大额数组就会把栈空间撑爆导致 段错误。
`(3)从堆上分配,亦称动态内存分配。`
程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。此 区域内存分配称之为动态内存分配。动态内存的生存期由我们决定,使用非常灵活,但问题也 多,比如指向某个内存块的指针取值发生了变化又没有其他指针指向 这块内存,这块内存就无法访问,发生内存泄露。
fork()系统调用
Linux内核在启动的后阶段会创建init进程来执行程序/sbin/init,该进程是系统运行的第一个进程,进程号为 1,称为 Linux 系统的初始化进程,该进程会创建其他子进程来启动不同写系统服务,而每个服务又可能创建不同的子进程来执行不同的 程序。所以init进程是所有其他进程的“祖先”,并且它是由Linux内核创建并以root的权限运行,并不能被杀死。Linux下有两个基本的系统调用可以用于创建子进程:fork()和vfork()。在我们编 程的过程中,一个函数调用只有一次返回(return),但由于fork()系统调用会创建一个新的进程,这时它会有两次返回。一次返回 是给父进程,其返回值是子进程的PID(Process ID),第二次返回是给子进程,其返回值为0。所以我们在调用fork()后,需要通 过其返回值来判断当前的代码是在父进程还是子进程运行,如果返回值是0说明现在是子进程在运行,如果返回值>0说明是父进 程在运行,而如果返回值<0的话,说明fork()系统调用出错。
fork 函数调用失败的原因主要有两个:
1. 系统中已经有太多的进程;
2. 该实际用户 ID 的进程总数超过了系统限制。
每个子进程只有一个父进程,并且每个进程都可以通过getpid()获取自己的进程PID,也可以通过getppid()获取父进程的 PID,这样在fork()时返回0给子进程是可取的。一个进程可以创建多个子进程,这样对于父进程而言,他并没有一个API函数可 以获取其子进程的进程ID,所以父进程在通过fork()创建子进程的时候,必须通过返回值的形式告诉父进程其创建的子进程PID。 这也是fork()系统调用两次返回值设计的原因。
三、多进程改写服务器程序
实现代码:
#ifndef __SOCKET_H //防止头文件重复调用
#define __SOCKET_H
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <getopt.h>
#define MSG_STR "Hello LingYun IoT Studio Client\n"
#endif
void print_usage(char *progname)
{
printf("%s usage: \n", progname);
printf("-p(--port): sepcify server listen port.\n");
printf("-h(--Help): print this help information.\n");
return ;
}
int main(int argc, char **argv)
{
int sockfd = -1;
int rv = -1;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t len;
int port = 0;
int clifd;
int ch;
int on = 1;
pid_t pid;
struct option opts[] = {
{"port", required_argument, NULL, 'p'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
while( (ch=getopt_long(argc, argv, "p:h", opts, NULL)) != -1 ) //实现命令行参数解析
{
switch(ch)
{
case 'p':
port=atoi(optarg);
break;
case 'h':
print_usage(argv[0]);
return 0;
}
}
if( !port )
{
print_usage(argv[0]);
return 0;
}
sockfd=socket(AF_INET, SOCK_STREAM, 0); //服务器第一步 socket();
if(sockfd < 0)
{
printf("Create socket failure: %s\n", strerror(errno));
return -1;
}
printf("Create socket[%d] successfully!\n", sockfd);
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); //使得端口号在短时间内可以重复使用
memset(&servaddr, 0, sizeof(servaddr)); //清空servaddr结构体的内存空间
servaddr.sin_family=AF //设置IPV4协议, AF_INET,则是IPV6协议
servaddr.sin_port = htons(port); //将本地字节序的端口转化为网络字节序
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //接受任意ip地址访问
//inet_aton("192.168.0.16", &servaddr.sin_addr); //指定ip地址访问
rv=bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //服务器第二步 ,bind();
if(rv < 0)
{
printf("Socket[%d] bind on port[%d] failure: %s\n", sockfd, port, strerror(errno));
return -2;
}
listen(sockfd, 13); //服务器第三步 ,listen();
printf("Start to listen on port [%d]\n", port);
while(1)
{
printf("Start accept new client incoming...\n");
clifd=accept(sockfd, (struct sockaddr *)&cliaddr, &len); //服务器第四步 ,accept();
if(clifd < 0)
{
printf("Accept new client failure: %s\n", strerror(errno));
continue;
}
printf("Accept new client[%s:%d] successfully\n",
inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
pid = fork(); //fork() 创建进程
if( pid < 0 ) //创建进程出错
{
printf("fork() create child process failure: %s\n", strerror(errno));
close(clifd);
continue;
}
else if( pid > 0 ) //父进程
{
/* Parent process close client fd and goes to accept new socket client again */
close(clifd); //关闭客户端fd,应为父进程只需完成accept(),使accept不要阻塞
continue;
}
else if ( 0 == pid ) //子进程,实现read、write操作,使得read不要阻塞
{
char buf[1024];
/* Child process close the listen socket fd */
close(sockfd);
printf("Child process start to commuicate with socket client...\n");
memset(buf, 0, sizeof(buf));
rv=read(clifd, buf, sizeof(buf)); //服务器第五步之read
if( rv < 0)
{
printf("Read data from client sockfd[%d] failure: %s\n", clifd, strerror(errno));
close(clifd);
exit(0);
}
else if( rv == 0) //客户端断开
{
printf("Socket[%d] get disconnected\n", clifd);
close(clifd);
exit(0);
}
else if( rv > 0 )
{
printf("Read %d bytes data from Server: %s\n", rv, buf);
}
rv=write(clifd, MSG_STR, strlen(MSG_STR)); //服务器第五步之write
if(rv < 0)
{
printf("Write to client by sockfd[%d] failure: %s\n", sockfd, strerror(errno));
close(clifd);
exit(0);
}
sleep(1);
printf("close client socket[%d] and child process exit\n", clifd);
close(clifd);
exit(0);
}
}
close(sockfd);
return 0;
}