Linux

2019/12/03 Knowledge

参考来源

目录

一、常用概念及操作

sudo

sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。

包管理工具

RPM 和 DPKG 为最常见的两类软件包管理工具:

  • RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为许多 Linux 系统的既定软件标准。YUM 基于 RPM,具有依赖管理和软件升级功能。
  • 与 RPM 竞争的是基于 Debian 操作系统的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。

发行版

基于的包管理工具 商业发行版 社区发行版
RPM Red Hat Fedora / CentOS
DPKG Ubuntu Debian

vim 三个模式

  • 一般指令模式(Command mode):VIM 的默认模式,可以用于移动游标查看内容;
  • 编辑模式(Insert mode):按下 “i” 等按键之后进入,可以对文本进行编辑;
  • 指令列模式(Bottom-line mode):按下 “:” 按键之后进入,用于保存退出等操作。

GNU

GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议(GNU General Public License),包含了以下内容:

  • 以任何目的运行此程序的自由;
  • 再复制的自由;
  • 改进此程序,并公开发布改进的自由。

磁盘

磁盘接口

M.2,U.2,AIC 是物理规格,像是 公路,铁路。

PCIe(比 SATA 快),SATA(最常用),SAS,IDE(最慢) 是 模拟高速接口,像是 县道,省道,高速这样。速率上限不同

SCSI,ATA,NVMe 是传输层协议,命令集。就是跑在路上面的小车,只是有 跑车 和 面包车 之分。

磁盘的文件名

Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘接口类型进行命名,常见磁盘的文件名如下:

  • IDE 磁盘:/dev/hd[a-d]
  • SATA/SCSI/SAS 磁盘:/dev/sd[a-p]

其中文件名后面的序号的确定与系统检测到磁盘的顺序有关,而与磁盘所插入的插槽位置无关。

三、分区

将一个磁盘分成多个区,可以挂载在多个目录下。

分区的好处:

  • 主要方面:只用一个分区,若遇到系统需要重装或者分区需要进行格式化等,原有的重要文件无法在本硬盘保留,而若提前进行了合理分区,则用户数据不会收到影响。
  • 次要方面:同等外部条件下,读取越频繁,磁盘越容易受损,我们把读写频繁的目录挂载到一个单独的分区,可以把磁盘的损伤控制在一个集中的区域。
  • 效率方面:分区将数据集中在某个磁柱的区段,当有数据要读取自该分区时,硬盘只会搜寻相应区段,有助于数据读取的速度与效能的提升!

分区表

磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。

1. MBR

MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中主要开机记录占 446 bytes,分区表占 64 bytes。

分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它使用其它扇区来记录额外的分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。

Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。

2. GPT

扇区是磁盘的最小存储单位,旧磁盘的扇区大小通常为 512 bytes,而最新的磁盘支持 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA),LBA 默认大小为 512 bytes。

GPT 第 1 个区块记录了主要开机记录(MBR),紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。

GPT 没有扩展分区概念,都是主分区,每个 LBA 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。

MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 233 TB = 8 ZB。

GPT

开机检测程序

BIOS

BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中(Read Only Memory)。

主要开机记录(MBR)中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。

UEFI

BIOS 不可以读取 GPT 分区表,而 UEFI 可以。Mac 用的就是简化版的 UEFI。

四、文件系统

各系统文件系统

  • Windows:FAT、NTFS、HPFS
  • Linux:ext4
  • Mac OS:HFS+

分区与文件系统

对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统。

组成

最主要的几个组成部分如下:

  • inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号;
  • block:记录文件的内容,文件太大时,会占用多个 block。 除此之外还包括:
  • superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等;
  • block bitmap:记录 block 是否被使用的位图。

文件系统

上图是一个分区的存储结构。

文件读取过程

对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中查找文件内容所在的所有 block,然后把所有 block 的内容读出来。

ext2

ext3 解决了 ext2 中断电容易导致文件系统丢失瘫痪的问题,使用日志解决这一问题

ext4 在功能上与 ext3 在功能上非常相似,但支持大文件系统,提高了对碎片的抵抗力,有更高的性能以及更好的时间戳。

