Linux FHS

我们常用Linux发行版本,各种类型文件通常放到不同的目录里,比如二进制文件放置到/bin、/sbin、/usr/bin、/usr/sbin,系统和应用配置文件放置到/etc,/home是非root用户的主目录(/home/{username}),/root是root用户的主目录……

那么为什么要这样放置文件?

这个其实就是Linux基金会制定的FHS标准。

文件系统层次结构标准(Filesystem Hierarchy Standard,FHS)定义了Linux操作系统中的主要目录及目录内容。FHS由Linux基金会维护。 当前版本为3.0版,于2015-06-03发布。具体的FHS规范内容可以看FHS官网。

注意:

  1. 虽然主流的发行版本基本实现了FHS标准,但是在实现细节上与标准还是有一些不一致的地方。还有一些小众的发行版本压根就没有遵循FHS标准。
  2. 这些年新起的Snap、Flatpak、AppImage等应用程序包管理方式,不再像以前的apt、rpm、dnf等遵循FHS规范,而是将应用程序的二进制文件、配置文件、依赖文件等都打包到一个目录里。

 

References:

  1. FHS Wiki https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard
  2. FHS 3.0 https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.html

 

闰秒

说道闰秒,可能很多人没有听说过,而闰年和闰月应该被很多人熟知。

