Redis 源码学习——浅谈 Redis 的字符串实现

Redis 源码学习——浅谈 Redis 的字符串实现 0x00 结构 Redis 的字符串,本质上还是沿用了 C 语言的字符串,但是还是有一些不同。Redis 的字符串附加了一些和字符串本身相关的信息。我觉得这个设计很巧妙(也许是一个常用的技巧,是我太菜了不知道)。 首先来看 sds 的定义 typedef char sds; 在目前的约定下,一个 char 是一个字节,这是开辟内存空间的最小的单位了,方便内存管理(我是这样认为的)。关于字符串的信息,定义在了几个结构体里面。 / Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. / struct attribute ((packed)) sdshdr5 { unsigned char flags; / 3 lsb of type, and 5 msb of string length / char buf[]; }; struct attribute ((packed)) sdshdr8 { uint8_t len; / used / uint8_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits / char buf[]; }; struct attribute ((packed)) sdshdr16 { uint16_t len; / used / uint16_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits / char buf[]; }; struct attribute ((packed)) sdshdr32 { uint32_t len; / used / uint32_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits / char buf[]; }; struct attribute ((packed)) sdshdr64 { uint64_t len; / used / uint64_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits */ char buf[]; }; 这几个结构体,其实是同一个作用,就是记录了字符串的各种信息,它们的区别就是用来表示不同长度的字符串,或者说用来开辟不同大小的空间。里面的几个成员变量,我认为是这样的作用:
Read more →

csapp-bomb-lab-phase-1

第一题比较简单,但本菜鸡也做了两个小时(╯‵□′)╯︵┻━┻。。。 首先打开事先已经反汇编的 bomb.s 文件,通过 bomb.c 已经知道每一关都是一个函数,它们的命名都是 phase_x,x 代表该关卡的数字,如果某个关卡输入的不正确,就会引爆炸弹 explode_bomb。首先看 main 函数的这几行 400e1e: bf 38 23 40 00 mov $0x402338,%edi 400e23: e8 e8 fc ff ff callq 400b10 <puts@plt> 400e28: bf 78 23 40 00 mov $0x402378,%edi 400e2d: e8 de fc ff ff callq 400b10 <puts@plt> 400e32: e8 67 06 00 00 callq 40149e <read_line> 400e37: 48 89 c7 mov %rax,%rdi 400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1> 400e3f: e8 80 07 00 00 callq 4015c4 <phase_defused> 400e44: bf a8 23 40 00 mov $0x4023a8,%edi 打开 gdb,先给这一行打上断点 break *0x400e23,然后 run 起来。这里可以看到调用了 puts 这个函数,寄存器 %edi 存储的是函数的第一个参数,我们把它的结果打印出来 x/s 0x402338、x/s 0x402378,发现得到了运行 bomb 后输出的字符串。说明第一关就是从这里开始的。
Read more →

我的 2017 —— 一个 PHPer 的自白