而对于 FAT 文件系统,它没有 inode,每个 block 中存储着下一个 block 的编号。

fat

磁盘碎片

指一个文件内容所在的 block 过于分散,导致磁盘磁头移动距离过大,从而降低磁盘读写性能。

inode

一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号;

文件储存在硬盘上,硬盘的最小存储单位叫做”扇区”(Sector)。每个扇区储存512字节(相当于0.5KB)。

操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个”块”(block)。这种由多个扇区组成的”块”,是文件存取的最小单位。”块”的大小,最常见的是4KB,即连续八个 sector组成一个 block。 文件数据都储存在”块”中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为”索引节点”。

每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。

inode包含文件的元信息,具体来说有以下内容:

  • 文件的字节数
  • 文件拥有者的User ID
  • 文件的Group ID
  • 文件的读、写、执行权限
  • 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
  • 链接数,即有多少文件名指向这个inode
  • 文件数据block的位置

可以使用 stat 命令,查看文件 inode 信息:

[root@ambari-namenode spark-ml]# stat start.sh 
  File: "start.sh"
  Size: 448       	Blocks: 8          IO Block: 4096   普通文件
Device: fd00h/64768d	Inode: 8214        Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2019-12-03 21:30:27.033069000 +0800
Modify: 2019-12-03 21:30:24.931069001 +0800
Change: 2019-12-03 21:30:24.981069001 +0800

inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用让 inode 记录的引用 block 块记录引用信息。

inode

inode 也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是 inode 区(inode table),存放 inode 所包含的信息。

每个 inode 节点的大小,一般是 128 字节或 256(ext4 和 xfs)字节。inode 节点的总数,在格式化时就给定,一般是每1KB或每 2KB 就设置一个 inode。假定在一块 1GB 的硬盘中,每个 inode 节点的大小为 128 字节,每 1KB 就设置一个 inode,那么 inode table 的大小就会达到 128MB,占整块硬盘的 12.8%。

查看每个硬盘分区的 inode 总数和已经使用的数量,可以使用 df 命令。

[root@ambari-namenode spark-ml]# df -i
文件系统	      Inode  已用(I)  可用(I) 已用(I)%% 挂载点
/dev/mapper/VolGroup-lv_root
                     1150560  129804 1020756   12% /
tmpfs                1007453       4 1007449    1% /dev/shm
/dev/vda1             128016      39  127977    1% /boot
/dev/vdb             6553600     122 6553478    1% /var/run/spark2/work

由于每个文件都必须有一个inode,因此有可能发生inode已经用光,但是硬盘还未存满的情况。这时,就无法在硬盘上创建新文件。

每个inode都有一个号码,操作系统用inode号码来识别不同的文件。

这里值得重复一遍,Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。

表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。

使用ls -i命令,可以看到文件名对应的inode号码(与 stat 一样得数):

[root@ambari-namenode spark-ml]# ls -i start.sh 
8214 start.sh

目录

为什么 inode 信息中没有文件名?

Unix/Linux系统中,目录(directory)也是一种文件。打开目录,实际上就是打开目录文件。

建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。

可以看到文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的写权限有关。

使用 ls -i 可以显示所有目录的文件名和 inode 号码:

[root@ambari-namenode spark-ml]# ls -i /opt/spark-ml/
 8418 chuandi.sh                       9026 nohup.out
 8606 kafka-avro-producer.properties  16321 spark-warehouse
 8163 lib                              8214 start.sh
 9030 ml-online-1.0-SNAPSHOT.jar

block

一个 block 只能被一个文件所使用,未使用的部分直接浪费了。因此如果需要存储大量的小文件,那么最好选用比较小的 block。

记录文件的内容,文件太大时,会占用多个 block。

使用 fdisk -l 查看硬盘:

[root@ambari-namenode spark-ml]# fdisk -l

Disk /dev/vda: 21.5 GB, 21474836480 bytes
16 heads, 63 sectors/track, 41610 cylinders
Units = cylinders of 1008 * 512 = 516096 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00056b13

16 heads(磁头个数), 63 sectors/track(扇区个数), 41610 cylinders(柱面) 相乘再乘以扇区大小(512 bytes)就可以算总容量(21474836480 bytes)。