闰年是指该年有366日,即较平常年份多出一日。闰年是为了弥补因人为历法规定的年度天数365日和平均回归年的大约365.24219日的差距而设立的。不同历法有不同置闰方法。儒略历每4年置1闰日,平均1年是365.25日。格里高利历每400年少3次闰日,平均是365.2425日。多出来的一天为2月29日。(参考:https://zh.m.wikipedia.org/zh-sg/%E9%97%B0%E5%B9%B4

我们生活里说的闰月通常是指农历(阴阳历)的闰月。闰月没有固定是一年的中的哪一个月,是动态的,总的规则是19年加7个闰月,即“19年7闰法”。(参考:http://www.gov.cn/govweb/fwxx/kp/2009-06/16/content_1341232.htm

那什么是闰秒呢?

为确定时间,世界上有两种常用的时间计量系统:基于地球自转的世界时(UT)(参考:https://zh.wikipedia.org/zh-sg/%E4%B8%96%E7%95%8C%E6%97%B6
)和基于原子振荡周期的国际原子时(TAI)(参考:https://zh.wikipedia.org/zh-sg/%E5%9C%8B%E9%9A%9B%E5%8E%9F%E5%AD%90%E6%99%82)。由于两种测量方法不同,随着时间推移,两个计时系统结果会出现差异,因此有了协调世界时的概念。
协调世界时以国际原子时秒长为基础,在时刻上尽量接近世界时。1972年的国际计量大会决定,当国际原子时与世界时的时刻相差达到0.9秒时,协调世界时就增加或减少1秒,以尽量接近世界时,这个修正被称作闰秒。
闰秒实际上是为适应地球自转的脚步而对国际原子时的人为增减。依据国际地球自转服务组织对国际原子时与世界时的监测数据,当两者之差达到0.9秒时,该机构就向全世界发布公告,在下一个6月或12月最后一天的最后一分钟,实施正闰秒或负闰秒。
自1972年协调世界时正式使用至今,全球已经实施了27次正闰秒调整,最近一次的闰秒调整是格林尼治时间2016年12月31日。从协调世界时正式使用以来,地球自转一直处于不断减慢的趋势,因此迄今为止的闰秒都是正闰秒,但相关科研发现,自2020年年中以来,地球自转速率呈现加快趋势,这意味着未来也可能会出现负闰秒。(参考:闰秒是什么,将何去何从  http://www.news.cn/tech/2022-11/19/c_1129142173.htm)

历史上已实行的闰秒日期
630 1231
1972 +1 +1
1973 0 +1
1974 0 +1
1975 0 +1
1976 0 +1
1977 0 +1
1978 0 +1
1979 0 +1
1980年 0 0
1981 +1 0
1982 +1 0
1983 +1 0
1984年 0 0
1985 +1 0
1986年 0 0
1987 0 +1
1988年 0 0
1989 0 +1
1990 0 +1
1991年 0 0
1992 +1 0
1993 +1 0
1994 +1 0
1995 0 +1
1996年 0 0
1997 +1 0
1998 0 +1
1999年 0 0
2000年 0 0
2001年 0 0
2002年 0 0
2003年 0 0
2004年 0 0
2005 0 +1
2006年 0 0
2007年 0 0
2008 0 +1
2009年 0 0
2010年 0 0
2011年 0 0
2012 +1 0
2013年 0 0
2014年 0 0
2015 +1 0
2016 0 +1
2017年 0 0
2018年 0 0
2019年 0 0
2020年 0 0
2021年 0 0
2022年 0
总计 11 16
27
目前TAIUTC秒差
37

之前我有写过关于monotonic clock的内容。当时就提到闰秒,正的闰秒会导致wall clock比较时出现负值,所以我们需要使用monotonic clock来克服这一个问题。那为什么呢?我们来看看目前如何处理正值的闰秒的,1998-12-31日的闰秒为例:

TAI (1 January 1999) UTC (31 December 1998 to 1 January 1999) Unix time
1999-01-01T00:00:30.00 1998-12-31T23:59:59.00 915148799.00
1999-01-01T00:00:30.50 1998-12-31T23:59:59.50 915148799.50
1999-01-01T00:00:31.00 1998-12-31T23:59:60.00 915148800.00
1999-01-01T00:00:31.50 1998-12-31T23:59:60.50 915148800.50
1999-01-01T00:00:31.75 1998-12-31T23:59:60.75 915148800.75
1999-01-01T00:00:32.00 1999-01-01T00:00:00.00 915148800.00
1999-01-01T00:00:32.25 1999-01-01T00:00:00.25 915148800.25
1999-01-01T00:00:32.50 1999-01-01T00:00:00.50 915148800.50
1999-01-01T00:00:33.00 1999-01-01T00:00:01.00 915148801.00

可以看到,发生闰秒的1998-12-31 23:59:59之后不是1999年1月1日00:00:00,而是中间多了一个1998-12-31 23:60:60,这两秒的Unix timestamp是一样的。所以如果在使用毫秒级、微秒级、纳秒级的时间戳时,在这两秒里就可能出现负的时间戳差值,比如:23:59:59.600 ~ 23:60:60.000,差值就是一个负值。

写在最后,使用闰秒这种处理方法已被证明具有破坏性,特别是在二十一世纪,尤其是在依赖精确时间戳或时间关键程序控制的服务中。相关国际标准机构一直在讨论是否继续这种做法。(参考:https://zh.wikipedia.org/zh-sg/%E9%97%B0%E7%A7%92

MySQL的Character Set和collate

常见的charset:

一般常用的charset是utf8、utf8mb4。

如下是MySQL 8的文档描述:

  • utf8mb4: A UTF-8 encoding of the Unicode character set using one to four bytes per character.
  • utf8mb3: A UTF-8 encoding of the Unicode character set using one to three bytes per character. This character set is deprecated in MySQL 8.0, and you should use utfmb4
  • utf8: An alias for utf8mb3. In MySQL 8.0, this alias is deprecated; use utf8mb4utf8 is expected in a future release to become an alias for utf8mb4.

总结来说,utf8mb4字符集支持最多使用4个字节来存储一个字符,这样就支持很多额外的特殊字符,比如emoji符号。可以说utf8mb3是个历史产物,当年MySQL支持UTF-8时,UTF标准里一个字符最多只占用3个字节。而后UFT-8标准更新后最多使用4个字节后,MySQL在5.5版本开始新增utf8mb4以支持4个字节存储一个字符。而到目前为止(2023-01)MySQL的utf8还是utf8mb3的别名,当然将来可能uft8会被指向utf8mb4。

 

以上都是老生常谈的知识了,大家无脑的使用utf8mb4就可以了。今天要说的其实是collate,collate主要用于排序和字符比较。因为最近遇到一个问题,就是sql查询时,使用 name like ‘%keywords%’时,MySQL是区分大小写匹配的,一看表的collate是utf8mb4_bin,破案了。那么我们来看看MySQL有哪些常用的collate。

 

utf8mb4_bin: 将字符串每个字符用二进制数据编译存储,区分大小写,而且可以存二进制的内容。

utf8mb4_general_ci:ci即case insensitive,不区分大小写。是一个遗留的校对规则,不支持扩展,它仅能够在字符之间进行逐个比较,没有实现Unicode排序规则,在遇到某些特殊语言或者字符集,排序结果可能不一致。但是,在绝大多数情况下,这些特殊字符的顺序并不需要那么精确。

utf8mb4_unicode_ci:是基于标准的Unicode来排序和比较,能够在各种语言之间精确排序,Unicode排序规则为了能够处理特殊字符的情况,实现了略微复杂的排序算法。

utf8mb4_general_cs:基本同utf8mb4_general_ci,区别是区分大小写。

utf8mb4_unicode_cs:基本同utf8mb4_unicode_ci,区别是区分大小写。

大家可以看到规律了吧:

*_bin: 表示的是binary case sensitive,也就是说是区分大小写的

*_cs: case sensitive,区分大小写

*_ci: case insensitive,不区分大小写

 

我本地MySQL8 配置文件中字符集相关变量

show variables like '%character%';

Variable_name    Value

character_set_client  utf8mb4

character_set_connection       utf8mb4

character_set_database   utf8mb4

character_set_filesystem  binary

character_set_results utf8mb4

character_set_server utf8mb4

character_set_system       utf8mb3

……


show CHARSET like 'utf%'; -- 查看MySQL的default COLLATE


Charset Description  Default collation Maxlen

utf16     UTF-16 Unicode utf16_general_ci 4

utf16le  UTF-16LE Unicode    utf16le_general_ci     4

utf32     UTF-32 Unicode utf32_general_ci 4

utf8mb3       UTF-8 Unicode   utf8_general_ci   3

utf8mb4       UTF-8 Unicode   utf8mb4_0900_ai_ci  4

这里我们会发现一个特殊的collate:utf8mb4_0900_ai_ci ,那这个0900表示什么了?原来utf8mb4_0900_ai_ci 是MySQL8新增的collate,中间的0900,表示的是Unicode 9.0的规范。对应的之前就有的utf8mb4_unicode_520_ci、utf8mb4_unicode_ci等。

 

References:

  1. https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-sets.html
  2. https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-utf8mb4.html
  3. https://zh.wikipedia.org/zh-hans/UTF-8
  4. https://dev.mysql.com/doc/refman/8.0/en/charset-database.html
  5. https://dev.mysql.com/doc/refman/8.0/en/charset-collation-names.html
  6. https://www.lifesailor.me/archives/2676.html

 

防空警报和防灾警报

防空警报

每年9月的第三个星期六是(中华人民共和国)全民国防教育日,这一天部分城市会试鸣防空警报。今年(2022年)是9月17日。

防空警报通常分为:“预先警报、空袭警报、解除警报”三种形式。预先警报是在敌方对我方空袭有预兆时鸣响;空袭警报是在敌方对我方空袭即将或者已经开始时鸣响;解除警报是在敌方空袭结束时鸣响。

预先警报:鸣36秒,停24秒,反复三遍为一个周期。

空袭警报:急促短音鸣6秒,停6秒,反复十五遍为一个周期。

解除警报:连续鸣三分钟为一个周期。

国内不同城市和地区试鸣防空警报的日期并不一样,具体可以参考维基百科或者当地政府网站。

 

防灾警报

不同城市和地区的防灾警报形式并不相同,比如参考3和4对应(山东省)日照市的和浙江省的防灾警报形式就不一样。所以具体到各省市请查询当地政府网站。

 

References:

  1. https://zh.wikipedia.org/zh-cn/防空警报
  2. https://video.sina.cn/news/2022-09-17/detail-imqmmtha7647263.d.html
  3. http://www.rizhao.gov.cn/art/2022/9/14/art_207870_10435896.html
  4. https://www.zj.gov.cn/art/2022/5/12/art_1554467_59699356.html

在Mac中的根目录下创建文件(比如创建/data目录)

从El Capitan (OS X 10.11)引入System Integrity Protection (SIP)开始,mac就已经开始逐步加强对系统文件的写限制,到Catalina(macOS 10.15)时完全限制了在根目录下进行写操作。从Catalina开始,官方提供了synthetic.conf文件以支持在根目录下创建软链。

可以通过man synthetic.conf查看文档。

操作步骤:

  1. sudo vi /etc/synthetic.conf

进行文件映射 or 软连接,如:

data Users/username/log
data1 Users/username/log1

注意:
1)  每行的两项配置不是以/开头。(可以理解系统会帮我们加入前缀/)
2)  data 与 Users/username/log 之间是使用tab进行分隔,否则重启后无效。如果指定目录不存在记得mkdir目录。

  1. 重启Mac,然后ls -l 就会发现/data, /data1就会存在了。

如果发现/data目录没有被创建,那么检查下你的/etc/synthetic.conf文件里的Tab分隔符是否被正确配置了。有的机器的vim配置了set expandtab,导致Tab被自动转换成了多个空格。这个时候可以在编辑模式下,先按ctrl+v再按tab键,就可以输入Tab了。可以用xxd查看,Tab是ASCII码是09,而空格的是20。

xxd  /etc/synthetic.conf

空格的情况(错误的情况)
00000000: 6461 7461 2020 2020 5573 6572 732f …  data    Users/…

Tab的情况(正确的情况)
00000000: 6461 7461 0955 7365 7273 2f…  data.Users/…

 

References:

https://javabase.cn/p/114

https://developer.apple.com/forums/thread/670391

 

聊聊WebSocket

  • WebSocket介绍

WebSocket是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。WebSocket协议在2011年由IETF标准化为RFC 6455,后由RFC 7936补充规范。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

  • HTTP和WebSocket之间的异同点。

WebSocket是一种与HTTP不同的协议。两者都位于OSI模型的应用层,并且都依赖于传输层的TCP协议。 虽然它们不同,但是RFC 6455中规定:it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries(WebSocket通过HTTP端口80和443进行工作,并支持HTTP代理和中介)。 为了实现兼容性,WebSocket握手使用HTTP Upgrade头从HTTP协议更改为WebSocket协议。
与HTTP不同,WebSocket提供全双工通信。
WebSockets URI的格式如下:

ws-URI = “ws:” “//” host [ “:” port ] path [ “?” query ]
wss-URI = “wss:” “//” host [ “:” port ] path [ “?” query ]

ws和wss分别对应明文和加密连接(对比HTTP和HTTPS)。

  • WebSocket实战

因为WebSocket被设计为和HTTP/HTTPS一起工作在80/443端口并且支持HTTP代理和网关,所以一个服务端程序同时接收HTTP和WebSocket请求。对,没错,一个Server端程序可以同时接收HTTP请求和WebSocket请求。

Demo如下:

//处理普通的HTTP请求
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, world!"))
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "index.html") 
})
http.HandleFunc("/ws/chat", func(w http.ResponseWriter, r *http.Request) {
    //这里写一个读取cookie的实例是告诉大家,在处理WebSocket的handshake请求时,服务端是可以读到该域名下的cookie的,具体细节和HTTP协议规范一致。 
    cookie, err := r.Cookie("key")
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("cookie:" + cookie.String())
    }
    //使用github.com/gorilla/websocket 
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        fmt.Println(err)
        return
    }
    //... 
    //此处conn就是一个websocket连接了,服务端可以通过conn来发送消息给客户端 
})