转眼间 2017 年过去了。我已经不能说自己是去年的毕业生了,时光匆匆,感觉自己越来越老了。 这一年,我所经历的,让我收获很多,让我懂得很多,让我明白了很多。也许是明确了某一个目标,也许是其它的什么,我觉得,2017 年也许真的是我的一个开端。 事 说实话,这一段的标题,真的不好想。所以我写的时候,空了出来。这里我要写的是,关于我对编程的一些感悟。经历了一些事,也参加了一些事,觉得,啊,原来我想要的,是这样的。 大会 2017 年,我去了 PHPCON。说实话,这是我第二次花钱去参加大会,也是花钱最多的一次。不过真的觉得很值,因为这次大会,让我明白了很多。首先,是鸟哥关于 PHP7 的介绍。他通过底层实现来介绍 PHP7 的一些优化的地方,但是由于水平有限,我不能听懂。包括韩天峰的演讲,我也一样不能完全理解。究其根本,还是由于我的基础知识太弱导致的。于是,我在会后,努力去学习这些基础知识,才发现,啊,原来他们讲的是这样的! 去年,我给我自己的定位是“Re: 从零开始的编程生活”。今年,也是这么觉得,而且感觉更加强烈了。 小会 值得一提的是,今年 7 月份,我参加了一个 Swoole 的分享会。分享会是在一家咖啡馆里开的,很有氛围(旁边就是 bilibili link(雾。会后,韩天峰 dalao 请了我们外地去的盆友吃了饭(然后被抢买单了(雾。这都不是重点,重点是,通过这次,我明白了,我接下去的路该怎么走。我更加明白了,我要学习哪些东西。 其它 其它嘛,也不好说,说了也不好。懂的自然懂。 书 上面都说了,我也知道自己该看些什么了。于是,我花了半年的时间看完了《深入理解计算机系统》,受益匪浅。同时,我看完了《深入 PHP:面向对象、模式与实践》,了解了很多常用的设计模式,完善了面向对象的很多知识。还有《Go 语言实战》。Go 是我一直想学习的一门语言,现在终于有机会完整地去学习了。我又开始看了《现代操作系统》和《数据结构与算法分析(C 语言描述)》。对于数据库,我现在正在看《高性能 MySQL》,收益颇多。 出游 沙巴 这是部门组织的出游,获得了最佳团队,拿到了一笔经费,于是有了这次出游。这次旅行,我感受到了异国风情,还和海水近距离地接触了。浮潜这个一直不敢做的项目,也尝试了。总之,这是一个不错的地方 四川 是的,今年我又去了四川,和群里的某基友一起去的。不过这次没有在成都逗留,而是直接出发去了稻城亚丁。从四川去稻城亚丁,花费了 3 天时间。从几百米的海拔,到近 5000 米的海拔。在汶川,我看到了璇口中学遗址,感触颇多。在前往稻城亚丁的路上,我看到了任何其它地方都看不到的景色。美!在亚丁,也花了 4 个小时爬山,看到了美丽的风景,真的值了。于是,我的下个目的地是——西藏。 2018 对于 2018,我给自己定了很多目标,不管能不能达成,我都要尽力去做。 2017 年,我虽然看了一些书,但是我觉得还是远远不够的。我希望在 2018 年,我能够阅读完《现代操作系统》、《数据结构与算法分析(C 语言描述)》。这些都是基础中的基础,我现在明白了,当了解了这些后,其它的也都能理解了。还有《高性能 MySQL》也要读完。再有就是《UNIX 环境高级编程》和《UNIX 网络编程》。这也是做服务端编程的基础,这两本书也许读不完,但是我会把它们当做一个持续的目标,这个目标也包括《TCP/IP 详解》。2017 年学习了 Go 语言,但是发现似乎《Go 语言实战》这本书讲的过于宽泛了。我希望能够阅读一遍《Go 程序设计语言》,辅之以实践。 2017 年,我坚持阅读 Laravel 的源码,让我学到了很多。今年,我依然会坚持阅读 Laravel 的源码。同时,我还尝试阅读了 PHP 的源码。发现,把它和上面的三本书配合起来,非常的合适。所以,2018 年,我依然要坚持阅读 PHP 的源码,同时学习扩展的编写。在有余力的情况下,我还要阅读 Nginx 的源码。
Read more →

《现代操作系统》读书笔记——进程

一个进程就是一个正在执行的程序实例,它包括程序计数器、寄存器以及变量的当前值。一个程序运行,它的逻辑计数器装入 CPU 的程序计数器中;一个程序暂停,CPU 的程序计数器被保存在内存的逻辑程序计数器中,在下次运行前不占用 CPU。 要特别注意的是,进程并不是在 CPU 中一直运行的,CPU 会在各进程之间来回切换,所以每个进程执行的速度是不确定的。所以,大多数进程并不受 CPU 多道程序设计或其它进程相对速度的影响。 进程的创建和终止 有四种情况会导致进程的创建,它们分别是系统初始化、正在运行的程序执行了创建进程的系统调用、用户请求创建一个新进程、一个批处理作业的初始化。拿 Linux 为例,Linux 启动时的第一个进程是 0 号进程,它是所有进程的祖先。其次是 1 号进程,它是所有用户进程的祖先。 我们都知道 Nginx。当我们启动 Nginx 后,它一直在默默地监听端口执行 WEB 服务,而我们没有感知。这一类进程,便是守护进程。 任何进程,都可以创建一个新的进程,这个进程便是子进程,执行的是 fork 系统调用。进程可以 fork 子进程,子进程共享父进程的数据,但是,子进程对数据做的任何修改,对于父进程,都是不可见的。子进程的内存地址空间,是父进程的副本,是不同的地址空间。 进程只能有一个父进程,但是一个进程可以有多个子进程。在 UNIX 中,进程不能剥夺其子进程的继承权。 可写的空间是不共享的,共享的空间是不可写的。子进程可以共享父进程的内存空间,但是要通过写时复制共享。即在修改部分内存,先要明确地复制,以确保发生在私有的内存区域。 进程终止通常由下面的情况引起,正常退出、出错退出、严重错误、被其他进程杀死。其中后面两种情况是非自愿的。Linux 中自愿终止进程可以通过调用 exit 系统调用完成。 进程的状态 进程由三种状态,运行、阻塞和就绪。 处在运行态的进程,在这个时刻实际占用 CPU。 处在就绪态的进程具备可以运行条件,但是因为其它进程正在运行,而被操作系统暂停运行。 处在阻塞态的进程,因该进程调用了本地阻塞的系统调用,导致暂停运行。处在阻塞态的进程,不具备可以运行的条件。除非外部某实践发生,例如本地阻塞的调用完成,方能够转换为就绪态,等待操作系统调度。 进程的实现 操作系统维护这一个进程表,每一个进程的详细信息都保存在这张进程表中。包括程序计数器、堆栈指针、内存分配情况、文件打开情况、中断向量等。 如前面所说,每个进程都不是一直在 CPU 中运行地。其中就绪态和阻塞态是非运行状态。当操作系统将正在运行的进程切换为就绪态,或者进程因为调用了本地阻塞的系统调用而进入阻塞态时,其运行信息被保存在进程表中。当操作系统重新切换进程为运行状态时,将进程表中的信息恢复,就像进程没有中断一样。 以 IO 为例。当一个进程 A 调用了网络 IO 的系统调用时,由于该系统调用时阻塞的,于是操作系统将其切换为阻塞态,并且将它的现场都保存在进程表中,并将此地址与中断向量映射保存。然后,切换 B 进程。一段时间过去,此时可能是 C 进程在 CPU 中运行,A 进程调用的 IO 完成了,则该硬件发生了一个中断。此时,操作系统将中断正在运行的 C 进程,同时通过中断向量找到进程表中的 A 进程的现场并恢复到 A 进程没有中断时的状态,继续运行。
Read more →

《现代操作系统》读书笔记——线程

Read more →

源码安装 NSQ

因为业务需要,要用到 NSQ。所以学习了下 NSQ。首先是安装,我在自己电脑上,倾向于源码安装。一是源码安装可以安装最新的代码,二是整个安装过程可以自己掌控。 但是,安装过程中遇到了一些坑。主要还是我对 Go 以及一些衍生工具用的不是特别熟悉,并且在网上搜索到的文章,都是抄来抄去的很多并不能解决我的问题。所以我把整个安装过程记录下来,给自己一个备忘,给别人一个方便。 安装 Go NSQ 是用 Go 写的,所以安装 NSQ 之前,要先安装 Go。 我这里给出安装具体过程的命令。具体可以参考我写的另外一篇文章 从零开始学习 Go ——安装。 echo "export GOROOT=$HOME/.golang/go" >> ~/.bash_profile echo "export GOPATH=$HOME/.golang/path" >> ~/.bash_profile echo "export PATH=$PATH:$HOME/.golang/go/bin" >> ~/.bash_profile echo "export GOROOT_BOOTSTRAP=$HOME/.golang/go1.4" >> ~/.bash_profile source ~/.bash_profile cd ~ mkdir .golang git clone https://github.com/golang/go.git go cp -r go go1.4 cd go1.4 git checkout -b release-branch.go1.4 origin/release-branch.go1.4 cd src ./make.bash cd ../../go git checkout -b release-branch.go1.8 origin/release-branch.
Read more →

从零开始写 PHP 扩展(一)

PHP 是用 C 语言写的。对于每个 PHPer 来说,都有着内心的一种希望写扩展的冲动了吧。然而,缺乏一个很好的切入点。Google 上搜 PHP 扩展开发,大部分都是复制品文章,甚至有些人连操作都没有操作过就搬运在了自己的博客。不过也有几篇好教程,但是都是 PHP 5 时代的产物,隐藏着非常多的坑。我会将我自己慢慢踩坑的过程记录下来,也许这就成了其它人的“教程”了吧。 生成一个扩展 想必很多人已经看到很多网上的教程了。大多都是教我们执行这个命令:$ ./ext_skel –extname=extname。但是,当你 clone 了 PHP 源码后会发现,master 分支下并没有 ext/ext_skel 这个文件。所以,我总结了一下: 如果你是直接下载 PHP 的源码,或者在已经 release 的版本分之下,你可以执行这个命令 $ cd ext $ ./ext_skel –extname=extname 如果你是直接在 master 分支下,只有 ext_skel.php 文件,这个时候你就直接可以执行这个 PHP 文件 $ cd ext $ php ext_skel.php –ext extname 由于我是直接在 master 分支下开发的,所以后面的都是默认在 master 分之下的操作。 生成了扩展之后,我们会看到四个文件和一个文件夹。现在这个阶段,我们只需要用到两个文件,.c 文件和 .h 文件。 一个小坑 在我们生成好扩展之后,我们可以试着编译一下 $ phpize $ ./configure $ make && make test 我们会惊讶地发现,编译的时候会有一个 warning。
Read more →

浅入理解单例模式

问题 恼人的全局变量 在 PHP 中,甚至不只 PHP 中,我们都会用到全局变量,以保存全局状态。可是,往往全局变量是全局共享的,任何地方任何代码都有可能将其覆盖。例如,我们定义一个全局变量叫做 PHONE。我们在某一行代码中,将其定义成了 iPhone,但是我们不小心在另一行代码中将其覆写成了 Nokia。这就非常的尴尬了,因为本来我们并不想它被覆写。 繁琐的参数传递 在一个系统中,我们会定义许多的方法,生成很多的对象。有时候,我们会使用很多的方法,对同一个对象做操作。在不使用全局变量的情况下,我们需要将对象作为参数传入方法中。但是这样传递同一个对象,可能会造成混乱,还可能造成不必要的依赖。 其实我们只需要一个全局可访问的对象就可以解决这个,但是全局变量又会出现我们上面的说的问题。 解决 目标 我们要解决这些问题,我们对这样的对象有下面的几个目标。 这个对象,无论在哪里都能访问,就想全局变量一样。 这个对象,和全局变量不同,不能被覆写。 这个对象,整个系统中只存在一个,对它的修改在整个系统中都能被感知到。 以上的几个目标,就是我们所需要的,也就是单例模式的特征。 UML 实现 class Preference { private static $instance; private $props = []; private __construct() {} public static function getInstance() { if (empty(self::$instance)) { self::$instance = new Preference(); } return self::$instance; } public function setProperty($key, $value) { $this->props[$key] = $value; } public function getProperty($key) { return $this->props[$key]; } } 我们在这里引入了一个私有的构造函数,这样,外部就无法实例化这个对象了。同时,我们使用 getInstance 方法来获取具体的实例,而无法去覆写它,这就达成了第二个目标。
Read more →

浮点数那些事

本文为作者自己的总结的,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。 说起浮点数,大家都是又恨又爱的。爱呢,是因为,只有它可以方便地使用小数;恨呢,是因为它并不能精确地表示小数。 以 PHP 为例:floor((0.1 + 0.7) * 10) 这样一个函数调用,根据数学老师死得晚原理,大家都能得出 8 这个结果。可是事实上呢?它会返回 7。数学老师的棺材板。。。(╯‵□′)╯︵┻━┻ 可是为什么会出现这种情况呢?这就要从浮点数的特性说起了。 万物皆二进制 我们都知道,在计算机中,一切的一切都是二进制表示的。假设一个 4 字节整型的十进制数 8,在大端表示的机器中,表示成 00000000 00000000 00000000 00001000(0x0000008)。将十进制整数转换成二进制数,是非常容易的。可是,小数呢?比如,我们要表示 1.75,该怎么存储在计算机中呢?显然,不能像整数一样存储了。 小数的二进制 让我们回忆一下,在十进制中,小数是怎么计算的。上面的 1.75 我们是这么算的:1 × 10^0 + 7 × 10^-1 + 5 × 10^-2 。那么我们按照相同的规则,来用二进制计算一下小数部分:0.75 = 1⁄2 + 1/4,也就是 1 × 2^-1 + 1 × 2^-2 ,再加上前面的整数部分,那么整个式子就变成了 1 × 2^0 + 1 × 2^-1 + 1 × 2^-2 ,写成二进制形式就是 1.11。所以,1.75 的二进制表示是 1.11。 对于将小数转换为二进制,和整数部分除二取余相反的,是乘二取整。
Read more →

php 内核探秘之 PHP_FUNCTION 宏

本人也只是个初入门的菜鸟,因对技术有着向往,故在“无趣”的工作之余,尽自己所能提升自己。由于我的 C 语言功底也有限,故本文的深度也有限,如有幸得大牛阅读,还望指导一二,小弟感激不尽。 PHP 的函数 作为 PHPer,我们几乎每天都在写函数,我们一定会好奇,那些 PHP 内置的函数,是长什么样子的。如果写过 PHP 扩展的话,一定知道这个宏:PHP_FUNCTION。在定义一个函数的时候,这样来使用这个宏。例如 array_change_key_case,它的定义是这样的:PHP_FUNCTION(array_change_key_case)。没错,就是这么简单。但是,在这个简单的背后,却没有这么简单。 PHP_FUNCTION 追根溯源 宏 相信对这篇文章感兴趣的同学,一定多少对 C 语言以及它的宏定义有一定的了解。如果没有,也不要紧,我这里来简单解释一下,什么是宏。 C 语言中的宏,我认为,可以理解为一种简单的封装。通过宏定义,可以对开发者隐去一些细节,让开发者在使用简单的语法来完成重复的复杂的编码。当然,宏定义还有其它的用途,但是,我们在 PHP_FUNCTION 涉及到的就是这个作用。有下面的代码。 #define TEST(test) void test(int a) TEST(haha) 宏,就是完全的替换,即使用后面的语句替换前面的。那么对于下面的 TEST(haha) 就相当于下面的样子。 void haha(int a) PHP_FUNCTION 的定义 首先,我们要定义函数,这样使用这个宏。 PHP_FUNCTION(array_change_key_case) { // TODO } 我们在 php-src/main/php.h 中找到了下面的定义。 #define PHP_FUNCTION ZEND_FUNCTION 也就是说,这里用 ZEND_FUNCTION 替换了 PHP_FUNCTION 这个宏。所以,我们的定义就相当于变成了这样。 ZEND_FUNCTION(array_change_key_case) { // TODO } 我们继续往下找,因为,这里还是宏,我们并没有看到我们希望看到的代码。我们可以在 php-src/Zend/zend_API.h 中找到下面的定义。 #define ZENDFN(name) zif##name #define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(ZEND_FN(name)) #define ZEND_NAMED_FUNCTION(name) void name(INTERNAL_FUNCTION_PARAMETERS) 我们看到,在宏定义中,使用了另外的宏。不要怕,还是一个词,替换。我们按照这样的步骤来。(## 是一个连接符,它的作用是,是将它前面的与后面的,按照字符串的方式连接起来。
Read more →