Linux基础

Vim 记录 Linux 基本概念、结构和用法

Kernel, Shell, Bash

通常我们说的 Linux 其实就是”Linux Kernel(内核)”的简称,它提供了 Linux 最核心的功能。而Shell直译为外壳,它基于 Linux Kernel,为用户提供操作 Linux 的各种命令接口或者为程序提供函数接口。Bash是 Linux 自带的一个终端(terminal)命令行工具软件,提供命令行界面来执行Shell命令或其它可可执行文件。

文件

unix/linux 系统的基本思想是:一切皆文件。即各种文件、设备都可以像文件一样进行操作,以流的方式读入、输出、跳转。

inode(index node) 和 fd(file descriptor) 的关系

inode 是文件的索引,记录文件的位置、属性、大小等,大概磁盘百分之一会用来保存 inode,并且 inode 数量是有限的(虽然一般情况下不会超限)。

linux 是多用户系统,所以一个文件可能被多个用户打开,这时就需要为每一个打开记录 offset,这就需要用 fd 资源记录。

  • 一个 inode 可以对应于多个 fd。
  • 可以用ulimit -n查看用户可占用的最大 fd 数量,这个数量默认为 256,是可以修改的。超过会报错:”too many open files”

hard link(硬连接), symbolic link(符号连接/软连接), alias(mac)

每个 linux 文件实体都会绑定一个唯一的 Inode Index(节点索引),并且会默认分配一个 hard link,这个 hard link 就是我们通常看到的一个”文件”,结构如下: 可以看到的”文件”(hard link/symbolic link/alias) -> Inode Index(文件实体)

  • hard link 会直接指向文件,对 hard link 的操作会直接影响文件本身。可能存在多个 hard link 指向同一个 Inode Index 的情况,这时删除其中一个 hard link 并不会删除文件,当所有 hard link 被删除时会真正删掉文件。
  • symbolic link 是一个独立的文件,其中存储着一个特定的文件路径。删除 symbolic link 不会对实体文件或指向实体文件的 hard link 有任何影响。而删除或移动文件后,指向它的 symbolic link 会失效。
  • alias 是 mac 系统提供的一种文件,类似 symbolic link 也是一个独立的文件,相比 symbolic link 增加了 Inode Index 信息,可以自动追踪被移动的文件,不会因为文件移动位置而失效。alias 只在 mac 系统下起作用。
  • 使用ls -li命令可以查看文件的 Inode Index 和软连接信息。注意 alias 在 linux 中只是一个普通的文件,并不会认为有连接关系。
  • 执行命令时可以看到容量关系 hard link > alias > symbolic link,与前面的说明一致,hard link直接显示文件本身的容量,aliassymbolic link包含的信息更多所以容量更大。

  • 软链接:
    1. 软链接是存放另一个文件的路径的形式存在。
    2. 软链接可以跨文件系统 ,硬链接不可以。
    3. 软链接可以对一个不存在的文件名进行链接,硬链接必须要有源文件。
    4. 软链接可以对目录进行链接。
  • 硬链接:
    1. 硬链接,以文件副本的形式存在。但不占用实际空间。
    2. 不允许给目录创建硬链接。
    3. 硬链接只有在同一个文件系统中才能创建。
    4. 删除其中一个硬链接文件并不影响其他有相同 inode 号的文件。
  • 硬链接的作用:
    1. 节省硬盘空间。bai 同样的文件,只需要 du 维护硬连接关系,不需要进行多重的 zhi 拷贝,这样可以节省硬盘空间。
    2. 重命名文件。重命名文件并不需要打开该文件,只需改动某个目录项的内容即可。
    3. 删除文件。删除文件只需将相应的目录项删除,该文件的链接数减 1,如果删除目录项后该文件的链接数为零,这时系统才把真正的文件从磁盘上删除。
    4. 文件更新。如果涉及文件更新,只需要先在 WinSxS 目录里面下载好一个新版本,然后修改面同名文件的硬即可改变;

一个文件包含两个参数i_counti_link。i_count 表示被调用的数量;i_link 表示被介质连接的数量(硬连接)。当它们同时为 0 时,会被系统删除。

  • 例如:用rm命令删除的文件,i_link 减 1,如果此时这个文件还在被某个进程使用,则 i_count 不为 0,该文件还可以继续被该进程使用,直到进程释放资源后才会被删除。