注意:硬盘的最小存储单位就是扇区了,而且硬盘本身并没有block的概念。

文件系统不是一个扇区一个扇区的来读数据,太慢了,所以有了block(块)的概念,它是一个块一个块的读取的,block才是文件存取的最小单位。

df -T 查看文件系统

[root@ambari-namenode spark-ml]# df -T
文件系统    类型         1K-块      已用      可用 已用% 挂载点
/dev/mapper/VolGroup-lv_root
              ext4    18102140  14621960   2560628  86% /
tmpfs        tmpfs     4029812        12   4029800   1% /dev/shm
/dev/vda1     ext4      495844     37564    432680   8% /boot
/dev/vdb      ext4   103212320    202472  97766968   1% /var/run/spark2/work

查看 Block 大小:

[root@ambari-namenode spark-ml]# tune2fs -l /dev/mapper/VolGroup-lv_root | grep "Block size"
Block size:               4096

可以看见 Block 大小是 4096B 即 4KB,也就是说我所使用的文件系统中1个块是由连续的8个扇区组成。

日志

如果突然断电,那么文件系统会发生错误,例如断电前只修改了 block bitmap,而还没有将数据真正写入 block 中。

ext3/ext4 文件系统引入了日志功能,可以利用日志来修复文件系统。

挂载

挂载利用目录作为文件系统的进入点,也就是说,进入目录之后就可以读取文件系统的数据。

目录配置

为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下:

  • / (root, 根目录)
  • /usr (unix software resource):所有系统默认软件都会安装到这个目录;
  • /var (variable):存放系统或程序运行过程中的数据文件。

一个分区 + 格式化完整流程事例 应用于此项目

将数据盘配置 LVM,挂载在 /hadoop 目录下,作为 Hadoop 各组件的存储目录。

  1. 首先将数据盘使用 parted 进行 gpt 分区(不能使用 fdisk(是 FAT 分区),它不能识别 2T 以上的硬盘)。命令中的硬盘大小(3000G)和硬盘路径(/dev/sdb)可通过 fdisk -l 查看。

     [root@activenamenode ~]# parted /dev/sdb
     GNU Parted 2.1
     使用 /dev/sdb
     Welcome to GNU Parted! Type 'help' to view a list of commands.
     (parted) mklabel
     新的磁盘标签类型? gpt
     警告: The existing disk label on /dev/sdb will be destroyed and all data on this disk will be lost. Do you want to continue?
     是/Yes/否/No? y
     (parted) mkpart
     分区名称?  []? gpt1
     文件系统类型?  [ext2]? ext4
     起始点? 0
     结束点? 3000G
     警告: The resulting partition is not properly aligned for best performance.
     忽略/Ignore/放弃/Cancel? i
     (parted) print
     Model: LSI MR9271-8i (scsi)
     Disk /dev/sdb: 3000GB
     Sector size (logical/physical): 512B/512B
     Partition Table: gpt
     Number  Start   End     Size    File system  Name  标志
     1      17.4kB  3000GB  3000GB               gpt1
     (parted) quit
    
  2. 创建物理卷。

    text[root@activenamenode ~]# pvcreate /dev/sdb1 Physical volume "/dev/sdb1" successfully created

  3. 创建卷组。

    text[root@activenamenode ~]# vgcreate vg_data /dev/sdb1 Volume group "vg_data" successfully created

  4. 查看卷组大小。记录结果中 vg_data 卷组的 Total PE,下一步需要用到。

    text[root@activenamenode ~]# vgdisplay --- Volume group --- VG Name vg_data System ID Format lvm2 Metadata Areas 1 Metadata Sequence No 2 VG Access read/write VG Status resizable MAX LV 0 Cur LV 1 Open LV 0 Max PV 0 Cur PV 1 Act PV 1 VG Size 2.73 TiB PE Size 4.00 MiB Total PE 715263 Alloc PE / Size 715263 / 2.73 TiB Free PE / Size 0 / 0 VG UUID fHWI3Z-zJBs-NcKE-1FMF-vMYf-AZMb-ATmb3j ……

  5. 创建逻辑卷,-l 指定逻辑卷大小,是 vg_data 卷组的 Total PE。

    [root@activenamenode ~]# lvcreate -l 715263 vg_data Logical volume "lvol0" created

  6. 查看逻辑卷路径,后续所有操作均需要使用。

    [root@activenamenode ~]# lvscan ACTIVE '/dev/vg_data/lvol0' [2.73 TiB] inherit ACTIVE '/dev/vg_system/LogVol00' [495.71 GiB] inherit

  7. 格式化 ext4 分区。

    [root@activenamenode ~]# mkfs.ext4 /dev/vg_data/lvol0 mke2fs 1.41.12 (17-May-2010) 文件系统标签= 操作系统:Linux 块大小=4096 (log=2) 分块大小=4096 (log=2) Stride=0 blocks, Stripe width=0 blocks 183107584 inodes, 732429312 blocks 36621465 blocks (5.00%) reserved for the super user 第一个数据块=0 Maximum filesystem blocks=4294967296 22352 block groups 32768 blocks per group, 32768 fragments per group 8192 inodes per group Superblock backups stored on blocks: 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 102400000, 214990848, 512000000, 550731776, 644972544 正在写入inode表: 完成 Creating journal (32768 blocks): 完成 Writing superblocks and filesystem accounting information: 完成 This filesystem will be automatically checked every 32 mounts or 180 days, whichever comes first. Use tune2fs -c or -i to override.

  8. 挂载分区。

     [root@activenamenode ~]# mount /dev/vg_data/lvol0 /hadoop
    
  9. 设置系统启动自动挂载。

     [root@activenamenode ~]# echo "/dev/mapper/vg_data-lvol0       /hadoop         ext4    defaults        0 0" >> /etc/fstab
    