仔细一想,你就会明白。客户端发起ws请求,第一次握手(handshake)的请求和正常的HTTP请求体的格式是遵循的同一个规范——RFC2616。所以WebSocket的第一次握手请求正好被设计为要求和正常的HTTP请求体一样,这样就解答了为什么在一个普通的HTTP Server程序只监听一个端口的情况下可以同时处理HTTP请求和WebSocket请求了。同时也请注意对客户端而言,是建立了两个连接:一个HTTP连接,一个WebSocket连接。

如果你还是不明白,那么我换另外一种说法:

客户端发起了一个请求,可能是HTTP请求也可能是WebSocket,但是他们的请求体格式是一样的。服务端可以事先和客户端约定,根据path路径区分来源,进而决定哪一个是当HTTP请求处理:返回正常HTTP Response,哪一个当WebSocket请求处理:返回Upgrade,继而持有连接实现双向通信。

摘抄部分RFC内容如下:

The handshake MUST be a valid HTTP request as specified by [RFC2616].
The method of the request MUST be GET, and the HTTP version MUST be at least 1.1.
For example, if the WebSocket URI is “ws://example.com/chat”, the first line sent should be “GET /chat HTTP/1.1”.

 

References:

https://datatracker.ietf.org/doc/rfc6455

https://zh.wikipedia.org/wiki/WebSocket