脚本文件

  • 脚本文件的第一行一般是#!/bin/bash表示这是个脚本文件,并且可以使用/bin/bash这个可执行文件来运行这个脚本,其他常见的有:
    • #!/usr/bin/python python 脚本
    • #!/bin/awk 用 awk 执行
    • #!/bin/expect expect 脚本
    • #!/bin/ksh

文件夹结构

/tmp/ 该文件夹下可以存放临时文件,Debian/Ubuntu 每次启动会清空一次,RHEL/CentOS/Fedora 每隔特定天数(至少一天)会清空一次近期未更新的文件。

/var/log/ 系统日志目录

/etc/resolv.conf 域名解析服务器 IP 地址

常用服务

crontab

cron 是 Linux 自带的 service 提供定时执行命令的功能,类似 Windows 任务计划。

crontab -e 打开 cron 的定时任务编辑页面,可参照说明进行编辑。 编辑完成退出后显示crontab:installing new crontab表示成功添加任务,不过这个任务要在三分钟后才生效,所以时间过近的不会执行。

  • -l 显示已有的配置列表
1
2
分 时 日 月 周 命令
#第一列代表分钟1~59:为*表示每分钟都要执行;为*/n表示每n分钟执行一次;为a-b表示从第a分钟到第b分钟这段时间每分钟要执行;a-b/c表示从a-b这段时间每c分钟执行一次;为a,b,c,...表示第a,b,c分钟要执行
  • cron 可执行文件位于/usr/sbin/crond
  • 不同的用户配置不同,root 用户用sudo crontab -e进入配置

  • 可以用下面语句对 cron 进行服务操作 /sbin/service crond start //启动服务 /sbin/service crond stop //关闭服务 /sbin/service crond restart //重启服务 /sbin/service crond reload //重新载入配置

  • 在/etc/rc.d/rc.local 这个脚本的末尾加上: /sbin/service crond start 则系统启动时就会运行 cron 服务

服务管理

Linux 服务管理两种方式 service 和 systemctl

systemd 是 Linux 系统最新的初始化系统(init),作用是提高系统的启动速度,尽可能启动较少的进程,尽可能更多进程并发启动。

systemd 对应的进程管理命令是 systemctl

systemctl 命令兼容了 service,即 systemctl 也会去/etc/init.d 目录下,查看,执行相关程序

常用命令

  • systemctl redis start 启动服务

  • systemctl redis stop 关闭服务

  • systemctl enable redis 开机启动

  • systemctl --user ... 通常要加上--userflag,管理用户空间的服务
  • restart 重启
  • reload 重新载入配置。只修改不载入不会生效。
  • disable 关闭开机启动
  • status 查看状态
  • systemctl list-units --type=service -all 列出所有服务

  • 有时修改了配置文件路径,查找对应的配置文件路径:find / -name "<service_name>.service" 2>/dev/null

配置文件

  • Linux 中.service 文件是某项服务对应的配置文件,可用于 systemd 管理和控制的服务的设置。
  • .service 文件通常包含 3 个模块,即[Unit]控制单元,表示启动顺序和依赖关系;[Service]服务,表示服务的定义;[Install]安装,表示如何安装配置文件。
  • .service 文件配置的服务常用 systemd 管理。然而,systemd 有系统和用户区分;系统(/user/lib/systemd/system/)、用户(/etc/lib/systemd/user/)。一般系统管理员手工创建的单元文件建议存放在/etc/systemd/system/目录下面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=httpd	    #当前配置文件的描述信息
After=network.target    #表示当前服务是在那个服务后面启动,一般定义为网络服务启动后启动

[Service]
Type=forking			#定义启动类型
ExecStart=/usr/local/apache/bin/apachectl start 	#定义启动进程时执行的命令。
ExecReload=/usr/local/apache/bin/apachectl restart  #重启服务时执行的命令
ExecStop=/usr/local/apache/bin/apachectl stop		#定义关闭进程时执行的命令。
PrivateTmp=true										#是否分配独立空间
Environment="SECRET=123"  # 设置多个环境变量
Environment="ANOTHER_SECRET=abc"

[Install]
WantedBy=multi-user.target    #表示多用户命令行状态

内核机制

系统中断