五、文件

文件属性

用户分为三种:文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。

使用 ls 查看一个文件时,会显示一个文件的信息,例如 drwxr-xr-x 3 root root 17 May 6 00:14 .config,对这个信息的解释如下:

  • drwxr-xr-x:文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段
  • 3:链接数
  • root:文件拥有者
  • root:所属群组
  • 17:文件大小
  • May 6 00:14:文件最后被修改的时间
  • .config:文件名

常见的文件类型及其含义有:

  • d:目录
  • -:文件
  • l:链接文件

9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r(4)、w(2)、x(1) 权限,表示可读、可写、可执行。

文件时间有以下三种:

  • modification time (mtime):文件的内容更新就会更新;
  • status time (ctime):文件的状态(权限、属性)更新就会更新;
  • access time (atime):读取文件时就会更新。

默认权限

  • 文件默认权限:文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。
  • 目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。

可以通过 umask 设置或者查看默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r–。

文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。

目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。

链接

链接

# ln [-sf] source_filename dist_filename
-s :默认是实体(硬)链接,加 -s 为符号(软)链接
-f :如果目标文件存在时,先删除目标文件
  • 实体链接(硬链接)

    在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode。

    删除任意一个条目,文件还是存在,只要引用数量不为 0。

    有以下限制:不能跨越文件系统、不能对目录进行链接。

    硬链接

  • 符号链接(软链接)

    符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。

    当源文件被删除了,链接文件就打不开了。

    因为记录的是路径,所以可以为目录建立符号链接。

    软链接

