JavenLaw

热爱可抵岁月漫长,坚持可解道阻且长

Skynet源码之:监视器(10)

monitor初始化 1,首先让我们看看监视器monitor在那里调用 监视器monitor不像 服务管理handle_storage,消息队列message_queue,模块加载modules 等具有复杂的结构,它的功能和流程比较简单。因此没有特别的函数用于监视器monitor的初始化。像服务管理有skynet_handle_init(),消息队列有skynet_mq_init(),模块加载有skynet_module_init(),定时器模块也有属于自己的skynet_timer_init(),网络模块则有skynet_socket_init() 因此监视器monitor的初始化,就发生在被第一次调用时才进行。在skynet_start.c文件,第 186 行 // config->thread 又是通过skynet_main.c 中读取启动配置获得的 // thread == config->thread // 因此 thread 就是启动配置的数值 struct monitor *m = skynet_malloc(sizeof(*m)); // 申请内存 memset(m, 0, sizeof(*m)); // 把内存都置为0 m->count = thread; // 把开启的worker线程数记录在count m->sleep = 0; // 初始值为0 m->m = skynet_malloc(thread * sizeof(struct skynet_monitor *)); // 很显然,老做法了 // 申请一段内存,实际就是一个数组,数组大小为需要监控的线程数量 // 数组的元素是什么呢?是指针 // 指针的类型是什么呢?指向 struct skynet_monitor 结构体的地址 int i; for (i=0;i<thread;i++) { m->m[i] = skynet_monitor_new(); // 最后为每个数组分配一个 指向struct skynet_monitor 结构体的指针 } 让我们来看看struct monitor结构体是什么?
2023-09-14 阅 读 全 文

Skynet源码之:定时器(9)