中断是 CPU 的基本工作模式,当设备/软件状况发生变化时,就会发起中断,切换 CPU 的上下文进行另一个处理。

  • IRQ(Interrupt Request)中断请求 中断请求通常由硬件发出,通过 CPU 的中断针脚把信号传入 CPU,标明某硬件需要 CPU。一旦 CPU 接受请求,则会停止当前执行的程序,执行一段中断程序进行上下文切换,然后执行另一段程序。
  • 中断处理程序 CPU 执行这段程序的目的是进行程序切换,将运行队列中的当前程序暂停、保存寄存器、放到等待队列,将等待队列的程序寄存器恢复、放到运行队列、状态改为 ready。
  • 中断向量 每一种中断,都有对应的一个中断编号,同时一个编号对应一个中断处理程序(如鼠标、硬盘、网卡、80软中断)。中断向量就是由中断编号+中断处理程序入口(中断向量地址)组成的一条,存放在中断向量表中,中断向量表在内存中的特殊位置。
  • 硬中断 由硬件引发的中断,直接通过 CPU 中断针脚发起中断
    • 硬中断是异步中断,可能在任何时间点发生
    • 如果同时发生了多个硬中断,则会在中断控制器中排入队列按优先级处理
    • NMI(Non-maskable interrupts)不可屏蔽中断 这种中断是不可能被 CPU 忽略或取消的。NMI 是在单独的中断线路上进行发送的,它通常被用于关键性硬件发生的错误,如内存错误,风扇故障,温度传感器故障等。
    • MI(Maskable interrupts)可屏蔽中断 这些中断是可以被 CPU 忽略或延迟处理的。当缓存控制器的外部针脚被触发的时候就会产生这种类型的中断,而中断屏蔽寄存器就会将这样的中断屏蔽掉。
  • 软中断 CPU 执行指令时产生的。例如”除零异常”,例如常见的”80 中断”是用户态调用内核函数时调用中断号为0x80(是十六进程,十进制对应 120)的中断程序,进入内核态函数调用。

OOM killer(Out Of Memory killer)

当系统内存不足时,会杀掉占用内存最大的程序,并输出系统日志。 查看相关日志的命令grep "Out of memory" /var/log/messagesegrep -i -r 'killed process' /var/log

进程

  • 进程描述符 在 linux 中,每一个进程都有一个进程描述符,这个”进程描述符”是一个结构体名字叫做 task_struct,在 task_struct 里面保存了许多关于进程控制的信息。task_struct 是 Linux 内核的一种数据结构,它会被装载到 RAM 里并包含进程的信息。
    • 标示符:描述本进程的唯一标示符,用来区别其他进程。
    • 状态:任务状态,退出代码,退出信号等。
    • 优先级:相对于其他进程的优先级。
    • 程序计数器:程序中即将被执行的下一条指令的地址。
    • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
    • 上下文数据:进程执行时处理器的寄存器中的数据。
    • I/O 状态信息:包括显示的 I/O 请求,分配给进程的 I/O 设备和正在被进程使用的文件列表。
    • 记账信息:可能包括处理器时间总和,使用的时钟总数,时间限制,记账号等。

Session

通常一个进程是属于一个进程组的,而每个进程组一定有个开端进程,这个进程就是session leader(简称 SL)。 通常用 terminal 连接时会启动一个session并设置当前进程为session leader。 SL 可以启动新的进程,都归属于同一个 session。

  • 进程以前台(阻塞)方式启动一个子进程时,signal 会发送给子进程
  • session leader 如果退出,会向所有同 session 内的进程发 SIGHUP 信号,默认动作是关闭。所以 session leader 退出通常会导致所有子进程退出。
  • 在 terminal 利用nohup启动子进程时,nohup 进程会重定向 stdin、stdout、stderr,并且会屏蔽掉 SIGHUP 信号,这样就可以保护子进程不被 terminal 退出影响了。
  • nohup 默认是在前台执行,所以通常要在命令末尾加&符号,以便后台执行

内核态、用户态

在 Linux 操作系统中,一个进程的内存空间被分为两部分:内核堆栈、用户堆栈,对应的进程有两个权限状态:内核态、用户态。 内核态为完全可信的内核代码,可以执行高风险的 CPU 指令(0 等级),可以与硬件交互,可以操作内核堆栈和用户堆栈; 用户态为程序自己的代码,可以执行低风险的 CPU 指令(3 等级),不可以直接与硬件交互,可以操作用户堆栈,可以通过内核函数调用与内核态资源交互。

当用户态和内核态相互切换时,CPU 寄存器就会进行备份、切换,涉及到寄存器(示例):代码地址、代码行号、堆栈地址、操作数 1、操作数 2…

什么情况下会发生用户态、内核态切换?

  • 系统调用用户态代码需要申请资源或访问硬件时,需要调用内核函数
  • 硬中断外部设备就绪
  • 发生异常用户态代码发生”除 0”等错误时