文件删除

  1. Linux 下控制文件真正被删除的计数器

    Linux 是 link 的数量来控制文件删除的。只有当一个文件不存在任何 link 的时候,这个文件才会被删除。一般来讲,每个文件都有两个link计数器:i_count 和 i_link

    • i_count 的意义是当前文件使用者(或被调用)的数量,当一个文件被某一个进程引用时,对应的这个值就会增加
    • i_nlink 的意义是介质连接的数量(硬链接的数量),当创建文件的硬链接的时候,这个值就会增加。

    可以理解为 i_count 是内存引用计数器,i_nlink 是硬盘的引用计数器。

    当这两个计数器都为 0 时,就会删除这个文件的 inode。

  2. rm命令原理

    对于删除命令 rm 而言,实际上就是减少磁盘引用计数 i_nlink。相当于删除外层文件夹中的一行。

    Q:如果一个文件正在被某个进程调用,而用户却执行 rm 把文件删除了,那么会出现什么结果?当用户执行 rm 删除文件后,再执行 ls 或其他文件管理命令,无法再找到这个文件了,但是调用这个删除的文件的进程却在继续正常执行,依然能够从文件中正确的读取及写入内容,这又是为什么呢?

    A:rm 操作只是将文件的 i_nlink 减少了,如果没有其它的链接 i_nlink 就为 0 了。但是由于该文件依然被进程引用,因此,此时文件对应的 i_count 并不为 0,所以执行 rm 操作,系统并没有真正的删除这个文件,只有当 i_nlink 和 i_count 都为 0 的时候,这个文件才会被真正的删除。也就是说,必须要解除该进程对该文件的调用,才能真正的删除。

    Q:当文件没有被调用,执行了 rm 操作之后,还能找回被删除的文件吗?

    A:rm 操作只是将文件的 i_nlink 减少了,或者说置为 0,实际上就是将 inode 的链接删除了,此时,并没有删除文件的实体(block 数据块),此时,如果及时停止机器工作,数据是可以找回的,如果继续写入数据,那么新数据可能会被分配到被删除的数据的 block 数据块,文件就被真正的回收了。

  3. 实际遇到的问题

    Q:web 服务器磁盘空间不够了,删除了所有无用日志还是显示磁盘空间不足,但是 du -sh 发现磁盘空间的占用率很小,这是为什么呢?是什么导致磁盘空间不足?

    A:删除命令只是删除了文件的一个 i_nlink,但是其他进程(tomcat)正在使用这些 log 文件,重启 tomcat 可以减少 i_nlink 使其变为 0,就真正删除了这个文件。

读取文件的一些命令

  • cat
  • tac: 反向 cat
  • more:可以按 数字 + 回车 跳转行数,用空格显示下一页,按键 b 显示上一页。
  • less:在 more 基础上提供 /字符串 搜索功能
  • od:显示二进制 加上 -h 显示 16 进制

文件搜索

  • which:which [-a] command 指令搜索。-a :将所有指令列出,而不是只列第一个。
  • whereis:文件搜索。速度比较快,因为它只搜索几个特定的目录。
  • locate:文件搜索。可以用关键字或者正则表达式进行搜索。locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。
      # locate [-ir] keyword
      -r正则表达式
    
  • find:文件搜索。可以使用文件的属性和权限进行搜索。
      # find [basedir] [option]
      example: find . -name "shadow*"
    

    与时间有关的选项:

      -mtime  n :列出在 n 天前的那一天修改过内容的文件
      -mtime +n :列出在 n 天之前 (不含 n 天本身) 修改过内容的文件
      -mtime -n :列出在 n 天之内 (含 n 天本身) 修改过内容的文件
      -newer file : 列出比 file 更新的文件
    

六、Bash

可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。其他的例如 zsh。echo $SHELL 查看当前的 shell。

特性

  • 命令历史:记录使用过的命令
  • 命令与文件补全:快捷键:tab
  • 命名别名:例如 ll 是 ls -al 的别名
  • shell scripts
  • 通配符:例如 ls -l /usr/bin/X* 列出 /usr/bin 下面所有以 X 开头的文件

变量操作

对一个变量赋值直接使用 =。

对变量取用需要在变量前加上 $ ,也可以用 ${} 的形式;

输出变量使用 echo 命令。

$ x=abc
$ echo $x
$ echo ${x}

变量内容如果有空格,必须使用双引号或者单引号。

  • 双引号内的特殊字符可以保留原本特性,例如 x=”lang is $LANG”,则 x 的值为 lang is zh_TW.UTF-8;
  • 单引号内的特殊字符就是特殊字符本身,例如 x=’lang is $LANG’,则 x 的值为 lang is $LANG。

可以使用 指令 或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version=$(uname -r),或者 version=uname -r 则 version 的值为 4.15.0-22-generic。

可以使用 export 命令将自定义变量转成环境变量,环境变量可以在子程序中使用,所谓子程序就是由当前 Bash 而产生的子 Bash。

Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令:

declare -i ab
ab=56+1
echo $ab

