JavenLaw

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

Skynet源码之:service_harbor(18)

service_harbor 的实现和《Skynet源码之:节点建立》密切相关 可以先了解一下 harbor 的设计和历史 ​ 让我们看看 service_harbor 的 harbor_create 创建函数 struct harbor * harbor_create(void) { struct harbor * h = skynet_malloc(sizeof(*h)); // 创建了一个 struct harbor 结构体的内存 memset(h, 0, sizeof(*h)); h->map = hash_new(); return h; // 返回 struct harbor 结构体的句柄 } // 下面开始都是一些结构体的定义 // 远程消息的头部 struct remote_message_header { uint32_t source; uint32_t destination; uint32_t session; }; // 远程消息 struct harbor_msg { struct remote_message_header header; void * buffer; size_t size; }; // 远程消息队列 struct harbor_msg_queue { int size; int head; int tail; struct harbor_msg * data; // 实质是一个 结构体数组,参考《Skynet消息队列》中二级消息队列的实现 }; struct keyvalue { struct keyvalue * next; char key[GLOBALNAME_LENGTH]; uint32_t hash; uint32_t value; struct harbor_msg_queue * queue; }; // slave结构体 struct slave { int fd; struct harbor_msg_queue *queue; int status; int length; int read; uint8_t size[4]; char * recv_buffer; }; // hashmap结构体 struct hashmap { struct keyvalue *node[HASH_SIZE]; }; // harbor结构体 struct harbor { struct skynet_context *ctx; int id; uint32_t slave; struct hashmap * map; struct slave s[REMOTE_MAX]; }; // 将 hashmap 初始化为0 static struct hashmap * hash_new() { struct hashmap * h = skynet_malloc(sizeof(struct hashmap)); memset(h, 0, sizeof(*h)); return h; } 看看 harbor_init 函数的做了什么
2024-05-12 阅 读 全 文

Skynet源码之:service_snlua(17)

根据前面logger服务的知识,我们知道: snlua服务也有create,init,cb,release一共4个接口 至于snlua模块的加载详细见《Skynet源码之:模块加载》 ​ snlua的启动 1,首先看看 snlua 是在哪里启动的呢? 在 config 配置中,有 config.bootstrap = “snlua bootstrap” 作为参数传入 在 skynet_start.c 文件,第 285 行,首先开启了第一个服务 logger ​ 在 skynet_start.c 文件,第 285 行,继续调用 bootstrap() 函数(注意不是 bootstrap.lua 服务) bootstrap(ctx, config->bootstrap) 开启新的服务,参数是: ​ ctx是 skynet_context * logger,cmdline 是 snlua bootstrap(就是配置中的 config->bootstrap = “snlua bootstrap”) 代码如下: // 由bootstrap函数,启动bootstrap服务 // 特别注意:通过bootstrap(logger, cmdline)传入的logger对bootstrap服务的启动没有任何关系 // 只是为了:bootstrap服务 万一在启动失败的时候,可以释放logger服务 static void bootstrap(struct skynet_context * logger, const char * cmdline) { // *********************************begin int sz = strlen(cmdline); char name[sz+1]; char args[sz+1]; int arg_pos; sscanf(cmdline, "%s", name); arg_pos = strlen(name); if (arg_pos < sz) { while(cmdline[arg_pos] == ' ') { arg_pos++; } strncpy(args, cmdline + arg_pos, sz); } else { args[0] = '\0'; } // *********************************end // 上面这段代码就是把 字符串cmdline 解析并存储在args // 重点代码看这里:name = "snlua",args = "bootstrap" // 这里就是启动 snlua 服务,同时把 bootstrap 作为参数传给snlua服务 struct skynet_context *ctx = skynet_context_new(name, args); if (ctx == NULL) { skynet_error(NULL, "Bootstrap error : %s\n", cmdline); // 如果启动失败,就需要把刚才启动的logger服务释放掉 skynet_context_dispatchall(logger); exit(1); } } ​
2024-05-10 阅 读 全 文

Skynet源码之:service_logger(16)

skynet有4类线程:Monitor线程 + Timer线程 + Socket线程 + Worker线程 其中Monitor线程负责的事情最为简单,监控好struct monitor,并定时执行检查函数即可,代码量比较少 Timer线程负责定时器,做的事情一般,检查struct timer,并根据到期的定时器的信息,向某个服务投递定时消息即可,代码量一般 Socket线程负责网络,做的事情最专业,负责管理socket,跟系统API打交道,代码比较多,功能非常关键 最后一个是Worker线程,负责执行不同的任务,这些任务都是不同的服务 现在的问题是:这些服务到底是什么?服务有几个类型?不同的服务又是如何实现的? 底层的服务类型一共也有4类:logger服务,harbor服务,gate服务,snlua服务 ​ logger服务 我们首先看logger服务,也是最简单的实现,也是整个Skynet最先开启的服务 在skynet_satrt.c文件中,第279行,首次调用了 skynet_context_new(config->logservice, config->logger) 服务 (当然,这是在前面消息队列,服务管理,监控器,定时器,模块加载已经完成初始化之后的调用) 同时,这个 skynet_context_new() 也是服务启动的接口,在c层的代码就是直接调用: skynet_context_new() 让我们看看实现吧: // 传入的参数 name = logger,而这个logger是默认写死在Skynet底层c代码的 // 存储在config->logseivice中,此值不能被配置,因为必须要有最基础的日志模块 // 重点区分 config->logseivice(字段名是:logseivice,值是:logger) // config->logger(字段名是:logger,值是:用户配置的参数) // 如果用户配置了logger,config->logger的值就是用户配置的日志文件路径,param就会有值 // 用户没有配置logger,param就是空 struct skynet_context * skynet_context_new(const char * name, const char *param) { struct skynet_module * mod = skynet_module_query(name); // 重点代码:查找模块 logger // skynet_module_query(name)实际返回的是:一个指向 struct skynet_module地址 的指针 // 并且这个地址存在 modules.
2024-05-08 阅 读 全 文

Skynet专题之:信号

信号在Linux编程中比较常见,在 POSIX 系统中,信号是用于通知进程发生了某个事件的机制 当进程接收到信号时,可以根据信号的类型和处理方式来采取相应的行动 ​ 如何使用信号 下面就根据在Skynet中的代码来分析以下信号的使用 sigign()函数在 skynet_main.c 文件中,第131行开始调用 // 信号设置函数 int sigign() { struct sigaction sa; sa.sa_handler = SIG_IGN; sa.sa_flags = 0; sigemptyset(&sa.sa_mask); sigaction(SIGPIPE, &sa, 0); return 0; } ​ sigaction结构体 第一,先看看 struct sigaction 结构体是怎么样的 // struct sigaction 结构体定义了信号的处理方式和相关的属性 // 用于设置和处理信号的行为,包括信号处理函数、信号的屏蔽集合和标志等 struct sigaction { void (*sa_handler)(int); // 信号处理函数 void (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数(扩展版本) sigset_t sa_mask; // 信号屏蔽集合 int sa_flags; // 信号处理标志 void (*sa_restorer)(void); // 用于恢复信号处理的函数(已弃用) }; // 下面依次解释字段的含义和作用 // sa_handler:指向信号处理函数的指针,用于处理接收到的信号。当信号发生时,系统会调用指定的信号处理函数来处理该信号 // sa_sigaction:扩展版本的信号处理函数指针,可以接收更多的信号相关信息,如信号的来源和附加数据等 // sa_mask:用于设置信号处理函数执行期间要屏蔽的信号集合。在信号处理函数执行期间,可以通过设置 sa_mask 来阻塞其他信号的传递 // sa_flags:用于设置信号处理的标志,例如设置为 SA_RESTART 可以使被信号中断的系统调用自动重启 // sa_restorer:已弃用的字段,用于指定在信号处理函数执行完毕后恢复信号处理的函数 ​
2024-05-07 阅 读 全 文

Skynet专题之:原子操作

原子操作 和 锁 都用于处理多线程环境下的并发访问问题,因此下面将统一解释它们 ​ 原子操作&锁的区别 在并发编程中 原子性是指一个操作要么完全执行,要么完全不执行,没有中间状态 它是在底层硬件或操作系统级别执行的操作,是不可中断的单个操作,具有原子性和互斥性 原子性操作的目的是:保证在多线程环境下对共享数据的操作不会出现竞态条件 ​ 锁是一种同步机制,用于实现临界区的互斥访问 它保证同一时间只有一个线程可以获得锁并执行临界区代码,避免多个线程同时修改共享数据而导致的数据竞争问题 锁的作用是提供互斥性,但锁并不保证临界区内的操作是原子的 锁的原子性体现在: ​ 当一个线程获得了锁并进入临界区时,它可以确保在持有锁的期间不会被其他线程打断 ​ 这确实具有一种原子性,即在给定的上下文中,临界区的执行是不可中断的 ​ 锁的目的是:保护共享资源,以确保在任何给定时间只有一个线程可以访问临界区 ​ 第一,各自的作用 原子: 原子操作用于对共享数据进行原子性的读取和修改,确保多个线程对同一数据进行操作时不会引发竞态条件和数据不一致的问题 原子操作可以保证某个特定操作在执行期间不会被其他线程中断,从而确保操作的完整性和一致性 锁: 锁用于实现临界区的互斥访问,确保同一时间只有一个线程可以进入临界区执行操作,从而避免多个线程同时修改共享数据而导致的数据竞争问题 锁可以保证在一个线程执行临界区代码时,其他线程会被阻塞,等待当前线程释放锁后才能继续执行 第二,区别和好处 粒度: 原子操作通常用于对单个变量或对象的操作,提供了更细粒度的并发控制。它可以在不阻塞其他线程的情况下,对共享数据进行原子性的读取和修改 锁通常用于对一段代码(临界区)的访问控制,提供了更粗粒度的并发控制。它可以确保同一时间只有一个线程可以执行临界区代码 开销: 原子操作通常比锁的开销更小,因为它不需要上下文切换和线程阻塞等额外开销。原子操作使用硬件级别的原子指令来实现,执行速度较快 锁的实现可能涉及线程调度和上下文切换,需要更多的开销。当临界区的代码较长或复杂时,锁的开销可能会更高 场景: 原子操作适用于对共享数据进行简单的读取和修改操作,如计数器、标志位等。它们可以在不阻塞其他线程的情况下,保证数据的一致性 锁适用于需要对一段代码进行互斥访问的情况,如修改共享数据的复杂算法、数据结构的更新等。它们可以确保在同一时间只有一个线程执行临界区代码,避免数据竞争和不一致性 ​ 原子操作的实现 一般是如何使用原子操作的呢?几乎都是使用封装好的原子操作函数和库 主要分为3类: ​ C/C++标准:atomic 头文件 + stdatomic.h 头文件 ​ GCC实现:GCC 编译器提供了一些内建函数 ​ POSIX标准:pthread.h 头文件 ​ atomic 库,是 C++ 标准库中的一部分,属于 C++ 标准 atomic 是 C++11 原子操作的一部分,提供了原子操作的支持,包含在 atomic.h 头文件中。它提供了一组模板类和函数,用于对各种类型的数据进行原子操作,比如加载、存储、交换、逻辑操作等。 atomic 主要用于 C++ 中
2024-04-29 阅 读 全 文

Skynet专题之:线程

在全局初始化skynet_globalinit()函数中,涉及到了线程相关的知识,现在将详细地了解线程的相关知识 ​ pthread_key_t pthread_key_t 是POSIX线程库中用于表示线程特定数据键的数据类型 在多线程程序中,线程特定数据允许每个线程拥有自己独立的数据副本,而不会互相干扰 这对于需要在线程之间共享数据的场景非常有用 对于线程特定数据的操作的例子,详细地可以看 skynet_server.c文件中对于 skynet_globalinit 的初始化 常用相关函数有: ​ pthread_key_create():创建一个线程本地存储的键 ​ pthread_setspecific():为当前线程设置线程本地存储键的值 ​ pthread_getspecific():获取当前线程特定键的值 ​ pthread_key_delete():删除一个线程本地存储的键 // pthread_key_t 是一个抽象的类型,实际上是一个整数或指针,用于标识线程特定数据键 // 它在创建线程特定数据键时由 pthread_key_create() 函数填充,并在后续的线程特定数据操作中使用 // 在使用 pthread_key_t 类型时,通常需要先声明一个 pthread_key_t 变量 // 然后通过 pthread_key_create() 函数创建线程特定数据键,并将键的值存储在该变量中 // 最后可以使用这个键来获取,设置线程特定数据副本 // 特别说明:网页显示问题,以下代码的 预处理语句 都省略了 # // 使用例子: include <pthread.h> include <stdio.h> pthread_key_t key; void destructor(void* data) { printf("Destructor called for thread %lu\n", pthread_self()); free(data); } void* thread_function(void* arg) { int* thread_specific_data = malloc(sizeof(int)); if (thread_specific_data == NULL) { perror("Failed to allocate memory"); pthread_exit(NULL); } *thread_specific_data = (int)(size_t)arg; // 将线程特定数据与键关联 if (pthread_setspecific(key, thread_specific_data) !
2024-04-29 阅 读 全 文

Skynet专题之:网络

网络的相关内容非常多且复杂,目前我认为自己还没理解的很透彻 更重要的是:我认为我并没有比网上的教程写的更好 ​ 在这里给出几个我觉得非常不错的学习账号 ​ 小林coding:小林coding (xiaolincoding.com) ​ 图解网络介绍 | 小林coding (xiaolincoding.com) ​ 9.2 I/O 多路复用:select/poll/epoll | 小林coding (xiaolincoding.com) ​ ​ 开发内功修炼(张彦飞allen):开发内功修炼@张彦飞 - 分享我的技术日常思考,和大伙儿一起共同成长! (kfngxl.cn) ​ 图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的! - 开发内功修炼@张彦飞 - 分享我的技术日常思考,和大伙儿一起共同成长! (kfngxl.cn) ​ 开发内功修炼网络篇电子书出炉!! - 开发内功修炼@张彦飞 - 分享我的技术日常思考,和大伙儿一起共同成长! (kfngxl.cn) ​ ​ 关于epoll:The method to epoll’s madness. My previous post covered the… | by Cindy Sridharan | Medium 需要翻墙 ​
2024-04-20 阅 读 全 文

Skynet专题之:锁

原子操作 和 锁 都用于处理多线程环境下的并发访问问题,因此下面将统一解释它们 ​ 原子操作&锁的区别 在并发编程中 原子性是指一个操作要么完全执行,要么完全不执行,没有中间状态 它是在底层硬件或操作系统级别执行的操作,是不可中断的单个操作,具有原子性和互斥性 原子性操作的目的是:保证在多线程环境下对共享数据的操作不会出现竞态条件 ​ 锁是一种同步机制,用于实现临界区的互斥访问 它保证同一时间只有一个线程可以获得锁并执行临界区代码,避免多个线程同时修改共享数据而导致的数据竞争问题 锁的作用是提供互斥性,但锁并不保证临界区内的操作是原子的 锁的原子性体现在: ​ 当一个线程获得了锁并进入临界区时,它可以确保在持有锁的期间不会被其他线程打断 ​ 这确实具有一种原子性,即在给定的上下文中,临界区的执行是不可中断的 ​ 锁的目的是:保护共享资源,以确保在任何给定时间只有一个线程可以访问临界区 ​ 第一,各自的作用 原子: 原子操作用于对共享数据进行原子性的读取和修改,确保多个线程对同一数据进行操作时不会引发竞态条件和数据不一致的问题 原子操作可以保证某个特定操作在执行期间不会被其他线程中断,从而确保操作的完整性和一致性 锁: 锁用于实现临界区的互斥访问,确保同一时间只有一个线程可以进入临界区执行操作,从而避免多个线程同时修改共享数据而导致的数据竞争问题 锁可以保证在一个线程执行临界区代码时,其他线程会被阻塞,等待当前线程释放锁后才能继续执行 第二,区别和好处 粒度: 原子操作通常用于对单个变量或对象的操作,提供了更细粒度的并发控制。它可以在不阻塞其他线程的情况下,对共享数据进行原子性的读取和修改 锁通常用于对一段代码(临界区)的访问控制,提供了更粗粒度的并发控制。它可以确保同一时间只有一个线程可以执行临界区代码 开销: 原子操作通常比锁的开销更小,因为它不需要上下文切换和线程阻塞等额外开销。原子操作使用硬件级别的原子指令来实现,执行速度较快 锁的实现可能涉及线程调度和上下文切换,需要更多的开销。当临界区的代码较长或复杂时,锁的开销可能会更高 场景: 原子操作适用于对共享数据进行简单的读取和修改操作,如计数器、标志位等。它们可以在不阻塞其他线程的情况下,保证数据的一致性 锁适用于需要对一段代码进行互斥访问的情况,如修改共享数据的复杂算法、数据结构的更新等。它们可以确保在同一时间只有一个线程执行临界区代码,避免数据竞争和不一致性 ​ 内存顺序 在开始锁的实现之前,有些必要的知识需要了解,可以先了解《Skynet专题之:原子操作》 即原子主要分为3类: ​ C/C++标准:atomic 头文件 + stdatomic.h 头文件 ​ GCC实现:GCC 编译器提供了一些内建函数 ​ POSIX标准:pthread.h 头文件 我们的自旋锁也是3种实现方式 ​ 内存顺序 ​ 假设有两个线程 A 和 B,它们共享一个整型变量 flag 用于表示某个条件是否满足 ​ 线程 A 负责设置 flag,表示某个事件已经发生
2024-04-18 阅 读 全 文

Skynet专题之:算法

这里主要描述记录一下Skynet中用到的算法 ​ 二分法 二分法:详细见《Skynet源码之:服务管理》中【给服务handle命名】部分中,函数 _insert_name() 对二分法的应用 ​ 队列实现 队列实现:详细见《Skynet源码之:消息对列》中【全局消息队列:入队和出队】部分中的实践 ​ 环形数组 环形数组 :详细见《Skynet源码之:消息对列》中【次级消息队列:入队和出队】 ​ 动态数组 动态数组:详细见《Skynet源码之:消息对列》中【次级消息队列:扩容】 ​ 定时器实现 定时器实现:详细见《Skynet源码之:定时器》中【时间轮算法】的内容 另外:红黑树 + 最小堆的算法,也可以实现定时器,需要寻找网上资料 ​ 雪花算法 分布式-全局唯一id-美团的leadf-snowlake 直接看ondrive中的lua代码 ​ 等待添加 其余的部分,等待添加。。。
2024-04-11 阅 读 全 文

Skynet使用之:sharedata数据共享

关于同节点内,不同服务的数据共享: ​ sharetable:ShareTable · cloudwu/skynet Wiki (github.com) ​ sharedata:ShareData · cloudwu/skynet Wiki (github.com) ​ STM:STM · cloudwu/skynet Wiki (github.com) ​ 跨节点内,不同服务的数据共享: ​ datacenter:DataCenter · cloudwu/skynet Wiki (github.com) ​ 在同个节点内(单个进程内)的方案 ​ 1,最简单粗暴的方法是通过消息传递数据。如果 A 服务需要 B 服务中的数据,可以由 B 服务发送一个消息,将数据打包携带过去 ​ 这里可以是同个节点内,也可以是跨节点(就是datacenter ) ​ 2,最原始的方法是把数据写到一个 lua 文件中,让不同的服务加载它(cluster的配置文件就是这样) ​ 注意:默认 skynet 使用自带的修改版 lua ,会缓存 lua 源文件 ​ 当一个 lua 文件通过 loadfile 加载后,磁盘上的修改不会影响下一次加载 ​ 所以你需要直接用 io.open 打开文件,再用 load 加载内存中的 string ​ 3,更好的方法是使用 sharetable 模块 ​ Skynet 使用修改过的 Lua 虚拟机,可以直接将一个 table 在不同的虚拟机间共享读取(但不可改变)
2024-03-30 阅 读 全 文