一次系统调用流程

  • 在用户态代码中调用系统函数
    • 设置 eax 寄存器系统调用号,如 198
    • 将参数封装成指针,保存到 CPU 特定的寄存器中
    • 保存当前进程上下文信息到进程描述符
    • 切换进程内核栈
    • 根据映射表加载 system_call 的地址到代码段寄存器
  • 发起一个 0x80 中断(由于向量表容量有限,80 中断相当于系统函数路由)
  • CPU 开始执行内核空间的程序,返回结果后切换回用户态
    • 读取 eax 寄存器的值
    • 根据上面的值找到对应的系统函数指针
    • 将参数拷贝到内核空间,读取参数
    • 调用系统函数
    • 将结果封装成指针保存到 CPU 的寄存器中
    • 恢复用户态寄存器上下文,返回用户态
  • 在用户态从寄存器读取系统调用结果,继续执行其他代码

用户态、内核态切换耗时的因素(按耗时排序)

  1. 进程(线程)切换导致 CPU 缓存失效(一、二、三级)
  2. 用户态和内核态之间拷贝大量数据(如 fd 较多的 select 调用)
  3. 切换到内核态时,进行安全检查和处理
  4. 切换时各个寄存器的保存(寄存器内容复制到内存中)、恢复(内存内容复制到寄存器)
  • 在进程切换时,大概要执行 100 万~ 800 万条指令
  • 对于 CPU 密集型,如果不是特别简单的计算,那么切换成本并不算高;对于 IO 密集型,由于 IO 不断变化,线程来回切换,积累的切换成本很高、比例大。 在服务端开发中,越来越趋于(网络)IO 密集型,所以对用户态内核态切换的性能耗时越来越重视。

fd(file description)

每一个进程在内核中,都对应有一个”打开文件”数组,存放指向文件对象的指针,而 fd 是这个数组的下标。 我们对文件进行操作时,系统调用,将 fd 传入内核,内核通过 fd 找到文件,对文件进行操作。

既然是数组下标,fd 的类型为 int, < 0 为非法值, >=0 为合法值。在 linux 中,一个进程默认可以打开的文件数为 1024 个,fd 的范围为 0~1023。可以通过设置,改变最大值。

在 linux 中,值为 0、1、2 的 fd,分别代表标准输入、标准输出、标准错误输出。因为 0 1 2 已经被 linux 使用了,通常在程序中打开的 fd,是从 3 开始的。但我们在判断一个 fd 是否合法时,依然要使用>=0 的判断标准。

fd 的分配原则,是从小到大,找到第一个不用的进行分配。

除了 open 之外, socket 编程的 socket()/accept()等函数,也会返回一个 fd 值。

cat /proc/sys/fs/file-max 查询所有进程允许打开的最大 fd 数量

cat /proc/sys/fs/file-nr 查询所有进程已经打开的 fd 数量及允许的最大数量

ulimit -n 查询单个进程允许打开的最大 fd 数量

ls -l /proc/1234/fd/ 查询单个进程(进程 id 为 1234)已经打开的 fd

修改单个进程 fd 限制数

用 root 账户修改/etc/security/limits.conf文件,最后增加两行

* soft nofile 1234
* hard nofile 1234

修改之后,不需重启服务器,只需退出当前 ssh 远程连接,重新登录之后重启相关程序即可生效。

  • 可用 root 账户修改/etc/sysctl.conf调整系统最大 fd 数

进程线程

Linux 最初是没有线程的设计的,后来其他操作系统有了线程,Linux 就通过进程模拟线程,通过 fork 创建进程将新的进程的资源句柄与原进程一致,这样就实现了线程的功能。

内存

可以用free命令查看内存情况

  • used 已使用的内存容量,包含 cached buffers shared 三部分
  • free 空闲内存容量
  • cached 这里指内存与硬盘间的 cache,主要针对读操作设计。把频繁从硬盘读取的数据缓存起来,之后如果再访问可直接从内存读取,减少磁盘访问耗时。
  • buffers 把要写到硬盘的数据先缓存一下,稍后再刷到硬盘上,这样避免频繁写操作耗时。Linux 有一个守护进程定期清空缓存内容将其写入硬盘,手动执行 sync 命令也可以把数据刷到硬盘。
  • shared 进程间共享内存的容量
  • swap 交换区,相当于 Windows 的虚拟内存。当内存不足时,将硬盘用作部分内存使用,扩展内存容量。

CPU

