阁下在阅读本奏章时,请先阅读tcp/ip低层通信说明,不然疑惑在心,影响大脑神经^^
先解释以下知识点:
进程:process是一个运行中的程序,它的管制由内核【系统】来完成,创建,销毁,调度。
进程凭证:每个进程都有自己的进程标识,不然一堆进程我怎么知道哪个进程是什么,进程标识是一个整形数字,同时每个进程拥有自己的所属用户和组标识,以便知道这个进程是哪个用户及组运行的。
进程在运行的时候会在操作系统【unix,linux/proc目录】下生成一个proc/PID的目录【win我不知道了,我不喜欢win,阁下知道可指点本人^^】,操作系统有多个运行中的进程,其中有一些特别的进程如init进程它的进程号一般为1,是所有进程的祖先进程,这个进程是不会死的,除非你浇水了^_^【当然有的系统如阿里云的服务器的可能进程名称不是init】
在这里你可以查看到当前进程的详细信息,并且你可以读取这些文件,你可以读取status可以查看进程的详细信息,如进程id,pid,进程名称,状态,有效用户和真实用户及组等数据,同时你应该看到fd目录,它是进程的文件列表,linux它把一些设备映射成文件了
针对输入设备对应的是0【比如你要获取键盘输入的数据,一般来说驱动程序是编写好键盘的驱动程序来读取的,输出设备是1【如果你用write函数可以直接write(1,data,size),当然了这个函数仅在c语言下使用,php不可以的,php的STDOUT它对应的内容是fopen('php://stdout', 'w'),一个进程启动后默认会打开这3个文件【所以你在fd目录会下会看到0,1,2,xx这些文件,它们就是一个个的文件描述符,用数字表示,0就是输入,早期就是这样玩,后来为了方便就起了个绰号叫stdin,stdout,stderr。
PS:关于进程组,会话,控制终端,进程组长,会话首领,守护进程以及进程的控制,进程信号,包括进程调度请自行google
一般情况下我们编写一个服务器程序是这样的
int sockfd = socket();
struct sockaddr_in address;
address.sin_port = 端口号
address.sin_addr = ip
bind(sockfd,address);
listen();
int connfd = accept();
recv(connfd);
首先创建一个套接字,然后绑定好ip和端口【为什么要绑定,因为当这个进程运行时,它创建好的sockfd会放在进程的【文件列表】里【在哪里查看啊,就是上面讲过的proc/PID进程号/fd目录下创建,当然里面默认有3个了即0,1,2,你再创建就从3开始,所以你去打开linux下的proc/PID你的服务器进程号/fd下看,它即sockfd已经关联了接收缓冲区,发送缓冲区,等待队列等数据,并且人家还绑定了ip和端口】
当该进程运行到accept或是recv时它立马阻塞。
阻塞:
操作系统一般是多个任务运行的,就是多个进程运行,每个进程的运行次序,时间由操作系统控制,采用一种时分技术,不断的从队列里取出,运行,这个时候由于运行到recv,但是现在没有数据啊【所以当前进程会放在等待队列里面,然后cpu运行其它进程了,这时候我们叫阻塞】,因为网卡还没有接收到数据,当网卡接受到数据时【想想怎么接受的,接受后放在哪个位置上】,此时它立马产生一个中断请求。
中断:
当cpu在运行主程序时,如果硬件产生了一个中断请求,cpu会立马停止正在运行的主程序,并且跳转到中断程序,并运行中断程序,因为这是硬件中断,优先级最高,会先运行【如果撸过单片机或是微机应该了解】,当网卡接受,它会把接受的数据写入内存,当然首先它会指定某一块存储单元即先通过地址总线寻找到一块地址单元,然后再通过数据总线把数据写入内存,控制总线的指令将是写入内存操作【可以去了解下cpu和内存的一个运行情况】
唤醒进程:
此时网卡接受到数据并写入内存,然后响应中断程序以后,就会根据端口号把数据写入对应的socket同时唤醒等待队列中的进程,同时网卡接受的数据由于含有ip和端口号,所以系统会找到端口号对应的sockfd文件描述符【这就是你为什么要绑定端口的理由,不然它找不到对应的sockfd】。
以上就是recv到数据后的一个简单说明。
IO模型之多路复用:
linux它提供了select,poll,epoll等多路利用的接口,当然有一些开源库如libevent它做了许多封装工作【后面有机会再说】
上面那个recv它只能监听一个客户端,并且整个进程在没有数据到达时,它就阻塞了,啥也没有做了,所以我们监听多个文件描述符,这个时候就用select。
int fd[sock1,sock2,sock3...];
ret = select(...fd...);
你没有看错,它是个文件描述集,select调用后,内核会线性轮询哪些sockfd就绪了就是该文件描述符产生的各种事件,当其中sock1接受到数据以后,select会立马返回,当然是返回集合,所以你要自己遍历,判断哪个是已经就绪的,你再进行读写操作。
当然这种方式效率不高,因为当连接数量一多,这fd集合越来越大,性能就慢慢下降了,并且它默认可以打开的【该进程能打开的文件数量】最多是1024个在linux下,即使你去修改也没有用,它性能依然低下,毕竟它是线性表,采用这样的数据结构使它无法支持很大的并发量。所以你懂的你在linux下玩workerman它默认用的就是select,除非你装了libevent。
听说【我只听说^_^】apache采用的就是select,nginx采用的是epoll。
为了提升性能,后来别人提出了增加版本的epoll
int epollfd = epoll_create();//创建一个epoll_event poll对象 内部采用非常平衡的二叉排序树来存放它们叫红黑树
因为二叉排序树有时候插入或是删除操作要遍历深度太多,所以搞成平衡的二叉排序树,减少遍历次数,使之性能提高,具体可以去看看二叉排序树,平衡树AVL,再看红黑树【首先先撸一下二叉树^_^】
采用这种数据结构存储每个sockfd文件描述符,在插入或是删除时性能都能提升,比线性表遍历次数要少许多了,这就是为什么人家要玩算法的原因,毕竟操作系统整个内核就是算法
epoll_event pool它还有一个就绪列表成员
epoll_ctl(epollfd,sockfd)
for(;;){
num = epoll_wait(epollfd,events,...)
}
当网卡接受到数据cpu响应中断程序后,它并不会立马唤醒当前的进程,而是将sock直接引用到该对象的rdlist就绪列表成员里【它是一个链表,有的是双向链表】所以你可以去看看线性表的链式存储结构怎么玩
然后最唤醒进程直接返回就绪的文件描述符列表
关于它的详情内容请自行查找资料!!!
Epoll如果是LT模式则是轮询模式,ET同是回调事件模式,它的算法时间复杂度为O(1)性能最高
下面是使用php的函数编写的简易多进程服务器
<?php
/**
* Created by PhpStorm.
* User: 1655664358@qq.com
* Date: 2018/8/7
* Time: 13:26
*/
class Server
{
public $_context;
const DEFAULT_BACKLOG=1024;
public $socket;
public $pidS = [];
public $readFds = [];
public $writeFds = [];
public $exceptions = [];
public function run()
{
umask(0);
if (!posix_setsid()){
throw new RuntimeException("setsid error");
}
$pid = pcntl_fork();
if ($pid>0){
exit(0);
}
cli_set_process_title("jackcsm");
$context_option['socket']['backlog'] = static::DEFAULT_BACKLOG;
$this->_context = stream_context_create($context_option);
stream_context_set_option($this->_context, 'socket', 'so_reuseport', 1);
$this->socket = stream_socket_server("tcp://0.0.0.0:12345",$errno,$error);
stream_set_blocking($this->socket,0);
$this->readFds[] = $this->socket;
for ($i=0;$i<4;$i++){
$this->worker();
}
}
function worker()
{
$pid = pcntl_fork();
if ($pid>0){
$this->pidS[$pid] = $pid;
}else if ($pid==0){
srand();
mt_srand();
while (1){
$reads = $this->readFds;
$writes = $this->writeFds;
$exceptions = $this->exceptions;
set_error_handler(function(){});
$ret = stream_select($reads,$writes,$exceptions,1024);
restore_error_handler();
if (!$ret){
continue;
}
if ($reads){
foreach ($reads as $fd){
if ($fd == $this->socket){
set_error_handler(function(){});
$connect = stream_socket_accept($this->socket,0,$remoteAddr);
stream_set_blocking($connect, 0);
restore_error_handler();
//stream_set_blocking($connect,0);
$this->readFds[] = $connect;
$this->writeFds[] = $connect;
$this->exceptions[] = $connect;
}else{
$data = fread($fd,10240);
if ($data==''){
foreach ($reads as $k=>$fdNode){
if ($k == $fd){
unset($this->readFds[$k]);
fclose($fd);
}
}
foreach ($writes as $k=>$fdNode){
if ($k == $fd){
unset($this->writeFds[$k]);
fclose($fd);
}
}
}else{
file_put_contents("jackcsm.log",$data);
//echo "来自客户端的数据:".$data.PHP_EOL;
}
// $pid = posix_getpid().posix_getppid();
//
// fwrite($fd,"<html>hello,world--$data--$pid</html>",1024);
// foreach ($reads as $k=>$fdNode){
// if ($k == $fd){
// unset($this->readFds[$k]);
// }
// }
// foreach ($writes as $k=>$fdNode){
// if ($k == $fd){
// unset($this->writeFds[$k]);
// }
// }
}
}
}
if ($writes){
foreach ($writes as $fd) {
if ($fd!=$this->socket){
set_error_handler(function(){});
fwrite($fd,"hello,world",20);
restore_error_handler();
foreach ($reads as $k=>$fdNode){
if ($k == $fd){
unset($this->readFds[$k]);
}
}
foreach ($writes as $k=>$fdNode){
if ($k == $fd){
unset($this->writeFds[$k]);
}
}
fclose($fd);
}
}
}
}
}
}
}
$pid = pcntl_fork();
if($pid==-1){
throw new RuntimeException("fork error");
exit(0);
}else if($pid==0){
(new Server())->run();
}else{
exit("exit");
}
它监听的是12345端口,你可以使用http,websocket或是telent客户端去连接它。
可以自行去运行代码,不过只支持在linux下pcntl扩展PHP官方不支持在win下,当然win是支持多进程的【你在win上装docker,虚拟机也可以】
另外你也可以查看我相关的注解workerman框架注解
© 著作权归作者所有
发表评论