输出结果 57。

ab=56+1
echo $ab

输出结果 56+1。

指令搜索顺序(即比如说输入 javac 后如何寻找)

  • 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls ;
  • 由别名找到该指令来执行;
  • 由 Bash 内置的指令来执行;
  • 按 $PATH 变量指定的搜索路径的顺序找到第一个指令来执行。(找到了 $JAVA_HOME 下的 javac 命令)

数据流重定向

重定向指的是使用文件代替标准输入、标准输出和标准错误输出。

1 代码 运算符
标准输入 (stdin) 0 < 或 «
标准输出 (stdout) 1 > 或 »
标准错误输出 (stderr) 2 2> 或 2»

其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。

可以将不需要的标准输出以及标准错误输出重定向到 /dev/null,相当于扔进垃圾箱。

如果需要将标准输出以及标准错误输出同时重定向到一个文件,需要将某个输出转换为另一个输出,例如 2>&1(1>&2)表示将标准错误输出转换为标准输出。

$ find /home -name .zshrc > list 2>&1

七、管道命令

管道是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管道。

在命令之间使用 分隔各个管道命令。
ls -al /etc | less

八、常用命令

cut 提取

cut 对数据进行切分,取出想要的部分。

切分过程一行一行地进行。

$ cut
-d :分隔符
-f :经过 -d 分隔后,使用 -f n 取出第 n 个区间
-c :以字符为单位取出区间

事例:

$ last
root pts/1 192.168.201.101 Sat Feb 7 12:35 still logged in
root pts/1 192.168.201.101 Fri Feb 6 12:13 - 18:46 (06:33)
root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16)

$ last | cut -d ' ' -f 1|head -1
root

sort 排序

$ sort [-fbMnrtuk] [file or stdin]
-f :忽略大小写
-b :忽略最前面的空格
-M :以月份的名字来排序,例如 JAN,DEC
-n :使用数字
-r :反向排序
-u :相当于 unique,重复的内容只出现一次
-t :分隔符,默认为 tab
-k :指定排序的区间

示例:/etc/passwd 文件内容以 : 来分隔,要求以第三列进行排序。

$ cat /etc/passwd | sort -t ':' -k 3
root:x:0:0:root:/root:/bin/bash
dmtsai:x:1000:1000:dmtsai:/home/dmtsai:/bin/bash
alex:x:1001:1002::/home/alex:/bin/bash
arod:x:1002:1003::/home/arod:/bin/bash

uniq 重复取一个、tee 双向输出(写文件同时显示在控制台)、split 将一个文件划分多个文件

grep

g/re/p(globally search a regular expression and print),使用正则表示式进行全局查找并打印。

$ grep [-acinv] [--color=auto] 搜寻字符串 filename
-c : 统计个数
-i : 忽略大小写
-n : 输出行号
-v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行 如 grep -v grep
--color=auto :找到的关键字加颜色显示

printf 用于格式化输出。它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。

printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt)

awk

是由 Alfred Aho,Peter Weinberger 和 Brian Kernighan 创造,awk 这个名字就是这三个创始人名字的首字母。

awk 每次处理一行,处理的最小单位是字段,默认以空格分开,每个字段的命名方式为:$n,n 为字段号,从 1 开始,$0 表示一整行。

awk 变量:

变量名称 代表意义
NF 每一行拥有的字段总数
NR 目前所处理的是第几行数据
FS 目前的分隔字符,默认是空格键

任何在 BEGIN 之后列出的操作(在 {} 内)将在 Unix awk 开始扫描输入之前执行,而 END 之后列出的操作将在扫描完全部的输入之后执行。因此,通常使用 BEGIN 来显示变量和预置(初始化)变量,使用 END 来输出最终结果。

事例1:

$awk
'BEGIN { FS=":";print "统计销售金额";total=0}
{print $3;total=total+$3;}
END {printf "销售金额总计:%.2f",total}' sx

输出:

统计销售金额

200.00

300.00

400.00

销售金额总计:900.00

事例2:

[root@ambari-namenode ~]# cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin

[root@ambari-namenode ~]# cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}'
root	 0
bin	 1
daemon	 2
adm	 3
lp	 4
sync	 5
shutdown	 6
halt	 7
mail	 8