可以用top命令查看 CPU 情况

  • load CPU 负载,表示排队等待 CPU 的进程数量。如果load average除以 CPU 数量 > 5,则表明 CPU 可能超负荷运行,需要关注。
    • CPU 负载高的可能原因:1. 正在进行 CPU 密集型计算;2. 可能 CPU 正在等待 IO(内存、硬盘、网络);
  • us(user cpu time) 用户态使用 CPU 占比。
  • sy(system cpu time) 内核态(系统态)使用 CPU 占比。
  • wa(io wait cpu time) CPU 等待 IO 的时间占比。如果该值较高,说明 IO 出现了瓶颈,需要具体分析是磁盘/内存/网络。

可以用nice命令来调整进程优先级。范围为-20(最高)~19(最低),默认值为 10。

Cache

高速缓存,位于 CPU 与内存之间的容量小速度快的存储器。根据程序的局部性原理设计,将频繁访问的数据放入 Cache 中,避免频繁读内存。Cache 分为 L1 Cache 和 L2 Cache,L1 Cache 位于 CPU 内部,速度更快容量更小,L2 Cache 以前位于主板,后来也直接封装到 CPU 中。

  • Cache 是一种设计思想,可以用于各种速度不匹配的存储之间,提高读取效率

Buffer

缓冲区,用于不同速度存储之间传输数据,避免通信次数过多造成效率低下。 用于写入时,可以将要写入的数据先放入高速存储的 Buffer,然后再定时一次性写入低速存储; 用于读取时,可以将要读取的低速存储中的数据包含附近数据一次性读取高速存储的 Buffer,然后慢慢处理。

  • Buffer 和 Cache 一样是一种设计思想,用于速度不匹配的存储之间,提高写入效率(有时也用于读取,比如从磁盘读取整块数据块)

网络通信

服务端连接过程

  1. 服务端程序 bind 一个端口,占用这个端口资源,端口与网卡绑定。这个过程中,进程的内核内存会分配一块内存,作为网卡的 buffer。
  2. 服务端程序 liseten,开始监听这个端口。
  3. 服务端程序 accept,阻塞,等待客户端连接。
  4. 服务端 accept 到新的 socket 连接后,开始与客户端通信。

网络通信中断过程

  1. 网卡接收到 socket 数据后,由 DMA(Direct Memory Access)控制器直接控制,把数据从网卡接收 buffer 移动到端口对应进程的内核对应接收 buffer 中。
    • 网卡自身的硬件缓冲区通常很小,比如 128KB,所以会尽快转移到内存缓冲区中
  2. 网卡向 CPU 发起一个中断,CPU 响应中断请求,执行中断处理程序,把当前运行的进程 A 的寄存器信息保存到对应的进程描述符中,然后由 A 的用户态切换到内核态
    • 这个中断请求是可忽略中断请求,CPU 可以不立即响应
  3. 在内核态执行网卡的中断处理程序,将之前读取网络数据并被阻塞的 B 唤醒为 ready 状态,放到运行队列
  4. 由于某种机会,B 被运行,继续执行 receive 函数,开始处于内核态,把内核内存 buffer 中的数据拷贝到用户内存 buffer 中,然后切换到 B 的用户态
  5. B 的用户态处理 buffer 中的数据

BIO(Blocked IO)/NIO(Non-blocked IO)

无论是 BIO 还是 NIO 函数调用,都需要从用户态切换到内核态,然后再切换回来。如果被阻塞,则需要切换线程。

  • BIO 读取时,如果有数据,则返回;如果没有数据,则阻塞等待
  • BIO 发送时,如果内核发送缓冲区容量充足,则直接放入后返回到用户态;如果内核发送缓冲区容量不足,则阻塞等待,直到容量充足,则放入数据后返回到用户态

    • BIO 发送时,如果操作系统的 TCP 协议正在读取内核发送缓冲区,则会上锁,所以这个过程也是被阻塞的
    • BIO 发送时,如果数据容量超过了内核发送缓冲区容量,则会分多次发送,期间在网络通信时进程会被挂起
  • NIO 读取时,如果有数据则读取,立即返回;如果没有数据,则立即返回
  • NIO 发送时,如果内核发送缓冲区容量充足,则放入后立即返回;如果内核发送缓冲区容量不足,则尽量放入,然后立即返回已写入缓冲区的容量

  • 如果一个进程内,根据多个网络 IO 的情况,选择使用 BIO 和 NIO,可以尽量避免进程切换,使 CPU 使用效率最大化。