https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API/Writing_WebSocket_servers

 

记录使用unredacter的流水账

unredacter是一个实验性的还原图片中打马赛克的文字的小工具。具体介绍请见github(https://github.com/BishopFox/unredacter)。 README中使用说明部分描述的使用步骤很简单,但是如果你照着尝试,会发现程序跑不起来,各种报错(node的三方库版本依赖的天坑😭),我就将我淌坑的经历记录下来吧。

工作环境:windows 11(因为windows 11才支持wslg),启用wsl2,使用Ubuntu 22.04镜像

步骤:

1. (wsl2的Ubuntu里)安装node v12版本。(直接去官网下载v12版本的二进制文件 https://nodejs.org/download/release/v12.22.12 ,将bin目录加到path路径即可)
2. (wsl2的Ubuntu里)下载源码到本地https://github.com/BishopFox/unredacter
3. (wsl2的Ubuntu里)进入项目根目录,执行 npm --registry https://registry.npmmirror.com install (注意:不要再使用https://registry.npm.taobao.org这个镜像地址了,该地址已经停用了)
4. (wsl2的Ubuntu里)进入项目根目录执行npm --registry https://registry.npmmirror.com install electron@10 (注意一定要安装electron v10版本, v11版本就不行)
5. (wsl2的Ubuntu里)进入项目根目录执行npm start

最终效果图:

问题1: 如果在执行 npm start时遇到如下错误:[5095:0621/232535.423408:FATAL:gpu_data_manager_impl_private.cc(442)] GPU process isn’t usable. Goodbye.
/home/{username}/unredacter /node_modules/electron/dist/electron exited with signal SIGTRAP

解决办法:在windows 11下需要按照官方说明安装对应显卡厂商的显卡驱动(https://github.com/microsoft/wslg ),这样wsl2就支持使用GPU了。

问题2: unredacter已经能正常工作了,但是隔了几天突然又不能工作了,有如下报错:
../../third_party/tcmalloc/chromium/src/tcmalloc.cc:337] Attempt to free invalid pointer 0x55dd1f867dc0
/home/{username}/unredacter-main/node_modules/electron/dist/electron exited with signal SIGSEGV
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! unredacter@1.0.0 start: `npm run build && electron ./dist/main.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the unredacter@1.0.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

解决办法:检查你的显卡驱动是不是被操作系统升级,导致和wslg不兼容了。对于AMD:

 

 

References:

https://mp.weixin.qq.com/s/W20dZpX6nJrNFMrXB4P4Mg

 

查看Linux内核和发行版本的方法

查看Linux发行版本的方法

  • cat /etc/issue,适用于所有的发行版本
  • lsb_release -a,也适用于所有的发行版本,但是有些发行版本(尤其是一些精简版的镜像)可能没有预置lsb_release,需要手动安装。
  • cat /etc/redhat-release,这种方法只适合Redhat系的发行版本,包括CentOS
  • cat /etc/centos-release,只适用于CentOS

 

查看Linux内核版本命令

  • cat /proc/version
  • uname -a

记PHP比较运算符的一个用例

最近被问到如下的一道题:

$x = false;
$i = 9;
for ($i = 9; $i > $x; $i -= 2) {
    echo $i . "\n";
}
//请说出代码执行后的输出结果。

一般想到int类型$i和bool类型的$x比较,bool类型的false会被转换成int类型的0,所以代码块会有5次输出后停止结束。

那么实际执行结果是怎样的呢?

实际执行一次,就会发现,结果是无限循环输出:9,7,5,3,1,-1,-3,-5,……。

为什么呢?

仔细查看PHP文档:https://www.php.net/manual/en/language.operators.comparison.php

会发现有这样的描述:

Type of Operand 1 Type of Operand 2 Result
null or string string Convert null to “”, numerical or lexical comparison
bool or null anything Convert both sides to boolfalse < true
object object Built-in classes can define its own comparison, different classes are uncomparable, same class see Object Comparison
stringresourceint or float stringresourceint or float Translate strings and resources to numbers, usual math
array array Array with fewer members is smaller, if key from operand 1 is not found in operand 2 then arrays are uncomparable, otherwise – compare value by value (see following example)
object anything object is always greater
array anything array is always greater

其中第二行就有关于bool类型和其他类型的比较,有明确说明,会把其他类型转换成bool类型。那么上文中的这道题,$i总是不等于0,即意味着$i总是会被转换成bool类型的true,true永远大于false,所以会有无限循环的输出。