定时器结构 1,首先看看是在哪里第一次调用 在skynet_start.c文件中,第275行,进行初始化skynet_timer_init(),代码如下: void skynet_timer_init(void) { TI = timer_create_timer(); // 创建timer并赋值给TI uint32_t current = 0; systime(&TI->starttime, &current); // starttime,就是以系统时间为标准,取当时的1秒记录 TI->current = current; // current,就是以当时取得1秒为起点,以10ms为单位,过了多少个滴答 TI->current_point = gettime(); } 接着看看什么是TI static struct timer * TI = NULL; // 声明了一个全局单例TI指针,TI指针指向一个struct timer结构体的地址,并标明static属性 看看具体的struct timer结构体是怎么样的 struct timer { struct link_list near[TIME_NEAR]; // TIME_NEAR == 256 struct link_list t[4][TIME_LEVEL]; // TIME_LEVEL == 64 struct spinlock lock; // 自旋锁 uint32_t time; uint32_t starttime; // 初始化时间的 开始日期点,单位为1秒 uint64_t current; // 从初始化时间到现在,总共度过了多少滴答 uint64_t current_point; // 上一次系统所处的时间点 }; // 可以看到 // near 是个结构体数组,数组的元素都是struct link_list结构体,数组大小是256 // t 是个二级数组,一共分为4组,每个组大小是64,数组的元素都是struct link_list结构体 ​
2023-08-12 阅 读 全 文

Skynet源码之:模块加载(8)

skynet_module结构体 1,让我们看看首先在哪里调用的skynet_module_init(config->module_path) 在skynet_start.c文件中,第274行,skynet_module_init(config->module_path) void skynet_module_init(const char *path) { struct modules *m = skynet_malloc(sizeof(*m)); // 分配内存 m->count = 0; m->path = skynet_strdup(path); // 实际就是把 config->path 复制给 m->path SPIN_INIT(m) M = m; } static struct modules * M = NULL; // 声明的全局单例M 看看 modules 结构 struct modules { int count; // 模块的数量 struct spinlock lock; // 自旋锁 const char * path; // 模块的路径 struct skynet_module m[MAX_MODULE_TYPE]; // 模块结构数量 MAX_MODULE_TYPE == 32 }; 再看看 skynet_module 结构体
2023-08-06 阅 读 全 文

Skynet源码之:消息队列(7)

全局消息队列 1,看下消息队列首先被调用的地方 在skynet_start.c文件中,第273行被调用skynet_mq_init(),代码如下: void skynet_mq_init() { struct global_queue *q = skynet_malloc(sizeof(*q)); // 分配内存给global_queue结构体 memset(q, 0, sizeof(*q)); // 初始化全局消息队列,把所有的数据置为0 SPIN_INIT(q); // 初始化自旋锁 Q = q; // 赋值给全局单列Q } 其中Q是skynet_mq.c中定义的global_queue结构体,代码如下: static struct global_queue *Q = NULL; // static 表示Q指针这个全局变量只对定义在同一文件中的函数可见 那么global_queue结构体的定义是什么呢? struct global_queue { struct message_queue *head; // 指向全局消息队列中,头部的消息 struct message_queue *tail; // 指向全局消息队列中,尾部的消息 struct spinlock lock; // 自旋锁结构定义 }; 该如何理解这个global_queue结构体的作用呢? 假设你管理了200个班级,并且每个班级的人数都不相同,此时你要批改200个班级所有学生的作业,你会如何安排每个班级的作业批改呢? 这个global_queue结构体就是用来管理这200个班级的批改记录 struct message_queue *head用来记录现在你批改到第几个班级,struct message_queue *tail用来记录最后一个班级是哪个 刚开始的时候,head记录的是第1个班级,tail记录的是第100个班级。随着作业的批改,head记录的是第50个班级 在批改作业的时候剩余班级的作业也都交了上来,tail记录变为最后第200个班级 global_queue结构体通过记录全局消息队列的头部和尾部来达到控制全局消息队列的目的,但其本身不属于全局消息队列 ​ 全局消息队列:入队和出队 2,既然是队列,那自然就是有进队列和出队列的操作,全局消息队列的操作实现如下:
2023-07-25 阅 读 全 文

Skynet源码之:服务管理(6)

在开始介绍服务管理的代码之前,我想解释一下:为什么我们称它为服务管理,而实际代码文件命名却是handle.c?在Skynet中有众多的的服务ctx,这个ctx是个复杂的结构,而如何管理众多的服务ctx是我们需要考虑的问题。最简单的办法就是给这些服务ctx进行编号,我们根据编号来查找,增加,删除,修改这些服务 而这些handle就是每个服务ctx的编号,handle和服务ctx唯一对应,能通过handle找到服务ctx这个结构的所有信息,handle像一个名单,管理着所有的服务ctx因此我们称handle.c模块为服务管理 ​ 理解handle_storage结构体 1,让我们看看首先在哪里调用的skynet_handle_init()代码 在skynet_start.c文件中,第272行,调用skynet_handle_init(config->harbor),代码如下: // 用于记录 服务名字 和 服务handle 的映射关系 struct handle_name { char * name; // 服务名字 uint32_t handle; // 服务handle }; // H的定义:H是一个指针,指向的地址存储着handle_storage这个结构体 static struct handle_storage *H = NULL; // 全局单例 void skynet_handle_init(int harbor) { assert(H==NULL); struct handle_storage * s = skynet_malloc(sizeof(*H)); s->slot_size = DEFAULT_SLOT_SIZE; // DEFAULT_SLOT_SIZE == 4 s->slot = skynet_malloc(s->slot_size * sizeof(struct skynet_context *)); memset(s->slot, 0, s->slot_size * sizeof(struct skynet_context *)); rwlock_init(&s->lock); // reserve 0 for system s->harbor = (uint32_t) (harbor & 0xff) << HANDLE_REMOTE_SHIFT; // HANDLE_REMOTE_SHIFT == 24 // harbor == 16777216,二进制为 1000000000000000000000000 s->handle_index = 1; s->name_cap = 2; s->name_count = 0; s->name = skynet_malloc(s->name_cap * sizeof(struct handle_name)); H = s; // 赋值给全局单例H // Don't need to free H } 那handle_storage的结构又是怎么样的呢?
2023-07-02 阅 读 全 文

Skynet源码之:节点建立(5)

节点的历史和优化 关于节点的知识,我们首先应该了解其历史和优化过程 Skynet开源时间:2012年8月1日 ​ 文章时间:2012年8月6日 看看云风对Skynet节点最初的设计思路 引用自:云风的 BLOG: Skynet 集群及 RPC (codingnow.com) 先谈谈集群的设计 最终,我们希望整个 skynet 系统可以部署到多台物理机上。这样,单进程的 skynet 节点是不够满足需求的。我希望 skynet 单节点是围绕单进程运作的,这样服务间才可以以接近零成本的交换数据。这样,进程和进程间(通常部署到不同的物理机上)通讯就做成一个比较外围的设置就好了 为了定位方便,我希望整个系统里,所有服务节点都有唯一 id 。那么最简单的方案就是限制有限的机器数量、同时设置中心服务器来协调。我用 32bit 的 id 来标识 skynet 上的服务节点。其中高 8 位是机器标识,低 24 位是同一台机器上的服务节点 id 。我们用简单的判断算法就可以知道一个 id 是远程 id 还是本地 id (只需要比较高 8 位就可以了) 我设计了一台 master 中心服务器用来同步机器信息。把每个 skynet 进程上用于和其他机器通讯的部件称为 Harbor 。每个 skynet 进程有一个 harbor id 为 1 到 255 (保留 0 给系统内部用)。在每个 skynet 进程启动时,向 master 机器汇报自己的 harbor id 。一旦冲突,则禁止连入 master 服务其实就是一个简单的内存 key-value 数据库。数字 key 对应的 value 正是 harbor 的通讯地址。另外,支持了拥有全局名字的服务,也依靠 master 机器同步。比如,你可以从某台 skynet 节点注册一个叫 DATABASE 的服务,它只要将 DATABASE 和节点 id 的对应关系通知 master 机器,就可以依靠 master 机器同步给所有注册入网络的 skynet 节点
2023-06-17 阅 读 全 文

Skynet源码之:守护进程(4)

守护进程,顾名思义就是在操作系统后台运行Skynet 守护进程相关知识:搞懂进程组、会话、控制终端关系,才能明白守护进程如何创建 - 知乎 (zhihu.com) ​ Skynet中的守护进程 在文件skynet_start.c文件,第266行中,有守护进程的代码: if (config->daemon) { if (daemon_init(config->daemon)) { exit(1); } } // daemon()守护进程是否开启是可以在config->daemon中配置的 // config->daemon 其实就是一个文件路径 + 文件名,例如:./path/skynet.pid 让我们看看 daemon_init(config->daemon) 做了什么操作: int daemon_init(const char *pidfile) { // 检查skynet的pid // 具体实现在 代码段1 int pid = check_pid(pidfile); // 如果返回的不是0,标识这个文件已经存在,则代表着skynet进程已经启动了 if (pid) { fprintf(stderr, "Skynet is already running, pid = %d.\n", pid); return 1; // skynet进程已启动,直接退出 } #ifdef __APPLE__ // 检测代码是否在苹果(Apple)操作系统上运行 fprintf(stderr, "'daemon' is deprecated: first deprecated in OS X 10.
2023-06-09 阅 读 全 文

Skynet源码之:环境准备(3)

Skynet环境准备的内容主要分为以下几方面 1,全局初始化函数:skynet_globalinit(),用于记录服务的总数量、初始化线程并设置信息 2,环境初始化函数:skynet_env_init(),建立env Lua虚拟机,用于存储配置信息 3,使用temp Lua虚拟机对配置文件进行解析、读取 4,对Skynet进程进行信号设置 5,luaL_initcodecache()对服务间Lua代码共享做一些设置 6,把temp Lua虚拟机中的配置,传递给 env Lua虚拟机 和 skynet_config结构体中 ​ 第一,全局初始化做了什么 skynet_globalinit() 函数在 skynet_main.c 文件中,第128行开始调用 static struct skynet_node G_NODE; // 全局单例 struct skynet_node { // 声明一个原子字段,用于记录服务的总数量 // 每当一个服务创建、退出都会改变这个字段 // ATOM_INT是Skynet封装的一个原子操作的对象,详细请看《Skynet专题之:原子操作》 ATOM_INT total; int init; // 标记是否完成初始化 uint32_t monitor_exit; // 标记监视器线程是否退出 pthread_key_t handle_key; // 声明线程私有数据,详细请看《Skynet专题之:线程》 bool profile; // default is on }; void skynet_globalinit(void) { // 初始化原子操作 // ATOM_INIT()是Skynet封装的一个原子操作的函数,详细请看《Skynet专题之:原子操作》 ATOM_INIT(&G_NODE.total, 0); G_NODE.monitor_exit = 0; G_NODE.init = 1; // 创建线程私有数据 // pthread_key_create() 用于创建一个多线程私有数据,详细请看《Skynet专题之:线程》 if (pthread_key_create(&G_NODE.
2023-05-10 阅 读 全 文

Skynet源码准备(2)

Skynet源码准备 参考资料 在阅读源码中,会涉及一些C/C++,Lua的知识,一些网站能比较方便查阅资料 1,C++ 参考手册 - C++中文 - API参考文档 (apiref.com) 2,C语言 - 参考手册 - C语言 - API参考文档 (apiref.com) 3,Lua 5.4 Reference Manual - contents ​ 工具准备 VirtualBox 官网:[Oracle VM VirtualBox](https://www.vmware.com/cn.html) 理由:VirtualBox的功能已经足够使用,而且免费,并且比VMware小 ​ CentOS 阿里:[阿里巴巴开源镜像站](https://developer.aliyun.com/mirror/) https://mirrors.aliyun.com/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-Minimal-2207-02.iso 腾讯:腾讯软件源 (tencent.com) https://mirrors.cloud.tencent.com/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-Minimal-2207-02.iso 网易:欢迎访问网易开源镜像站 (163.com) http://mirrors.163.com/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-Minimal-2207-02.iso 清华:清华大学开源软件镜像站 | Tsinghua Open Source Mirror https://mirror.tuna.tsinghua.edu.cn/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-Minimal-2207-02.iso 理由:CentOS是比较常用的Linux服务器系统,已经用习惯了 ​ Terminal 安装:Microsoft Store 配置:CentOS 理由:功能强大,好用,免费,在Windows Store上直接安装使用 ​ Samba 官网:Samba - opening windows to a wider world 作用:用于Linux和Windows之间的文件共享 安装:yum install samba
2023-05-07 阅 读 全 文

Skynet框架分解(1)

Skynet框架分解 这部分不关涉到代码,但依然希望在进行Skynet框架学习之前,希望你能先阅读以下资料,你会有个大概的认知。 ​ Skynet的WIKI:Home · cloudwu/skynet Wiki (github.com) ​ Skynet的入门:GettingStarted · cloudwu/skynet Wiki (github.com) ​ Skynet的设计:云风的 BLOG: Skynet 设计综述 (codingnow.com) ​ 基础知识 Skynet最重要的特点就是把不同的业务分为相互隔离的独立服务,服务之间再通过消息相互合作,最后加上单进程多线程的模式。因此要完全释放Skynet能力的关键就在于:如何设计服务和线程的分配关系,即如何充分利用CPU核心来并行运作数千个相互独立的业务。事实上Skynet很多的优化都是围绕着这点在做。例如有4种不同的线程,其中Woker线程的数量一般要求比实际的CPU核心多一点;每个Woker线程根据权重选择不同数量的消息处理,以提高效率;各种锁的选用也是考虑服务的实际情况…… ​ 如何利用有限数量的线程,满足成千上万的服务,这就是Skynet核心解决的问题。只要我们设计的好,那我们才算是充分利用好了Skynet。因此,在开始前我们需要了解计算机基础的知识:核心、进程、线程、协程。了解这些基础知识后,我们才知道如何去组织设计我们的服务,如何编写我们的代码最高效,以及如何优化现有的代码。 ​ 核心 核心为一个物理CPU拥有的运算单元数量,例如8核,即一个CPU有8个核心,拥有8个独立的运算单元。一个运算单元有一套寄存器,可以独立执行一个线程。所以一个核心同一时刻只能运行一个线程,不论这个线程是哪个进程的。每隔一定时间,大概几十毫秒,就会切换线程,即切换任务。(可能是同一个进程的线程,也可能是另外一个进程的线程) ​ 进程 操作系统分配资源的最小单位(包括CPU核心、内存、磁盘IO 等)。一个进程可以拥有多个线程。操作系统是抢占式的,因此每个进程内的线程都需要抢占核心运行。当然,运行一定时间后也会被操作系统停止,让出核心给其他线程执行。 ​ 线程(内核级线程) 线程是核心调度和分配的基本单位。一个进程下的线程共享该进程的资源和程序代码。我们把线程准确称为内核级线程。 ​ 协程(用户级线程) 由用户程序实现的线程,内核执行权切换是由用户态程序自己协商切换的,不需要内核的干涉。我们把协程准确称为用户级线程。 ​ 线程和协程的区别 线程的概念刚开始出现,但操作系统厂商可不能直接就去修改操作系统的内核,因为对他们来说,稳定性是最重要的。 贸然把未经验证的东西加入内核,出问题了怎么办?所以想要验证线程的可用性,得另想办法。 研究人员就编写了一个关于线程的函数库,用函数库来实现线程。 把创建线程、终止线程等功能放在了这个线程库内,用户就可以通过调用这些函数来实现所需要的功能。 刚刚我们说的线程库,是位于用户空间的,操作系统内核对这个库一无所知,对我们通过线程库创建的线程也一无所知。 也就是说操作系统眼里还是只有这一个进程,相当于我们在进程里嵌套了一个操作系统,用来建立我们自己的用户级线程。 那我用线程库写的一个多线程进程,多个线程也就都只能在一个物理CPU核心上运行,无法运行在多物理核心上。 这其实是用户级线程的一个缺点,这些线程只能占用一个核,所以做不到并行加速,而且由于用户线程的透明性,操作系统是不能主动切换线程的。 简单理解为:用户态线程是寄生在某个内核线程或者说寄生在某个进程上的,对于操作系统来说,它依然只是一个线程,当然只能运行在一个核心上 ​ 并行和并发的区别 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这说明你不支持并发也不支持并行。 并发:你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 并行:你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。 ​ 并发的关键是你有处理多个任务的能力,不一定要同时。 并行的关键是你有同时处理多个任务的能力。 它们最关键的点就是:是否是『同时』。 ​ 可以查阅我的知乎文章: 简单理解:CPU物理数,核心数,线程数,进程,线程,协程,并发,并行的概念 - 知乎 (zhihu.com) ​ Skynet相关 可以把Skynet理解为一个简单的操作系统,它可以用来调度数千个Lua虚拟机,让它们并行工作。每个Lua虚拟机都可以接收处理其它虚拟机发送过来的消息,以及对其它虚拟机发送消息。每个Lua虚拟机,可以看成Skynet这个操作系统下的独立进程,你可以在Skynet工作时启动新的进程、销毁不再使用的进程。Skynet同时掌控了外部的网络数据输入和定时器的管理,Skynet会把这些转换为一致的(类似进程间的消息)消息输入给这些进程。 ​ 我们先看看操作系统的工作:操作系统拥有很多CPU核心,通过算法调度,把不同进程(实质就是线程)安排给不同的CPU核心去执行,从而完成一个个任务。这里的CPU核心是分开独立的,因此多个线程可以并行执行。而协程是存在某个线程中的,但单个线程只能在某个核心下运行,因此多个协程只能并发执行。 再来对比Skynet框架的工作:Skynet进程有很多工作线程,通过算法调度,把不同服务(就是Lua虚拟机)安排给对应的线程去执行,从而完成一个个任务。这里的工作线程是分开独立的,因此多个服务可以并行执行。而协程是存在某个服务中的,因此Lua虚拟机中的协程只能并发执行。这些协程永远不会真正的并行,它们只是在轮流工作,永远只有一个协程在运行(因为协程的本质还是用户级线程,只能限定在一个核心上运行)
2023-04-23 阅 读 全 文