列表:
1 | . |
列表:
1 | . |
本文解释利用github pages搭建个人主页/项目主页的方法。
github pages简介:官方链接。
github pages使用了CNAME record技术,参考:链接1、链接2、Custom domains in Github Pages。
注:Read the Docs也是一个很好的搭建个人主页的网站。
有3种类型的 Github Pages 站点(sites):project, user 和 organization 。
Project sites 连接到 github 上特定 project ,比如 Javascript library 或 recipe collection。user 或 organization sites 连接到 github.com 的特定账户。
发布 user site ,你必须创建一个你的个人账户下的一个名为 <username>.github.io 的 repository 。发布 organization site ,你必须创建一个组织所有的名为 <organization>.github.io 的 repository 。除非你使用 custom domain ,否则 user 和 organization sites 将位于 http(s)://<username>.github.io 或 http(s)://<organization>.github.io 。
project site 的源文件存储在作为 project 的相同的 repository 中。除非使用 custom domain , 否则 project sites 将位于 http(s)://<username>.github.io/<repository> 或 http(s)://<organization>.github.io/<repository> 。
有关如何自定义影响您网站的域名的更多信息,参见"About custom domains and GitHub Pages"。
每个 github 账户允许创建 1 个 user 或 organization 站点。无论是被组织还是个人所有,project 站点的个数不限制。
参考官方文档。
例如,你的project站点配置的发布源是gh-pages分支,然后在gh-pages分支上创建了一个about/contact-us.md文件,你将可以在https://<user>.github.io/<repository>/about/contact-us.html访问它。
你也可以使用Jekyll等静态站点生成器来给你的github page配置一个主题。
参考官方文档。
推荐的markdown编辑器:
VSCode markdown插件:
在线表格生成器:可以生成Markdown、Text、HTML、LaTex、MediaWiki格式的表格。
WSL,Windows Subsystem for Linux,是Windows提供的轻量级Linux虚拟机。
安装教程:见链接。
启用systemctl的方法:链接。
替代方法:不需要启动systemctl,因为会比较占用资源,启动也会变慢。可以使用service命令替代。
使用ssh连接到服务器时,需要服务器运行着sshd程序,否则连接不上,会出现"Connection refused"错误。
参考链接。
查看openssh-server有没有安装:
1 | dpkg --list | grep ssh |
注:如果安装了openssh-server,执行which sshd可以看到路径。
WSL默认没有安装openssh-server,安装方法:
1 | sudo apt-get install openssh-server |
启动ssh:
1 | sudo service ssh start |
git push不再支持输入用户名和密码,当提示输入密码时,需要输入personal access token.
步骤1:在github上创建personal access token;
步骤2:在命令行上使用personal access token;
步骤3:为了避免每次都需要输入personal access token,可以将其缓存在git client上:
1 | gh auth login |
注:使用gh命令需要先安装GitHub CLI:
1 | sudo apt-get install gh |
报错解释:
这个报错信息通常出现在使用SSH连接到一个新的主机时。它表示你的计算机无法验证远程服务器的身份,因为服务器的公钥不在你本地计算机的known_hosts文件中。这是SSH为了防止"中间人"攻击而进行的安全检查。
解决方法:
验证指纹信息:你可以查看远程主机的指纹信息,并与服务器gitee.com的公钥指纹进行对比,确保它们匹配。你可以在~/.ssh/known_hosts文件中找到已知主机的公钥指纹。
如果确认指纹正确无误,且你信任这个服务器,可以添加这个主机及其公钥到你的known_hosts文件中,以便SSH不再警告。执行以下命令:
ssh-keyscan -H gitee.com >> ~/.ssh/known_hosts
如果你不想添加到known_hosts文件中,可以在第一次连接时使用ssh -o StrictHostKeyChecking=no来跳过这个检查。但这样做会降低安全性。
如果你是在多个服务器上使用相同的IP地址,并且之前已经添加过这个IP的记录,那么可能是服务器的公钥发生了变化,这种情况下你应该联系服务器管理员确认公钥的变更。
注意:在实际生产环境中,不建议无条件信任新的SSH指纹,除非你完全了解这个服务器的来源和身份。
以下几种静态站点生成器都可以用来搭建个人主页。如果使用除JekyII外的工具,则需要配置Github Actions以构建和发布你的站点。
mkdocs是一个快速的静态网页生成器。
发布个人网站的方法:参见mkdocs-material官网。
Jekyll 是一个静态站点生成器,内置对 GitHub Pages 的支持和简化的构建进程。
RCU(Read-Copy-Update,读-拷贝-更新)是 Linux 内核中一种高效的 并发读-写同步机制,专门用于在多核系统下实现 大量读、少量写 的场景。它的核心思想是:读操作完全无锁,写操作通过复制更新,最后再安全地回收旧版本。
RCU 的核心思想
__rcu它是 RCU 机制中非常关键的一环,用来让 编译器和内核知道某个指针是 RCU 保护的。
__rcu 的定义在 Linux 内核中(以 x86_64 为例):
1 | #define __rcu |
实际上,它 本身对编译器不产生直接影响
主要用途是 标记类型,告诉 Sparse 静态分析工具 或 内核开发者:这个指针受 RCU 保护,不能随意直接读/写
也就是说,__rcu 是 一个注释性质的宏,编译器编译时忽略,但静态分析工具会检查 RCU 访问规则。
__rcu 的作用标记 RCU 保护的指针,常见用法:
1 | struct files_struct { |
含义:
fdt 是 RCU 指针fdt = new_ptr; 或直接解引用 fdt->xxxrcu_assign_pointer(fdt, new_fdt); → 安全更新指针rcu_dereference(fdt) → 安全读取指针__rcu 与编译器和内核__rcu 指针的写操作是否用 rcu_assign_pointerrcu_dereferencercu_assign_pointer 内部会加上 适当的写屏障 (smp_wmb())rcu_dereference 内部会加 读取屏障 (smp_rmb())__rcu 的核心原理总结| 方面 | 说明 |
|---|---|
| 编译器作用 | 本身是空宏,不改变代码 |
| 静态分析 | Sparse 检查 RCU 指针的安全读写 |
| 内存屏障 | 通过 rcu_assign_pointer / rcu_dereference 添加屏障,保证并发安全 |
| 运行时 | 指针仍然是普通指针,实际存储和访问和普通指针一样 |
sendfilemmap + writesplicereadv / writev:一次性读写多个缓冲区,减少系统调用 read/write的次数。参考
:玩转 Linux 内核:超硬核,基于 mmap 和零拷贝实现高效的内存共享
read + write从磁盘读取文件,发送到网络:
read:
write:
发生了 4 次拷贝、4 次上下文切换(每次系统调用都会发生两次上下文切换:用户态 → 内核态,内核态 → 用户
态)。
mmap + writemmap 的本质:虚拟地址(用户空间) → 物理页
将一段 用户空间的虚拟地址空间 映射到某个物理资源(文件、设备、或匿名页)上:
| mmap 类型 | 说明 |
|---|---|
| 映射文件 | 映射到关联文件的内核页缓存(page cache) |
| 匿名内存(RAM) | 映射到物理页 |
| 映射设备(如 GPU、FPGA) | 映射到设备地址 |
mmap:
write:
发生了 3 次拷贝、4 次上下文切换。
sendfile函数签名:
1 | ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); |
一次系统调用 sendfile 就可以完成从 磁盘文件 → 网卡 的拷贝。
发生了 3 次拷贝、2 次上下文切换。
注意到,sendfile 方法 IO 数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比
如静态文件服务器。
sendfile 被称为“零拷贝”,因为完全绕过用户空间,没有用户空间和内核空间的数据拷贝。
mmap + write 只是多了一次系统调用(多了 2 次上下文切换),但是一般不称为零拷贝,因为它本质上是write 来完成。io_uring 是异步 IO,核心机制之一就是通过 mmap 映射共享环形缓冲区。mmap 是共享内存的方式之一,以公用文件名中转。文件名一般使用 /dev/shm/ 下的文件名,因为其为tmpfs 内存文件系统,数据不会落盘;如果使用普通文件名,则可能触发磁盘读写,尽管由于页缓存的存在,splice函数签名:
1 | ssize_t splice(int fd_in, loff_t *off_in, |
splice 把数据从一个文件描述符“拼接”到另一个文件描述符,而其中至少一个必须是管道(pipe)。
为什么一定要 pipe 参与?
pipe 是内核态缓冲区 pipe 是一种特殊的内核对象,拥有自己的 buffer(pipe buffer)。、
普通文件或 socket 虽然也有缓冲区(页缓存、socket buffer),但是它们不属于通用的缓冲区:
示例:
1 | int pipefd[2]; |
vmsplice函数签名:
1 | ssize_t vmsplice(int fd, const struct iovec *iov, |
vmsplice 将用户空间的内存页映射到 pipe buffer 。
示例:
1 | struct iovec iov = { .iov_base = user_buf, .iov_len = len }; |
vmsplice 几乎总是需要与 splice 协作使用,因为 vmsplice 本身只能将用户空间的内存页映射到一个
pipe 中,而不能直接发送到 socket 或写入文件。
| 特性 | sendfile | splice | vmsplice |
|---|---|---|---|
| 零拷贝 | ✅(文件 → socket) | ✅(文件/pipe/socket → 文件/pipe/socket) | ✅(用户页 → pipe) |
| 输入源 | 文件 | 文件 / pipe / socket | 用户缓冲区 |
| 输出目标 | Socket | 文件 / pipe / socket | Pipe |
| 用户空间参与 | 不参与 | 不参与 | 用户页作为数据源 |
完整示例:
1 | #include <unistd.h> |
常规 TLS(OpenSSL)会在用户态做加密,破坏零拷贝。可用内核 TLS(KTLS)或 NIC TLS/offload 实现零拷贝出
网(把加密放内核或网卡)。
io_uring(modern Linux)支持零拷贝模式(例如 splice/sendfile 的异步提交、SQE/ CQE),并且可以做更高
效的批处理。
https://www.xiaolincoding.com/os/8_network_system/zero_copy.html#如何实现零拷贝
散布读写支持一次性将数据从文件描述符读写到多个缓冲区:
如果 select / poll / epoll 通知可读写,那么一定可读写吗?答案是不一定。因为内核不是 实时地 检查
内核缓冲区是否有空间或有数据,所以内核的通知有时间差和虚假性。而 epoll 等函数只关注事件变化,不检查
缓冲区。这样可以提高效率。最终的结果就是鼓励用户程序尝试,但是不保证一定成功,也就是可能阻塞。所以需
要非阻塞 IO 来进一步提高性能。
EPOLLONESHOT阅读 manual:
Since even with edge-triggered epoll, multiple events can be generated upon receipt of multiple
chunks of data, the caller has the option to specify the EPOLLONESHOT flag, to tell epoll to
disable the associated file descriptor after the receipt of an event with epoll_wait(2). When the
EPOLLONESHOT flag is specified, it is the caller’s responsibility to rearm the file descriptor
using epoll_ctl(2) with EPOLL_CTL_MOD.
如果某个文件描述符上有多个数据块到达,那么即使是边沿触发也无法保证事件只通知一次。这可能是由于数据包
过大被分片,或者是新数据到达。
EPOLLONSHOT 需要调用者自行 reset 这个标志。https://blog.csdn.net/salmonwilliam/article/details/112347938
struct task_struct (进程控制块)
task_struct结构体,内核用它来描述进程。files ,执行该进程的 files_struct 。1 | // https://elixir.bootlin.com/linux/v6.16/source/include/linux/sched.h |
files_struct (进程的文件表)
file * 结构体。1 | // https://elixir.bootlin.com/linux/v6.16/source/include/linux/fdtable.h |
struct file (打开文件表项)
open()、pipe()、socket() 创建一个 struct file。1 | // https://elixir.bootlin.com/linux/v6.16/source/include/linux/fs.h |
struct file 再指向更底层的对象,比如 inode(磁盘文件)、socket 缓冲区、pipe 缓冲区。
i-node 包含以下内容
stat结构中的大多数信息都取自i节点。
软链接与硬链接
| 类型 | 定义 |
|---|---|
| 硬链接 (Hard Link) | 表示有多少目录项(文件名)指向同一个 inode 。它们指向同一个文件内容。 |
| 软链接 (Symbolic Link / Symlink) | 类似快捷方式,是一个 独立文件,内容是指向目标文件的路径。 |
unlink,而不是delete。S_IFLINK,表明是符号链接。| 特性 | 硬链接 | 软链接 |
|---|---|---|
| 是否指向 inode | 是,直接指向同一 inode | 否,指向目标路径 |
| 是否可以跨文件系统 | 否,只能在同一分区 | 可以跨分区 |
| 是否可以链接目录 | 通常不能(除非 root) | 可以 |
| 删除目标文件后 | 文件内容仍可访问 | 链接会失效(称为“悬挂链接”) |
| 占用空间 | 不占用额外数据空间(只是多了一个目录项) | 占用少量空间存储路径信息 |
| 更新文件内容 | 所有硬链接同步可见 | 通过软链接修改目标文件内容时可见,软链接本身只是路径 |
O_APPEND 标志打开一个文件,那么相应的标志也被设置到文件表项的文件状态标志中。每次对文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为 i 节点表项中的文件长度(相对其他进程来说是原子操作,不论是两个独立的进程,还是父子进程)。这就使得每次写入的数据都追加到文件的当前尾端处。这里有一个测试的例子,文章结论不见得正确,请参考评论的讨论。PIPE_BUF:只保证小于PIPE_BUF的内容是原子;如果大于则可能被多次多段写入。PIPE_BUF 是管道(pipe)单次写入保证原子的最大字节数,Linux 上是 4096 字节。1 | # 查看 PIPE_BUF 大小 |
以下是 man 2 write 关于 O_APPEND 的说明:
If the file was open(2)ed with O_APPEND, the file offset is first set to the end of the file before writing. The adjustment of the file offset and the write operation are performed as an atomic step.
若一个文件用 lseek 定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为 i 节点表项中的当前文件长度(注意,此时,设置偏移量和写操作之间不是原子操作)。
dup()后的内核数据结构dup() / dup2() 只复制 fd ,也就是在 fd 数组中新增了一个 fd 项。一般用来重定向。
struct file ,并添加到 fd 数组或 fd 表中。
fork() 后的子进程直接复制父进程的 fd 数组,exec() 也不能将其替换;
struct task_struct 是深拷贝,所以 fd 数组被复制;struct file* 仍然指向父进程创建的 struct file (共享);fcntl()设置了FD_CLOEXEC标志,此时 exec 会关闭继承的文件描述符。子进程对文件表项的修改,会不会影响父进程?
fork()开启用户进程(子进程),该子进程复制父进程shell的所有文件描述符,于是0, 1, 2文件描述符被打开;测试代码:
1 | #include <fcntl.h> |
1 | $ ./a.out |
1 | $ ./a.out |
分析
a.out改变了。第三次运行:
重新启动shell,并运行a.out
1 | $ ./a.out |
分析
引申:
在此我们注意到,文件描述符0, 1, 2(标准输入、标准输出、标准错误)在一个shell及其所有子进程中,对应的文件(设备)是同一个。由于共享了文件表项,指向了同一个v-node表项,故都指向同一个虚拟终端。这与我们的平时观察一致,不然shell运行程序时,输入输出的入口在哪里呢?
如果进程打开文件,此时我们使用 rm / unlink 删除文件,会发生什么?
什么也不会发生。因为 Linux 文件系统的设计允许文件名(目录项)和文件内容(inode)分离。只有当所有引用(包括文件描述符和内存映射)都关闭后,inode 才会被删除。
在 Linux 中,一个文件由三部分组成:
/lib/libexample.sorm 只是删除了目录项(文件名),并没有删除 inode 或数据块,只要还有进程引用它。
引用 inode 的方式包括:
这些引用会让内核知道:这个 inode 仍然在使用中,不能释放。
这是为了支持非常重要的行为:
✅ 允许进程继续使用已打开或已映射的文件,即使文件名被删除。
这在很多场景下非常有用:
在 Linux 内核中,每个 inode 结构体有一个字段 i_count,表示该 inode 当前被引用的次数。这个引用包括:
这个字段不是用户空间可以直接查看的,但你可以通过以下方式间接观察:
| 方法 | 能看到什么 |
|---|---|
ls -li |
inode 号 + 硬链接引用计数 |
lsof -p <PID> | grep / fuser </path/to/so> |
是否有进程打开文件 |
/proc/<PID>/fd/ |
查看文件描述符引用 |
/proc/<PID>/maps |
查看映射 |
ldd |
查看可执行文件依赖的 .so |
strace |
跟踪运行时加载行为 |
内核字段 i_count |
真实引用计数(需内核调试) |
在本地文件系统(如 ext4)中:
rm 也能删除目录项但在 NFS 文件系统 中:
.nfsXXXX所以此时如果 rm -rf 目录,但是目录下某文件被使用,会提示 xxx/.nfs000000004ec2d5e70000da89 。
1 | $ lsof -p <PID> | grep .nfs |
因为 so 被手动删除,此时引用的 so 被重命名成 .nfsXXXX 。
手动删除 .so 文件后,系统生成了 .nfsXXXX 文件,但进程仍然能继续使用它。进程怎么知道新文件名?
答案是:进程根本不知道新文件名,也不需要知道。
当一个进程打开一个文件(比如 libexample.so),它获得的是一个 文件描述符(fd),这个描述符指向的是内核中的 inode,而不是文件名。
那 .nfsXXXX 文件名是给谁看的?
它是 NFS 客户端自动创建的临时文件名,用于:
📌 这个文件名不会被通知给进程,也不会影响进程的运行。
正常情况下:进程关闭后 .nfsXXXX 自动消失
⚠️ 异常情况:文件可能残留
如果出现以下情况,.nfsXXXX 文件可能不会自动删除:
在这些情况下,.nfsXXXX 文件会残留在文件系统中,直到手动清理。
通过上文的叙述,我们很容易想到管道本质上也是一种特殊的文件,所以管道机制之所以可以进程间通信也是根据共享文件表项保证的。
管道和文件进行进程间通信的本质相同。
TODO
Unix 权限涉及三个部分:** 用户 、 进程 、 文件 **。
权限分为常用权限、SELinux 权限。
manpage: man 2 stat 中搜索 “mode” 可以看到几种常用权限的详情。
用户用 user ID 区分。多个用户可以划入同一个用户分组,一个用户可以同时属于不同的分组。这就是 UID(User ID)和 GID(Group ID)。
现在我将使用我的凭据登录到 shell 并运行:
1 | grep $LOGNAME /etc/passwd |
rotem: x:1000:1000:rotem,:/home/rotem:/bin/bash
您可以看到我的日志名 (rotem)、均为 1000 的 UID 和 GID,以及其他详细信息,例如我登录的 shell。
每个进程都有一个所有者,并且每个进程都属于一个组。
进程有 3 种 UID:real user ID、effective user ID、saved user ID。其中还有一个 set-user-ID 的概念,这个概念和 effective user ID 紧密关联。
在我们的 shell 中,我们现在将运行的每个进程都将继承我的用户帐户的权限,并将使用相同的 UID 和 GID 运行。
当您 fork 一个新进程时,该进程会继承父进程的 RUID。通常父级的 RUID 是你的 shell 并且它有当前登录用户的 UID。所以新进程有当前登录用户的 UID 的 RUID。通常这不会改变,只有 root 可以改变它。
举个例子,想想 init 进程派生了你的登录 shell。在 fork 期间,shell 将具有 root 的 RUID(因为 shell 的父级是 init)。但是,init 进程使用 /etc/passwd 将 shell 的 RUID 改成你的 UID. 因此,此后登录 shell 的 RUID 将是您的 UID,而不是 root。所以,我们可以说 RUID 是进程所有者的。
让我们运行一个简单的命令来检查它:
1 | $ sleep 100 & ps aux | grep 'sleep' |
然后根据 ps 命令打印出的 PID (3741) ,查进程 UID 和 GID:
1 | $ stat -c "%u %g" /proc/3741 |
然而,系统判断一个进程对一个文件是否有权限时,要验证的 ID 是 effective user ID,而不是 real user ID。
Linux 通常都不建议用户使用 root 权限去进行一般的处理,但是普通用户在做很多很多 services 相关的操作的时候,可能需要一个特殊的权限。为 了满足这样的要求,许多 services 相关的 executable 有一个标志,这就是 set-user-ID bit。当这个 set-user-ID bit=ON 的时候,这个 executable 被用 exec 启动之后的进程的 effective user ID 就是这个 executable 的 owner id,而并非 parent process real user id。如果 set-user-ID bit=OFF 的时候,这个被 exec 起来的进程的 effective user ID 应该是等于进程的 user ID 的。
我们以 ping 命令为例。
使用 which 命令搜索二进制位置,然后运行 ls -la:
-rwsr-xr-x 1 root root 64424 Mar 10 2017 ping
可以看到文件的所有者和组是 root. 这是因为该 ping 命令需要打开一个套接字,而 Linux 内核 root 为此需要特权。
但是,如果没有 root 特权,我如何使用 ping?
注意文件权限的所有者部分中的 “s” 字母而不是 “x”。
这是特定二进制可执行文件(如 ping 和 sudo)的特殊权限位,称为 set-user-ID。
这是 EUID 和 EGID 发挥作用。
将会发生的情况是,当执行设置了 setuid 的二进制文件 ping 时,该进程将其有效用户 ID (EUID) 从默认值 RUID 更改为此特殊二进制可执行文件的所有者,在本例中为 root。
这一切都是通过这个文件有这个简单的事实来完成的 set-user-ID。
内核通过查看进程的 EUID 来决定该进程是否具有特权。因为现在 EUID 指向 root,操作不会被内核拒绝。
注意:在最新的 Linux 版本中,ping 命令的输出看起来会有所不同,因为它们采用了 Linux Capabilities 方法而不是这种 setuid 方法(对于不熟悉的人)请阅读 此处。
参考 链接 1 。
Unix 包含另一个权限位,即该权限 set-user-ID 位。如果为可执行文件设置了该位,那么只要所有者以外的用户执行该文件,该用户就可以访问所有者的其他任何文件,从而获得所有者的所有文件读 / 写 / 执行特权!
这会导致运行该文件的任何人或进程都可以访问系统资源,就好像他们是该文件的所有者一样。
1 | $ ls -l testfile |
为文件添加 set-user-ID 权限:
1 | $ sudo chmod u+s testfile |
说明:大写 S 表示,有 set-user-ID 权限,但是没有执行权限。
阅读 Manual:man 2 stat。
The set-group-ID bit (S_ISGID) has several special uses.
For a directory, it indicates that BSD semantics is to be used for that directory: files created there inherit their group ID from the directory, not from the effective group ID of the creating process, and directories created there will also get the S_ISGID bit set.
如果目录具有设置组 ID(S_ISGID):
For a file that does not have the group execution bit (S_IXGRP) set, the set-group-ID bit indicates mandatory file/record locking.
如果一个文件有 “设置组 ID(set-group-ID)” 但是没有 “组执行权限(S_IXGRP)”,也就说具有如下权限:
1 | % ls -l a.txt |
那么,此时 set-group-ID 位喻示强制文件 / 记录锁。
TODO:强制文件 / 记录锁是什么?
Mandatory File Locking
Why remove group execute for mandatory file lock?
The sticky bit (S_ISVTX) on a directory means that a file in that directory can be renamed or deleted only by the owner of the file, by the owner of the directory, and by a privileged process.
大写 T 表示,有 restricted deletion flag or sticky bit(粘滞位),但是没有执行权限,t 权限只对目录有效,作用是保护目录项不能被其他用户删除。目录要同时具有 x 和 s 才能保证粘滞位有效。
为什么要设置一个 saved set-user-ID 呢?它的意义是,它相当于是一个 buffer, 在 exec 启动进程之后,它会从 effective user ID 位拷贝信息到自己。
setuid() 来将 effective user ID 设置成为 real user ID 和 saved set-user-ID 中的任何一个。但是非 root 用户是不允许用 setuid() 把 effective user ID 设置成为任何第三个 user id。《Unix 环境高级编程》的例子是,普通用户去执行一个 tip 进程,set-user-ID bit=ON,执行起来的时候,进程可以有 uucp (executable owner) 的权限来写 lock 文件,也有当前普通用户的权限来写数据文件。在两种文件操作之间,使用 setuid() 来切换 effective user id。但是正是因为 setuid() 的限制,该进程无法获得更多的第三种用户的权限。
saved set-user-ID 是无法取出来的,是 kernel 来控制的。注意 saved set-user-ID 是进程中的 id,而 set-user-ID bit 则是文件上的权限。
用户 ID:
| 属性 | 决定特权 | 说明 |
|---|---|---|
| EUID | ✅ 是 | 内核权限检查时使用 |
| RUID | ❌ 否 | 表示谁启动了进程 |
| FSUID | ⚠️ 有时 | 文件访问权限相关 |
| SUID | ⚠️ 有时 | 用于权限恢复 |
组 ID:
| 属性 | 说明 | 用途 |
|---|---|---|
| EGID | 有效组 ID | 用于权限检查 |
| RGID | 实际组 ID | 表示谁启动了进程 |
| SGID | 保存组 ID | 用于权限恢复 |
| FSGID | 文件系统组 ID | 文件访问专用 |
以上都是讨论用户 ID,如果没有特殊说明,以上的组 ID 和用户 ID 基本是类似的,只是作用对象为组。
符号形式表示文件权限有 5 种:rwxst。
r:可读。对文件来说,意味着能执行 vim 查看、cat 等。对目录来说,意味着能执行 ls 查看其下的文件列表。
w:可写。对文件来说,意味着能执行 vim 等工具编辑并保存。对目录来说,意味着能够创建、删除文件或新的目录。
x:可执行(文件) / 可搜索(目录)。对文件来说,意味着可以执行。对目录来说,意味着能够 cd 进入该目录。
《UNIX 环境高级编程》P80 中如此描述:
读权限允许我们读目录,获得该目录中所有文件名的列表。当一个目录是我们要访问文件的路径名的一个组成部分时,对该目录的执行权限使我们可以通过该目录(也就是搜索该目录,寻找一个特定的文件名)。
ls 没有可搜索权限的目录可以看到文件列表、文件类型,但是不能看到其他信息,比如文件权限、所有者、大小、修改时间等,因为这些信息保存在 inode 中,必须先cd进入该目录,才能读取这些信息。同样,ls -R不能显示没有执行权限的目录下的子目录下的文件,因为这也必须先cd该目录,然后执行ls显示子目录的文件。
1 | $ ls -lR mydir/ |
给目录递归恢复权限:
1 | $ chmod -R u+X mydir/ |
s:即 set-user-ID 权限,如果可执行文件有 s 权限属性,那么任意进程执行该文件时,将自动获得该文件所有者相同的所有权限。如果文件没有 x 权限,却有 s 权限,那么 ls -l 命令将该文件显示为大写的 S。文件只有同时具备 s 权限和 x 权限,才有意义,因为一个文件要应用 set-user-ID 属性,首先要保证其可执行。例如 ping 文件:
1 | $ ls -l /bin/ping |
由于设置了 s 权限,所以任何文件都能以 root 用户的身份运行,也就被内核允许打开套接字。
t:即 restricted deletion flag or sticky bit,称为 “粘滞位” 或 “限制删除标记”。仅仅对目录有效,对文件无效。在一个目录上设了 t 权限位后,(如 /home,权限为 1777) 任何的用户都能够在这个目录下创建文档,但只能删除自己创建的文档 (root 除外),这就对任何用户能写的目录下的用户文档 启到了保护的作用。如果目录 / 文件没有 x 权限,却有 s 权限,则 ls -l 命令将目录 / 文件显示为大写的 T。目录只有同时具备 t 权限和 x 权限,才有意义,因为一个目录如果本来就不允许增删目录项(x 权限),删除其他用户的文件更无须提了。
t 权限时:
root 用户除外,始终可以删除任何文件例如:/tmp 和 /var/tmp 目录供所有用户暂时存取文件,亦即每位用户皆拥有完整的权限进入该目录,去浏览、删除和移动文件。
可以用 4 位八进制数(0-7)表示这些文件权限,由 4、2、1 相加得到,0 表示所有权限都没有。
这 4 位的含义如下:
以下是 chmod 的 man page 的说明 [2]:
A numeric mode is from one to four octal digits (0-7), derived by adding up the bits with values 4, 2, and 1. Omitted digits are assumed to be leading zeros. The first digit selects the set user ID (4) and set group ID (2) and restricted deletion or sticky (1) attributes. The second digit selects permissions for the user who owns the file: read (4), write (2), and execute (1); the third selects permissions for other users in the file’s group, with the same values; and the fourth for other users not in the file’s group, with the same values.
ls 命令ls -l 命令用于查看文件权限。
chmod 命令chmod 命令用于改变文件权限。
基本用法:
1 | chmod [OPTION]... MODE[,MODE]... FILE... |
** 大写 X 参数 ** [3]:
例如 chmod u+X filename 或 chmod u-X filename。
在 chmod 的 man page 中介绍如下:
The letters
rwxXstselect file mode bits for the affected users: read (r), write (w), execute (or search for directories) (x), execute/search only if the file is a directory or
already has execute permission for some user (X), set user or group ID on execution (s), restricted deletion flag or sticky bit (t) [2:1].
加黑体的话很费解,但是又十分准确,解释如下:
对所有目录赋予执行权限,这意味着可以执行 cd。
对所有文件,如果原来文件的 ugo(user / group / others)任意一个原先有执行权限,那么动作与小写 -x 参数相同;如果原先没有,那么忽略。例如:
1 | ls -l |
结果:
-rw-rw-r-- 1 rotem rotem 0 Nov 9 10:55 file1
-rw-rw-r-x 1 rotem rotem 0 Nov 9 10:56 file2
可以看到,原来 file1 的 ugo 都不具备执行权限,file2 的 others 具备执行权限。
X 动作,将无效:1 | sudo chmod u+X file1 |
-rw-rw-r-- 1 rotem rotem 0 Nov 9 10:55 file1
X,能够生效:1 | sudo chmod u+X file2 |
-rwxrw-r-x 1 rotem rotem 0 Nov 9 10:56 file2
题外话: chmod -R u-X mydir 该命令无法递归执行,因为当执行完顶层目录的权限更改之后,已经没有权限 cd 顶层目录了,其他执行全部被停止。
SELinux 提供更为严格的访问控制。
可以用 ls -Z 查看文件的 SELinux 权限(安全上下文)。这部分将来另用一篇博客说明。暂时可以参考文献 [4]。
/etc/services: 端口列表
/etc/protocols: 协议列表
主要功能:允许端口重用。客户端一般不用,只在服务端常见。
更准确地说,它有几个具体效果(以 TCP 为例):
TIME_WAIT 状态下可重绑定
正常情况下,当服务端关闭套接字,端口进入 TIME_WAIT 状态(持续 ~1-4 分钟)
如果不设置 SO_REUSEADDR,你必须等 TIME_WAIT 结束才能再次 bind() 相同端口
设置后,可以立即重新 bind()
多个套接字绑定到同一个端口(但 IP 必须不同)
常见于多播、广播,或者一台机器上多个不同地址绑定同一个端口
Linux 特性:允许 相同 IP 和端口 多个套接字同时监听(结合 SO_REUSEPORT 使用更常见,nginx 就依赖这个做负载均衡)。
TIME_WAIT 只会出现在主动关闭连接的一方。
一般情况:客户端主动关闭 → 客户端 TIME_WAIT,服务端不需要考虑
特殊情况:如果服务端主动关闭 listen_fd(例如服务进程退出后重启),就会产生 TIME_WAIT,需要优化手段。
多进程/线程共享端口(同一IP+端口),避免 TIME_WAIT 堆积影响单个进程。前提是都设置了 SO_REUSEPORT)。
内核会 均匀分发新连接 到这些套接字(负载均衡)。
常见用途:
与 SO_REUSEADDR 的区别
| 选项 | 功能 | 使用场景 |
|---|---|---|
| SO_REUSEADDR | 允许端口在 TIME_WAIT 状态下重用 | 服务端重启,避免 “Address already in use” |
| SO_REUSEPORT | 允许同一端口被多个进程/线程同时绑定,内核负载均衡 | 高性能多进程网络服务 |
用于关闭 Nagle 算法。
背景:Nagle 算法
Nagle 算法的作用:
把很多小的 TCP 包合并成一个大包再发送,以减少网络中小包的数量,提高效率。
规则:如果前一个包还没被确认(ACK),新的小包不要立刻发送,而是先缓存,等到收到 ACK 或者缓冲区积累够大时再发。
- 如果要发送的数据很大(≥ MSS,最大报文段长度),直接发。
- 如果应用要发小数据:
- 有未确认的包 → 暂时把新数据放在缓冲区里,不发。
- 收到了 ACK → 把缓存的数据打包一起发送。
代价:
会造成 延迟(例如即时通信、RPC 请求这种场景,一次 send() 可能会等下一个小包一起发)。
✅ 建议使用的场景:
❌ 不建议使用的场景:
开启 SO_KEEPALIVE 后,内核在后台做三件事(以 Linux 默认值为例,可能因系统不同而不同):
tcp_keepalive_time(默认 7200s = 2小时)
如果一个连接在 tcp_keepalive_time 时间内都没有任何数据往来,内核会开始发送探测包。
tcp_keepalive_intvl(默认 75s)
探测包之间的间隔。
tcp_keepalive_probes(默认 9次)
如果连续 tcp_keepalive_probes 次探测都没有回应,内核会认为连接已经断开,并通知应用层 read()/recv() 返回 0 或 ECONNRESET。
可以通过 /proc/sys/net/ipv4/tcp_keepalive_* 修改这些参数:
1 | cat /proc/sys/net/ipv4/tcp_keepalive_time |
应用场景
RESET vs KeepAlive
客户端正常断开
客户端调用 close() 或 shutdown()
会向服务端发送 FIN,服务端的 read() 返回 0
这是“主动优雅关闭”,服务端能马上感知
✅ 这种情况下不需要 SO_KEEPALIVE
客户端异常掉线(断电、拔网线、进程挂掉)
TCP 不能立即知道对方掉线
为什么?
✅ 所以需要 SO_KEEPALIVE 来让内核周期性探测,最终发现对方掉线
客户端发送 RST 的情况
| 场景 | 服务端能否立即发现 | 是否需要 Keepalive |
|---|---|---|
| 客户端正常 close() | 是 | 否 |
| 客户端异常掉线(断电、拔网线) | 否 | 是 |
| 客户端本地 abort() 或 write 已关闭套接字 | 是(会收到 RST) | 否 |
所以 SO_KEEPALIVE 主要用于发现客户端异常掉线。没有它,长连接可能永远挂在 ESTABLISHED,资源泄漏。
切换内核态和用户态为什么要内存复制?
| 问题 | 描述 |
|---|---|
| 安全性 | 用户程序可能传入非法地址,导致内核崩溃或被利用攻击 |
| 稳定性 | 用户程序可能在 poll() 返回前修改数组,导致内核读取不一致 |
| 内存管理 | 用户空间内存可能被 swap 出或者释放,内核无法保证访问有效 |
我们在说“切换到内核态”时,其实指的是 CPU 特权级别从用户态(ring3)切换到内核态(ring0) 的过程。
用户态 vs 内核态
用户态(user mode)
应用程序(比如你写的 C++ 程序)运行的环境。
权限受限,不能直接操作硬件(如磁盘、网卡),也不能随便访问内核内存。
只能通过 系统调用 (syscall) 向内核请求服务。
内核态(kernel mode)
操作系统内核运行的环境。
拥有最高权限(ring0),可以操作 CPU 指令集、内存管理、驱动、硬件寄存器。
系统调用的实现代码就在内核态里。
“切换到内核态”的过程
当你调用一个系统调用,比如:
read(fd, buf, size);
你的程序在 用户态,调用 read()。
实际上 read() 会触发一条特殊指令(例如 Linux x86-64 用 syscall 指令)。
CPU 捕捉到这个指令,自动:
从用户态切换到内核态。
切换到内核栈。
保存用户态寄存器上下文。
跳到系统调用处理函数(比如 sys_read)。
内核态代码执行 sys_read,比如访问文件系统、检查 socket buffer。
处理完后返回,CPU 再切回用户态,恢复上下文,继续执行应用代码。
🔹 为什么要区分用户态/内核态?
安全:用户程序不能直接操作硬件。
稳定性:即使用户程序崩溃,内核和其他进程不会受影响。
性能隔离:内核提供抽象(系统调用接口),避免应用直接干扰底层资源。
✅ 所以,总结一句:
“切换到内核态”就是 CPU 从运行普通用户代码(权限受限)切换到执行内核代码(最高权限)的过程,常常发生在系统调用、异常、硬件中断时。
C++11的值必定属于:左值、右值(将亡值、纯右值)三者之一。不是左值就是右值。详见值类别。
string之外字面值常量、函数返回值、运算表达式。示例:
1 | int main() { |
右值引用涉及“右值”和“引用”两个概念。
const左值引用可以接受左值或右值。示例:
1 | int a1 = 10; // 10是纯右值 |
引用(包括右值引用)本身是左值,可以取址,但不能对右值取址。
示例:
1 | #include <utility> |
因为输入参数是一个引用(右值引用也是引用),可以直接访问所引对象的资源并接管它,同时将源对象的资源置空。
示例:
1 | #include <iostream> |
完美转发是指对模板参数实现完美转发:即输入什么类型(左值、右值)的参数,就是什么类型的参数。
示例:
1 | #include <iostream> |
noexcept关键字。std::move_if_noexcept可以在移动构造函数抛出异常时回退到复制构造函数。示例:
1 | #include <iostream> |
编译器默认会采用“返回值优化”(RVO或NRVO)。要观察移动语义与复制语义的不同,应该关闭编译器优化。
关闭优化命令:
1 | g++ -o test main.cc -fno-elide-constructors |
如果没有定义复制构造/赋值函数,编译器会为我们合成(浅复制)。但如果自定义了复制构造函数、复制赋值运算符或析构函数,编译器将不会合成移动构造/赋值函数。
示例:
1 | #include <iostream> |
std::move的实现std::move是一个类型转换,没有完成其他工作。
实现:
1 | template<class T> |
unique_ptr与std::move示例:
1 | #include <iostream> |
解释:std::move调用unique_ptr的移动构造函数,转移up所拥有的资源,并将up置为空。
sizeof示例:
1 | struct A { |
代码:
1 | getaddrinfo(localhost, "sunrpc", &hints, &result); // "sunrpc" 或 端口 111 都可以 |
这段代码,connect() 返回 -1 ,报错为 Protol not available 。
getaddrinfo() 查询到的地址信息,为什么报错?IPv4 ,没有开启 IPv6,如果开启 IPv6,这个错误会消失,不支持 IPv4 ?运行这些命令:
hostnamehostname -fhostname -ahostname -Ihostnamectlcat /proc/sys/kernel/hostnamemore /etc/host*/sbin/ifconfig -a1 | $ hostname |
注:上面 cat /etc/hosts 的第二个 IP 进行了个人信息隐藏。xx 代表数字。
说明:
| 地址 | 协议版本 | 作用 |
|---|---|---|
| 127.0.0.1 | IPv4 | 本地回环地址,用于本机内部通信 |
| ::1 | IPv6 | 本地回环地址,用于本机内部通信 |
于是问题找到了:/etc/hosts/ 只配置了 ::1 ,那么使用 IPv4 协议尝试 connect localhost 就会返回 -1 。