八、进程管理

查看进程

ps

查看自己:ps -l

查看所有进程:ps aux 显示全部进程(比后者多出 CPU 内存使用),ps -ef 一定要有 - 才代表全部 f 会显示付进程关系。

top

实时显示进程信息。

每两秒钟刷新一次:top -d 2

netstat

查看端口占用。

进程状态(通过 ps aux可以看见状态)

[root@ambari-namenode]~/git-2.11.0# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  19232  1120 ?        Ss   Nov13   0:05 /sbin/init
root         2  0.0  0.0      0     0 ?        S    Nov13   0:00 [kthreadd]
root         3  0.0  0.0      0     0 ?        S    Nov13   0:18 [migration/0]
root         4  0.0  0.0      0     0 ?        S    Nov13   0:32 [ksoftirqd/0]
状态 说明
R(TASK_RUNNING) 只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行。
D(TASK_UNINTERRUPTIBLE) 不可中断阻塞,通常为 IO 阻塞。此刻进程是不可中断的。不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到。
S(TASK_INTERRUPTIBLE) 处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。
通过ps命令我们会看到,一般情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态(除非机器的负载很高)。毕竟CPU就这么一两个,进程动辄几十上百个,如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来。
Z(TASK_DEAD - EXIT_ZOMBIE) zombie 进程已经终止但是尚未被其父进程获取信息。
T(TASK_STOPPED or TASK_TRACED) 向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态,进程既可以被作业控制信号结束,也可能是正在被追踪。

进程状态

SICCHLD

任何一个子进程(init 除外)在 exit 后并非马上就消失,而是留下一个称外僵尸进程的数据结构,等待父进程处理。这是每个子进程都必需经历的阶段。另外子进程退出的时候会向其父进程发送一个 SIGCHLD 信号。

设置僵死状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程 ID,进程的终止状态,以及该进程使用的 CPU 时间,所以当终止子进程的父进程调用 wait 或 waitpid 时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程 ID 将被重置为 1(init 进程)。继承这些子进程的 init 进程将清理它们(也就是说 init 进程将 wait 它们,从而去除它们的僵尸状态)。

当一个子进程改变了它的状态时(停止运行,继续运行或者退出),有两件事会发生在父进程中:

  • 得到 SIGCHLD 信号;
  • waitpid() 或者 wait() 调用会返回。

其中子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等。

在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。

wait()

pid_t wait(int *status)

父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。

如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 -1,同时 errno 被置为 ECHILD。

参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL。

waidpid()

pid_t waitpid(pid_t pid, int *status, int options)

作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。

pid 参数指示一个子进程的 ID,表示只关心这个子进程退出的 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。

options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。

孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。

孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。

由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。

僵尸进程

unix 提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以得到。这种机制就是:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号 process ID、退出状态 the termination status of the process、运行时间 the amount of CPU time taken by the process等 )。直到父进程通过 wait/waitpid 来取时才释放。

如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。

僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。

系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。

要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 进程所收养,这样 init 进程就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。

九、五种 IO 模型

什么是 socket

我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。

I/O 模型

一个输入操作通常包括两个阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

Unix 有五种 I/O 模型:

  • 阻塞式 I/O
  • 非阻塞式 I/O
  • I/O 复用(select 和 poll)
  • 信号驱动式 I/O(SIGIO)
  • 异步 I/O(AIO)

同步/异步 阻塞/非阻塞

对应IO的关系

同步和异步关注的是消息通信机制。

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

阻塞式 IO

应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。

应该注意到,在阻塞的过程中,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率会比较高。

下图中,recvfrom() 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

阻塞IO

非阻塞 IO

应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。

由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低。

非阻塞IO

IO 复用

使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。

它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。

如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。

IO复用

信号驱动 I/O

应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。

相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。

信号驱动IO

异步 I/O

应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。

异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。

IO 比较

  • 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。
  • 异步 I/O:第二阶段应用进程不会阻塞。

同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段。

非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。

IO对比

I/O 多路复用中的 select/poll/epoll

IO 多路复用(IO Multiplexing)是指单个进程/线程就可以同时处理多个 IO 请求。

实现原理:用户将想要监视的文件描述符(File Descriptor)添加到 select/poll/epoll 函数中,由内核监视,函数阻塞。一旦有文件描述符就绪(读就绪或写就绪),或者超时(设置timeout),函数就会返回,然后该进程可以进行相应的读/写操作。

select/poll/epoll 都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。

select

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。

  • fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义,所以只能监听少于 FD_SETSIZE 数量的描述符。有三种类型的描述符类型:readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。
  • timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。
  • 成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。

poll

int poll(struct pollfd *fds, unsigned int nfds, int timeout);

poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态。

poll 中的描述符是 pollfd 类型的数组,pollfd 的定义如下:

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

poll 和 select 比较

select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。

  • select 会修改描述符,而 poll 不会;
  • select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制;
  • poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
  • 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。

select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。

几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。

epoll

由于epoll的实现机制与select/poll机制完全不同,上面所说的select的缺点在epoll上不复存在。

设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的。如何实现这样的高并发?

在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

epoll的设计和实现select完全不同。epoll通过在linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树)。把原先的select/poll调用分成了3个部分:

  1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)

  2. 调用epoll_ctl向epoll对象中添加这100万个连接的套接字

  3. 调用epoll_wait收集发生的事件的连接

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。

从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。

epoll 仅适用于 Linux OS。

epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。

epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。

三者区别

  • select:将文件描述符放入一个集合中,调用 select 时,将这个集合从用户空间拷贝到内核空间(缺点 1:每次都要复制,开销大),由内核根据就绪状态修改该集合的内容。(缺点 2)集合大小有限制,32 位机默认是 1024(64位:2048);采用水平触发机制。select 函数返回后,需要通过遍历这个集合,找到就绪的文件描述符(缺点 3:轮询的方式效率较低),当文件描述符的数量增加时,效率会线性下降;
  • poll:和 select 几乎没有区别,区别在于文件描述符的存储方式不同,poll 采用链表的方式存储,没有最大存储数量的限制;
  • epoll:通过内核和用户空间共享内存,避免了不断复制的问题;支持的同时连接数上限很高(1G 左右的内存支持 10W 左右的连接数);文件描述符就绪时,采用回调机制,避免了轮询(回调函数将就绪的描述符添加到一个链表中,执行 epoll_wait 时,返回这个链表);支持水平触发和边缘触发,采用边缘触发机制时,只有活跃的描述符才会触发回调函数。

总结,区别主要在于:

  • 一个线程/进程所能打开的最大连接数
  • 文件描述符传递方式(是否复制)
  • 水平触发 or 边缘触发
  • 查询就绪的描述符时的效率(是否轮询)

三者应用场景

很容易产生一种错觉认为只要用 epoll 就可以了,select 和 poll 都已经过时了,其实它们都有各自的使用场景。

  1. select 应用场景

select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。

select 可移植性更好,几乎被所有主流平台所支持。

  1. poll 应用场景

poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。

  1. epoll 应用场景

只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。

需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。

需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。

当连接数较多并且有很多的不活跃连接时,epoll 的效率比其它两者高很多;但是当连接数较少并且都十分活跃的情况下,由于 epoll 需要很多回调,因此性能可能低于其它两者。

什么是文件描述符

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

内核通过文件描述符来访问文件。文件描述符指向一个文件。

什么是水平触发?什么是边缘触发?

  • 水平触发(LT,Level Trigger)模式下,只要一个文件描述符就绪,就会触发通知,如果用户程序没有一次性把数据读写完,下次还会通知;
  • 边缘触发(ET,Edge Trigger)模式下,当描述符从未就绪变为就绪时通知一次,之后不会再通知,直到再次从未就绪变为就绪(缓冲区从不可读/写变为可读/写)。
  • 区别:边缘触发效率更高,减少了被重复触发的次数,函数不会返回大量用户程序可能不需要的文件描述符。
  • 为什么边缘触发一定要用非阻塞(non-block)IO:避免由于一个描述符的阻塞读/阻塞写操作让处理其它描述符的任务出现饥饿状态。

Search

    Table of Contents