先说一句很多刚接触 Linux 的人都会问的话:我已经会用 Windows 了,为什么还要折腾 Linux 这套"看起来像黑底白字的古代界面"?

答案藏在 Linux 的身份里。Linux 是当前服务器、云计算、大数据乃至嵌入式和移动设备领域事实上的操作系统霸主——如今互联网行业找后端、运维、嵌入式、云计算相关的岗位,几乎绕不开它。而它的操作方式恰恰和你习惯的 Windows 相反:Windows 用图形界面(GUI,Graphical User Interface,图形用户界面,靠鼠标点击)跟系统对话,Linux 大多数时候用一套字符命令(CLI,命令行界面)跟系统对话。别急着觉得"落后",恰恰是这套字符界面,让同一台机器可以被成千上万人远程管理、让操作可以被精确地脚本化、让一次命令能跑遍全世界数百万台服务器。你理解了这层区别,就理解了为什么这节课要带你从最基础的命令学起。

这篇文章只想做一件事:把 Linux 最常用的几十个命令,掰开揉碎讲清楚。不仅是"这条命令长这样",更要讲"它为什么要长成这样""它在系统里到底干了什么""什么情况下你会踩坑"。如果你只会抄命令不追究原理,看这篇勉强够用;但我的目标是让你读完删无可删、日后遇到什么命令都有底气去 man 查、去理解、去举一反三。

这节课算是我们"Linux 系统编程"这门课的地基。地基打得越实,后面越省力。我们从头走一遍。

认识 Linux:它从哪里来

在动手敲命令之前,我想先让你对"Linux 到底是什么"有个踏实的概念。它不是一个孤立的产物,而是站在一个叫 UNIX 的巨人肩膀上长出来的。

UNIX 的历史:Linux 的爷爷

故事要从上世纪 60 年代讲起。1968 年,来自通用电气公司、贝尔实验室和麻省理工学院的研究人员,联手开发了一个叫 Multics(MULTiplexed Information and Computing Service,多路信息计算服务)的特殊操作系统。它把"多任务""文件管理""多用户连接"这些在当时相当超前的新概念整合到了一起。你可以把它理解成那个年代操作系统的"概念车"——想法很华丽,但项目庞大臃肿,最终没能走向实用。

不过概念车没有白造。1969 到 1970 年间,AT&T 贝尔实验室的研究员 Ken Thompson(肯·汤普森) 和 Dennis Ritchie(丹尼斯·里奇),在吸收了 Multics 诸多优点的前提下,开发出了一套新的系统,名字就叫 UNIX。起初它跑在小型的机器上,满足了科研环境的需求。从诞生起,UNIX 就是一个有价值、高效并且支持多用户、多任务的操作系统。

这里有个很重要的转折:UNIX 的第一个版本是免费提供给许多知名大学的计算机系使用的。这听起来像送人情,实际上影响深远——它让大学里的研究者们接触到了 UNIX 的源码,也因此在校园里培养出一大批熟悉 UNIX 的人,为后来 UNIX 的分支遍地开花埋下了种子。

到了 1972 年,贝尔实验室开始发放 UNIX 的商业版本并授权给不同用户使用,其中就包括加州大学伯克利分校的计算机系。伯克利给系统添加了许多新特性,后来这些特性成了标准。1975 年,伯克利以"BSD"(Berkeley Software Distribution,伯克利软件发行版)的名义,由下属部门发行了自己的 UNIX 版本。这个 BSD 版本成了 AT&T 贝尔实验室版本的有力竞争者,而其他独立开发的 UNIX 版本也开始涌现。

后面这段历史一路分叉:

  • 1980 年,微软开发了叫做 Xenix 的 UNIX 的 PC 版本;AT&T 则发行了第一个商业版本 System Ⅲ,后来被对商业软件产品支持良好的 System Ⅴ 取代。
  • 同时,BSD UNIX 在 70 年代末成为美国国防部高科技研究机构科研项目的基础,伯克利为此发行了功能强大的 BSD Release 4.2,它包含了高级的文件管理器,以及基于 TCP/IP 网络协议的联网特性——没错,今天整个互联网赖以运转的 TCP/IP,正是经由 BSD 这条线传播开来的。BSD Release 4.2 被 Sun Microsystem 等许多厂商采用。
  • 版本一多,问题随之而来:软件开发商不知道该让自己的程序到底跑在哪个 UNIX 版本上合适。于是到 80 年代中期,"UNIX 标准"的需求变得迫切,但竞争的两个标准本身也分裂了——一个基于 AT&T 的 UNIX,一个是 BSD 版本。
  • AT&T 为此成立了"UNIX 系统实验室",专门综合 UNIX 的不同版本。1991 年,它把 System Ⅴ Release 3、SUN OS 和 Xenix 的特点合到一起,发行了 System Ⅴ Release 4。为了和它竞争,IBM、惠普的 Open Software Foundation(OSF)也搞出自己的 UNIX 标准版本,于是出现了两个商业标准版本并存的局面。
  • 1993 年,AT&T 把她的 UNIX 转卖给了 Novell 公司。Novell 发行了自己的 UNIX 版本 UNIXWare,SUN 公司则把 System Ⅴ Release 4 融进它的 SUN OS,发行了 Solaris。

这一段岔路多得让人眼花,但你只需要抓住一条主线:UNIX 是个家族,天下并不是一家的,而是从贝尔实验室的根、一路被大学、厂商、标准化组织折腾出无数分支。正是这种"大家各改各的、谁都没想统一"的混乱局面,才给后面一个小年轻——Linux——留出了靠"免费 + 共享"一统江湖的空间。

Linux 的历史:那个叫 Torvalds 的年轻人

时间来到 1991 年。当时任教的一位教师 Andrew S. Tanenbaum(安德鲁·谭宁邦)为了教学,自己写了一个迷你操作系统 Minix,用在操作系统课程的课堂上。它很小、很好懂,是学生学习操作系统的理想教材。

1991 年 10 月 5 日,芬兰赫尔辛基大学的一名研究生 Linus Benedict Torvalds(林纳斯·贝内迪克特·托瓦兹) 在一个叫 comp.os.minix 的 Usenet 新闻组(一个早期的在线讨论组)中宣布,他基于 Minix 的启发,编写出了一个类似的、像 UNIX 的小型操作系统,取名叫 Linux——他自己名字 Linus + 一个常见的 "x" 后缀,致敬 UNIX。

一个容易被误解的细节:你可能以为发布的是 0.01 版本,其实不是。真正的 Linux 0.01 版本从未被公开发布,因为那版根本不实用,Linus 只是在第一个 Linux 的 FTP 站点(ftp://nic.funet.fi)上提供过它的源码。他在 10 月 5 日公开发布的是 0.02 版。这个版本已经能运行 GNU 的 Bourne Again Shell(即 bash)和 GNU 的 C 编译器(gcc),以及为数不多的其他语言工具。

之后 Linux 以惊人的速度迭代:0.02、0.03、跳到 0.10……世界各地的程序员都开始加入开发,很快就到了 0.95,这意味着距离正式发布 1.0 不远了。正式的 Linux 1.0 版本于 1994 年公布。

你可以把这段历史跟 Windows 的发展史对比着看:Windows 诞生在 1985 年前后(Windows 1.0),走的是"闭源商业 + 图形界面"路线;Linux 诞生在 1991 年,走的是"开源 + 字符界面"路线。两条路线后来在服务器、桌面、移动、云几个战场此消彼长,构成了今天 IT 世界两套主流操作哲学。

开源与 GPL:Linux 最深的底色

现在你可能要问:为什么 Linux 能在一堆 UNIX 商业产品(每个都收费、每个都闭源)的围剿下活下来,还能越做越大?答案就是"开源"。

严格来说,Linux 只是操作系统内核本身(内核是操作系统的核心,负责管理 CPU、内存、设备驱动、进程调度这些最底层的活),我们通常说的"Linux 操作系统",往往是指"Linux 内核 + 用户空间的应用程序 + GUI 组件"这一整套。开源(Open Source)的意思是源代码公开,任何人都可以看到、修改、再发布。

Linux 采用的开源许可证是 GNU GPL(GNU 通用公共许可证,GNU General Public License),最初由理查德·斯托曼(Richard Stallman)为 GNU 计划撰写。GPL 有两条核心规定你要记住:

  1. 任何基于 GPL 软件开发的衍生作品,在发布时也必须采用 GPL 许可证方式,同时必须公开源代码——这叫"传染性",一旦你用了 GPL 的代码,你的修改也得开源,防止有人"白嫖开源成果然后闭源垄断"。
  2. 只要遵循 GPL,任何个人和机构都可以自由使用 Linux 的底层源代码、自由修改和再发布。

正是因为这条规则,Linux 成为自由软件和开源软件发展中最著名的例子,也成了"开源"的代名词。你可以把它想成一种"用合法契约保护的共享精神":我允许你抄我、改我,但前提是你改完也得共享回来。这一条,是理解整个 Linux 生态的关键钥匙。

它在企业里到底被用在哪?

数据最直观。权威统计显示,Linux 在服务器领域占据了大约 75% 的市场份额,尤其在政府、金融、农业、交通、电信等国家关键领域大规模应用。为什么服务器偏爱 Linux?核心是三点:稳定(常年运行不死机)、免费(省下巨额授权费)、可定制(源码在手、想怎么改怎么改)。

桌面领域,国内如中标麒麟、红旗、深度(Deepin)等发行版在政府、企业、OEM 里应用广泛,Ubuntu、SUSE 也影响力巨大,但从整体易用性、软硬件兼容性来说,和 Windows 仍有一定差距。

移动和嵌入式领域是另一片天地:Android 本身就跑在 Linux 内核之上,2015 年统计数据里它占据全球智能手机市场约 84.6% 的份额。路由器、电视、车载系统、数字视频录像机、舞台灯光控制系统,几乎都跑着定制版 Linux。思科的网络防火墙和路由器用定制 Linux,阿里云当年的 YunOS 也是。

云计算和大数据领域 Linux 更是中坚:据 Linux 基金会研究,约 86% 的企业用 Linux 构建云计算、大数据平台。可以这么说,你叫得上名字的云服务、大数据框架,背后几乎都是在 Linux 上跑的。

常见的发行版,傻傻分不清?

"发行版"这个概念你需要先建立:Linux 内核只有一个,但围绕它打包出来、拿来就能用的完整操作系统有很多个,每个大厂/社区都会内置自己的包管理、软件仓库和默认工具,这些就是"发行版"(Distribution,缩写 Distro)。常见的有:

  • Debian:以运行极其稳定著称,非常适合当服务器。但同时它对小白不太友好,更适合系统管理员和高级用户。
  • Ubuntu:Debian 的衍生版之一,当今最受欢迎的免费操作系统,在服务器、云计算甚至在移动设备上都常见。新手用户最值得从 Ubuntu 上手。
  • 红帽企业级 Linux(RHEL,Red Hat Enterprise Linux):第一个面向商业市场的 Linux 发行版,有服务器版,支持 x86、x86_64 等众多处理器架构,Red Hat 公司通过 RHCSA(红帽认证系统管理员)/RHCE(红帽认证工程师)课程对社会进行培训和认证——这张证在运维圈含金量很高。
  • CentOS:用 RHEL 的免费源代码重新构建的企业级发行版(因为 RHEL 的源码是开放的,允许重编译发行),去掉了注册商标和一些细微的 Binary 包差异,几乎和 RHEL 一模一样,但不用付费。它用 YUM 管理软件包。注意,CentOS 的项目战略这些年有过重大调整,生产环境选型时建议以官方最新公告为准。
  • Fedora:红帽公司的"测试平台",新产品先在 Fedora 上开发和测试,稳定后才能进入企业级版本。适合想尝鲜最新技术、等不及稳定版的人。
  • Kali Linux:Debian 的衍生版,专为渗透测试(Penetration Test,即安全测试人员对系统发起模拟攻击以查找漏洞的活动)而设计,前身是 Backtrack,自带大量安全测试工具,用 APT 管理软件包。它是"白帽黑客"的得力工具——所谓白帽黑客,就是做正当安全工作的专业人员,和黑入别人系统的恶意黑客相对。

软件包管理老师要带你认识两个词:YUM(RHEL/CentOS/Fedora 系用的包管理器,前端往往还有一个叫 DNF 的升级版)和 APT(Debian/Ubuntu/Kali 系用的包管理器)。后面装软件时会反复用到。

远程登录与「能不能用图形界面」

为什么你不用图形界面?

如果你启动 Linux 进入了图形化桌面,这对你以后的工程师工作而言基本可以"忘掉"了。这是因为在实际的工作环境里,服务器是放在机房或云上的,你面前的电脑是 Windows/Mac,你从不直接坐在服务器边上。管理服务器的唯一方式,就是通过网络远程登录进去,用一个字符终端操作它——机房上千台机器,没人给每台都配个显示器啊。

一个底层原因你也值得知道:字符界面比图形界面少包了一层"壳"。 图形界面本质上是一层运行在 Linux 之上的大规模应用程序,它搬运、渲染、绘制,消耗大量 CPU 和内存。而字符终端只传文本,一台服务器要同时服务几十上百个远程登录的终端会话,用 GUI 根本扛不住,也拖慢系统。所以"不用图形界面"不是保守,而是效率使然。

思考题:为什么不使用图形界面?

答案从三个层面理解。第一,工作需要:真实服务器放在机房/云端,无法为每台配显示器,远程字符登录是唯一且必要的管理途径。第二,性能:图形界面是运行在系统之上的一层庞大应用,要消耗 CPU、内存做渲染绘制;服务器要同时承载大量并发任务和大量远程终端,字符界面仅传送文本、开销极小,能让服务器的资源真正用在业务上而非画面上。第三,可脚本化与可自动化:字符命令可以被写入脚本批量执行、被工具编排,这是任何点击操作都无法比拟的;一台运维人员通过脚本能同时管上万台服务器,这是运维自动化的根基。

用 XShell 远程登录 Linux

既然要用字符终端登服务器,Windows 下最常用的一个远程终端软件叫 XShell。它有"Home/School"免费版本(个人和学校可免费使用),官网在 https://www.netsarang.com/products/xsh_overview.html(现在的中文页面入口 https://www.xshell.com/zh/free-for-home-school/ 也可用)。

远程登录依赖的是 SSH(Secure Shell,安全外壳协议),它在非加密网络上给你提供一条加密的、可用于远程登录和命令执行的安全连接。这一层加密很重要——你敲的命令、你输的密码,都在这条加密隧道里走,不会被"窃听"到。

过程很简单,大致分三步:

  1. 在 Linux 主机上看 IP:在 Linux 终端敲 ifconfig(查看/配置网络接口的命令),会打印出这台机器的网卡信息和它的 IP 地址(IP 是网络里每台设备的"门牌号",别人靠它找到这台机器)。现在很多系统里 ifconfig 已被 ip addr 取代,功能类似。
  2. 在 XShell 里填 IP 登录:新建会话,主机填刚才看到的 IP,端口默认 22(SSH 的默认端口),用户名密码填 Linux 的账号。
  3. 网络畅通的话,会提示输入用户名和密码,输对就能连上。

XShell 里有个新手最容易踩的坑——复制粘贴不能用 Ctrl+C / Ctrl+V。因为 Ctrl+C 在 Linux 终端里是"中断当前程序"的意思(后面细讲),被占用了。XShell 自己的复制是 Ctrl + Insert,粘贴是 Shift + Insert(有些笔记本的 Insert 键要配合 Fn 键来按)。

Shell:你与内核之间的"王婆"

登录进去后,你面对的其实是一系列叫 Shell 的东西。理解它,是你理解一切命令的前提。

Linux 严格来说是一个操作系统,我们称之为 内核(kernel)——它是那个真正管理 CPU、内存、设备、进程的"大总管"。但我们普通用户不能直接使用内核。为什么?因为内核的接口又底层又危险:你可以让内核干任何事,也意味着一个误操作就可能毁掉整个系统。这不是能力问题,是安全设计——内核不信任普通玩家,也不打算迁就普通玩家的误触。

于是有了一个"外壳"程序,也就是 Shell,来隔在用户和内核之间。

Shell 最简单的定义:命令行解释器(command interpreter)。 它有两件主要工作:

  • 把用户的命令"翻译"给内核去处理;
  • 把内核的处理结果"翻译"给用户看。

你想想 Windows 就懂了:你操作 Windows 也不是直接操作内核,而是通过图形界面点鼠标(比如进 D 盘就双击盘符)。图形界面就是 Windows 的"Shell"之一。Linux 的 Shell 和它作用相同,只不过一个用点击、一个用敲字符。

课件里用得妙的一个类比,我抄给你并扩展一下:假设你是一个闷骚又害羞的程序员,操作系统内核就是你们村头漂亮且让你心动的姑娘"小花"。你不好意思直接跟小花表白,于是委托媒婆(王婆)替你传话。所有的话你都跟王婆说,由王婆转达给小花;小花的意思也由王婆带回给你。这个"王婆",对应到你最常用的那个 Shell——bash(Bourne Again Shell)。

所以你要记牢一句话:你敲的每一条命令,都不是"直接"让硬件干活,而是先交给 Shell,Shell 解析、翻译成内核能懂的调用,内核再去驱动硬件,最后结果原路返回。 这也解释了为什么后面学的不少"命令"其实是一些小程序(比如 ls、cat 都住在 /usr/bin/ 下)——Shell 负责找到它们并替你运行它们。

Linux 世界里最重要的一句话:一切皆文件

在 Linux 里,请先植入一个贯穿始终的核心理念——一切皆文件(everything is a file)。

这句话的意思是:在 Linux 眼中,你的普通文本文件、目录、硬盘、串口、键盘、网卡,甚至进程之间的通信管道,统统被抽象成"文件"来统一管理。它们都有一组统一的"读写"接口。你不用关心背后是内存还是磁盘,只要会用打开/读/写/关闭这一套操作,就能操作它们。

也许有人会问,为什么这么设计?答案是统一。Windows 里你想读文件要一种操作、读硬盘要另一种、读网卡又是一套,接口五花八门。Linux 把它们全压成"文件"这一个模型,程序员只需掌握一套文件 IO 知识就能通吃所有设备。代价是要么理解了它、要么被它的各种"文件类型"绕晕。

为支撑"一切皆文件",Linux 里存在(马上会遇到)这些文件类型,每个都以一个字母标记:

文件类型字母含义生活类比
-普通文件一张存着内容的纸
d目录(directory)一个收纳夹
l软链接(soft link,符号链接)快捷方式
b块设备文件(block device,如硬盘、光驱)按块读写的"砖墙"
c字符设备文件(char device,如屏幕、键盘)逐字符读写的"水龙头"
p管道文件(pipe/FIFO)一根单向水管
s套接字文件(socket,网络通信端点)一个通信插座

后面讲 ls -l 时你会看到,每个文件这一长串权限信息里的第一个字母,就是上面的某一个。现在先有个印象。

安全检查:先认识「当前我在哪」—— pwd

很多命令前你都想先确认自己站在哪个目录。这就要用到 pwd(Print Working Directory,打印当前工作目录)。

pwd 没有任何常用选项,敲下去直接打印你当前所在的目录的绝对路径:

pwd     # 打印当前所在目录,例如 /home/whb/test

它为什么重要?因为后面所有"相对路径"都是"相对当前目录"而言的。你连自己在哪都不清楚,用相对路径就是一通瞎走。这也是为什么实战里指令常常是 pwd 起手。

你会注意到 prompt(命令提示符)里其实也常写着当前目录——比如 [whb@bite-alicloud test]$ 里的 test。但 pwd 给出的是从根 / 开始的完整路径,更精确。

目录树、路径与定位文件的唯一性

在讲具体命令前,我要把 Linux 路径这个地基讲透,你后面所有命令都建立在这上面。

目录树:一棵倒着长的树

Linux 把磁盘上的所有文件和目录组织成一棵目录树。既然是"树",就从根开始生长,根是 /(一个正斜杠)。每个节点要么是目录、要么是文件:

  • 普通文件一定是目录树的叶子节点——文件里不能再装东西了,它只有内容。
  • 目录可能是叶子(空目录),也可能是路上的节点(里面还套着子目录/文件,一直往下延伸)。

想象一棵倒过来的现实之树:/ 是树干扎根的土壤,目录是一级级的枝杈,文件是枝头挂的叶子。叶子不可能再长出枝杈,但枝杈可以继续分下去。

为什么需要"路径"来定位?

树状组织有它的意义:树保证了"从根到任意节点只有一条路",从而保证了定位文件的唯一性。 因为任何一个节点都只有一个父节点,所以从根目录出发,沿着一条确定的链就能走到指定文件,这条路就是"路径",而且对给定文件来说它是唯一的、不含歧义的。

打个比方,你给朋友写信要写完整地址:省 → 市 → 区 → 街道 → 门牌号。这个地址从大到小逐级精确定位到一户人家,不依赖收信人现在在哪。Linux 的绝对路径正是这种"完整地址"。

绝对路径 vs 相对路径

根据是否依赖"当前位置",路径分两种:

  • 绝对路径:从根目录 / 开始写,完整到目标,如 /home/whb/test/file.txt。它"不依赖其他目录",任何人在任何位置用这条路径都指向同一个文件,所以唯一性不会随用户位置的改变而丧失。正因为这个特性,绝对路径常被用在特定服务的配置文件里——配置里写死了某个文件该在哪,不随着用户的 cd 而变。
  • 相对路径:相对于当前用户所处的目录来定位,如当前在 /home/whb 时写 test/file.txt。它写起来简短,所以命令行的日常操作里用得最多。但它的意思是"从我站的地方出发……",一旦你换了个目录,它指向的地方就变了。

还有个关键符号你要记牢:. 代表当前目录,.. 代表当前目录的父目录(上级目录)。 它俩其实也是目录树里的真实节点——每个目录里都藏着这两个隐藏项。

结合"一切皆文件",目录也是文件。想象 /home/whb 这个目录文件里,记录了它下面的子项(比如 test、temp),同时也记录了一个指向父目录的 ..。于是"返回上级目录"这件事,本质上就是通过 .. 这个"指针"往上走一层。

思考题:绝对路径和相对路径,什么时候该用哪个?

答案:在命令行交互、尤其是手敲命令时,多用相对路径,因为它短、快、符合思维直觉;其中又有两个特殊捷径——用 ~ 代表当前用户家目录(下面会讲),用 - 代表"最近访问过的目录"。在写进配置文件里的场景,尽量用绝对路径,因为配置文件不依赖"执行者此刻站在哪个目录",一个固定的绝对路径无论谁、无论在哪执行都指向同一位置,最稳健。一句话:人(交互)用相对的,机器(配置/脚本)用绝对的。

走进目录的钥匙:cd

cd(Change Directory,切换目录)改变当前工作目录。语法:cd 目录名。

  • 进子目录:cd test
  • 返回上级目录:cd ..
  • 用绝对路径直达:cd /home/whb/test
  • 用相对路径:cd ../temp(从 test 往上一级再进 temp)
  • 快速回家:cd ~ 或直接 cd(~ 就是当前用户家目录的缩写;每个新建用户系统都会在 /home 下给他建一个以用户名命名的目录作为工作目录,root 用户则内置在 /root)
  • 回到最近访问的目录:cd -。它俩互相"弹跳"很好玩:在 /home/whb 敲 cd /,再 cd - 就回到 /home/whb,再 cd - 又回到 /…… 因为 - 记住的是"上一次所在的目录"。
# 先看自己在家目录
cd ~
pwd                          # /home/whb
# 回上级
cd ..
pwd                          # /home
# 绝对路径定位
cd /home/whb/test
pwd                          # /home/whb/test
# 相对路径定位
cd ../temp
pwd                          # /home/whb/temp
# 用 - 在最近两个目录间来回切换
cd /                         # 跳到根目录
cd -                         # 打印并跳回 /home/whb/temp
cd -                         # 又跳回 /

认识文件类型与时间:touch 与 stat

文件时间三兄弟背后的真相

先讲一个命令 touch。它的字面意思是"触碰"。功能有两层:

  1. 若指定的文件不存在,就新建一个空的普通文件(这是最常用的用途);
  2. 若文件已存在,就更新它的时间戳。

语法:touch [选项] 文件...,常用选项:

  • -a:只修改 access time(访问时间,文件最后一次被读取的时间)
  • -m:只修改 modification time(修改时间,文件内容最后一次被改动的时间)
touch newFile.txt    # 新建一个空文件 newFile.txt
ls -l                # 用 -l 能看到它的详细信息(权限、属主、大小、时间)

新建完我们想看清它的"时间三兄弟",用 stat 命令(statistics,查看文件的详细状态信息):

stat newFile.txt

大致输出长这样:

  File: 'newFile.txt'
  Size: 0                 Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d        Inode: 1844719     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1003/     whb)   Gid: ( 1003/     whb)
Access: 2023-01-11 15:06:45.832787572 +0800
Modify: 2023-01-11 15:06:45.832787572 +0800
Change: 2023-01-11 15:06:45.832787572 +0800
Birth: -

这个输出信息量巨大,我们一点点拆。先看最关键的三行时间:

  • Access(atime):访问时间,即这个文件最后一次被读取的时间。你 cat 它一次,atime 就可能变。注意"读取"不等于"修改"。
  • Modify(mtime):内容修改时间,即文件内容最后一次被改动的时间。你 echo xxx > file 往里写内容,或改一行,mtime 就会变。
  • Change(ctime):状态(inode)改变时间,即文件的元数据(权限、属主、链接数等"文件本身的管理信息",不是文件内容)最后一次被改变的时间。比如你改了文件的权限 chmod、改了属主 chown、给它创建了硬链接,ctime 都会变。
  • 还有一行 Birth:(出生时间),在很多文件系统上因为不记录而不显示(显示 -),可忽略。

这里藏着课件专门留的一个思考题:

思考题:我执行 touch -a newFile.txt(只改 access 时间),为什么输出里 Change(ctime)也跟着变了?

答案:ctime 记录的是"文件元数据/状态被改变"的时间,而不是"文件内容被改变"的时间。当你执行 touch -a 去修改文件的 atime 时,这一改动本身就是对文件元数据(那个记录 atime 的 inode 信息)的一次写入——你可以理解为"在文件系统里,你要把某个表项的数值从 A 改成 B,这个改动作本身也算一次状态更新"。所以系统在更新 atime 的同时,顺手也更新了 ctime,用来记下"这个文件的元数据刚刚被碰过"。因此 ctime 大致是三者里"最铁面无私"的一个:任何会改动文件状态的操作(touch、chmod、chown、建链接)都会让它刷新,哪怕文件内容一字未动。想验证,你可以连续做:stat f.txt 记下三个时间 → touch -a f.txt → 再 stat f.txt,你就会看到 atime 和 ctime 同时更新,而 mtime 纹丝不动。

再补一句记忆口诀方便你区分:atime 是被"看"过,mtime 是内容被"改"过,ctime 是"档案登记信息"被动过。 其中 mtime 和 ctime 的关系常常让人犯迷糊——最朴素的记忆是:改了内容(mtime 变),必然也导致文件状态变化(ctime 变),所以大多数情况下你看到 mtime 变、ctime 几乎也一起变;但反过来改权限(chmod)只会让 ctime 变,mtime 根本不动。

ls -l 里默认显示的时间,是 mtime(内容修改时间),这点也值得记住——很多人误以为那是"创建时间"。

这也是理解后面 find 按时间查找、以及日志排障的关键。touch 更多关于时间的细讲,会放到后面"基础 IO"部分统一再说,这里你先把三兄弟的概念理清。

看目录里有什么:ls

ls(list)是 Linux 里用得最多的命令之一。功能:列出目录下的子目录与文件;如果参数是文件,就列出该文件的信息。

语法:ls [选项] [目录或文件]。

常用选项,我逐个讲清楚,别上来硬背:

  • -a:列出所有文件,包括以 . 开头的隐藏文件。在 Linux 里,文件名以 . 开头的就是隐藏文件(例如 .bashrc),ls 默认不显示它们。
  • -l:以长格式列出详细信息(权限、链接数、属主、属组、大小、时间、名字)。经常简写成 ll 或 ls -al。
  • -d:把目录本身当作文件显示,而不是列出它里面的内容。绝大多数默认 ls 给出的是目录的"内容";想要目录本身的信息(比如看权限),用 ls -d 目录。
  • -i:显示文件的 inode 索引号(inode 是"一切皆文件"里每个文件在磁盘上的"身份卡",后面权限一节细讲)。
  • -k:以 KB 为单位显示文件大小(配合 -l 用)。
  • -n:用数字的 UID(用户 ID)和 GID(用户组 ID)代替用户名和组名显示。
  • -F:在每个文件名后面补一个表示类型的字符——* 是普通可执行文件,/ 是目录,@ 是符号链接(软链接),| 是 FIFO 管道文件,= 是套接字文件。这是快速识别文件类型的好帮手。
  • -r:对目录里的条目反向排序。
  • -t:以时间排序(最新的排最前)。
  • -s:显示每个文件占用的块大小,配合排序可以帮你找出目录下最大的文件(比如 ls -lS 按大小排、ls -sl 就能看到每个文件的块数)。
  • -R:递归列出所有子目录下的文件(recursive)。
  • -1:每行只输出一个文件(Linux 下是纵向排布)。

来看几个实际演示(课件里的经典样例):

ls                     # 列出当前目录
# dir  file.txt
 
ls -a                  # 连隐藏文件一起列出来,会看到 . 和 ..
# .  ..  dir  file.txt
 
ls -a -l               # 详细列出,观察每一列
# total 12
# drwxrwxr-x  3 whb whb 4096 Jan 11 15:01 .
# drwx------ 22 whb whb 4096 Jan 11 14:21 ..
# drwxrwxr-x  2 whb whb 4096 Jan 11 14:22 dir
# -rw-rw-r--  1 whb whb    0 Jan 11 14:22 file.txt
 
ls -a -l -F            # 加 -F,在名字后标记类型
# ... dir/   file.txt(普通文件无后缀标记)
 
ls -alFn               # 多个选项可以合写成一个,等价于 -a -l -F -n
# ... 属主列变成数字 UID/GID:1003 1003

这里你要学会读 ls -l 的第一行细账。我拿 -rw-rw-r-- 1 whb whb 0 Jan 11 14:22 file.txt 逐列拆给你:

片段含义
-文件类型(这里是普通文件,d 则是目录)
rw-属主(用户)权限:可读可写不可执行
rw-属组(集团队)权限:可读可写不可执行
r--其他用户权限:只读
1硬链接数(普通文件一般 1)
whb属主(文件的所有者,owner)
whb属组 / group
0文件大小(字节)
Jan 11 14:22最近一次内容修改时间 mtime
file.txt文件名

那 total 12 是什么?它表示这些文件(含当前目录)总共占用的磁盘块数(块大小相关),是 ls -l 对目录做的一个总计,不用太纠结。

合并选项是 Linux 惯例:ls -al 等价于 ls -a -l,一个减号后可以连着跟多个单字母选项。这个习惯贯穿所有命令。

站在起点:绝对/相对都能走——再聊路径的应用

前面 cd 里我们已经体会到路径了。这里稍微补一点实践技巧:

用绝对路径定位目录或文件:

cd /home/whb/test       # 直接到指定目录
ls /home/whb            # 列出别处的目录内容(不进去也能看)

用相对路径定位目录或文件:

cd ../temp              # 上一级再进 temp
ls ../who/are/you       # 用相对路径看别处的文件

用 ls -l 配合 -d 看某个目录自身的详细信息:

ls -ld  /home/whb/test   # 显示 test 目录本身的权限与信息,而不是它的内容

小坑提醒:当目录或文件名含空格时,直接写会被 Shell 拆成多个参数。解决方法是加引号或用反斜杠转义:

touch "my file.txt"     # 用双引号包住含空格的名字
touch my\ file.txt      # 或用反斜杠转义空格,两者等价
ls my\ file.txt         # 访问时同样要处理空格

这个坑在路径处理里极为常见,放这里先打个预防针。

建目录:mkdir 与 rmdir

mkdir:创建目录

mkdir(Make Directory,创建目录)。语法:mkdir [选项] 目录名。

常用选项:

  • -p / --parents:递归创建。若路径中的某些中间目录尚不存在,加上这个选项后系统会自动把它们一并建出来,即一次可以建立多层目录。
mkdir mydir                     # 创建单个空目录
mkdir -p path1/path2/path3/path4   # 一次性递归创建整条路径的所有层级
tree path1                      # 用 tree 以树状查看(下面有 tree 说明)

tree 命令会把目录递归地画成一棵树,非常直观。它不一定预装,如果没有:root 用 yum install -y tree(CentOS/RHEL 系),普通用户可能要用 sudo yum install -y tree(需要 sudo 授权,后面讲 sudo 时你会明白普通用户暂时装不了)。在 Debian/Ubuntu 系则是 sudo apt install -y tree。

rmdir:删除空目录

rmdir 是 mkdir 的反操作,但它有个严格限制——只删除"空"目录(里面没有文件和子目录)。语法:rmdir [-p] 目录。选项 -p:当子目录被删除后,如果父目录也因此变成空目录,就连带把父目录也删除。

rmdir mydir                     # 删除一个空目录
rmdir -p path1/path2/path3/path4   # 逐级删:删 path4,path3 变空再删,一路删到 path1

这里有两个教科书级的坑,我建议你亲手踩一遍:

坑一:rmdir 目标目录里只要有一个文件/子目录,就报 "Directory not empty"(目录非空),删不动。 坑二:rmdir -p 一旦在某层遇到非空目录,就会"卡住"——它只能把"从末级向上、每一层都恰好变空"的链条删干净,一旦中间某一级还有东西,它就不再继续向上(也不删那一级),并报错。比如:

mkdir -p path1/path2/path3/path4
touch path1/myfile.txt          # 在 path1 里塞一个文件
rmdir -p path1/path2/path3/path4   # 会失败:删掉 path4、path3 后,要删 path2 时……
# 报错:rmdir: failed to remove directory 'path1/path2': Directory not empty
# (因为再往上 path1 里面有 myfile.txt,系统在删到 path2 这一级就检测到 path1 非空,
#   于是不再继续,连 path2 也保留了下来)

这个例子清晰说明了 rmdir -p 的"逐级向上检查"逻辑。

更强大的删除工具:rm

因为 rmdir 只能删空目录、普通文件还删不了,实战里大杀器是 rm(remove,删除)。它可以同时删除文件或目录。语法:rm [选项] 文件或目录。

  • -f:force,强制删除。即使文件属性是只读(写保护)也直接删除,不询问。
  • -i:interactive,删除前逐一询问确认。
  • -r:recursive,递归删除目录及其下所有子文件。删目录几乎必须加它(或 -rf)。
rm  file.txt              # 删除普通文件
rm  dir                   # 报错:cannot remove 'dir': Is a directory(rm 默认不删目录)
rm -r dir                 # 加 -r 才能递归删除目录
rm -i newFile.txt         # 删除前询问:rm: remove regular empty file 'newFile.txt'? y

递归删除时,-i 会对每个目录和文件都逐一询问(一层层往下问 "descend into directory 'd'?" 再问 "remove directory 'd/d/d/d'?"):

mkdir -p d/d/d/d
rm -ri d     # 对每一层目录、文件逐一询问,全部回答 y 后才能删干净

而真正的"原子弹"/"核按钮"组合是 rm -rf:

rm -f  path1          # 报错:cannot remove 'path1': Is a directory(-f 不是递归,删不了目录)
rm -rf path1          # 递归+强制,一次性连目录带整个子树全部删掉,绝不询问

这里我必须郑重给你两个保命的提醒:

第一,rm -rf 一旦敲下去,没有回收站,没有撤销。 手滑 rm -rf / 之类,能把你整个系统从根删掉。强烈建议你在学习阶段只在临时目录练习,并且对重要文件多用 rm -i 或添加一个 alias(别名)把 rm 默认改成 rm -i(后面 alias 一节讲)。

第二,给别人 / 给目录的写权限,不等于别人就能凭玄学绕过你的目录管理,删除的依据在"除非目录被设成粘滞位否则任何有目录写权限的人都能删目录里的文件"这个规则上(粘滞位最后讲)。现在你只要记住 rm -rf 的破坏力和不可逆。

人不可能记下所有参数:man

Linux 命令的参数极多,没人能全记住。所以有个命令是专门"查别的命令"的—— man(manual,联机手册)。语法:man [选项] 命令。

常用方式:

  • man 命令:查看该命令的手册。比如 man ls。
  • man -k 关键字:用关键字搜索联机帮助,找到相关命令。
  • man 章节号 命令:指定在第几章查找。

man 手册共 9 章(不同系统略有差别),不同章节里同名条目含义完全不同,这是个大坑,务必理解:

章节内容例子
1普通命令(用户命令)man ls
2系统调用(函数,编程用)man fork、man open、man write
3库函数man printf、man 3 printf
4特殊文件(/dev 下各种设备文件)略
5文件格式(配置文件的字段含义)man 5 passwd
6给游戏预留略
7附加说明和一些变量man 7 signal、man 7 environ
8系统管理命令(通常只有 root 能用)man 8 ifconfig
9略略

关键就在:同一个词,在不同章节可能是"一个命令"也可能是"一个系统调用"或"一个库函数"。 比如:

man printf        # 不指定章节,从第 1 章开始找;printf 通常是 shell 命令,查到第 1 章
man 3 printf      # 指定第 3 章:查 C 语言的库函数 printf 的用法(含头文件、原型)
man fork          # fork 没有命令只有系统调用,man 会自动去第 2 章找
man 2 fork        # 明确查第 2 章的 fork(系统调用),能看到需要包含的头文件和函数原型
man man           # 查看"man 命令本身"的使用说明

man -a 选项会把所有章节都依次显示出来:man printf 缺省从第一章开始搜,命中了就停;用 -a 则当你在第一处按 q 退出后,它会继续往后搜,直到把后面所有章节都搜完。这在想确认"某个词到底有哪几个条目"时很有用。

小贴士:作为系统编程学习者,第 2 章(系统调用)和第 3 章(库函数)是你查询的头号宝库——通过 man 2 open 你不仅能查到函数签名,还能直接看到"需要 include 哪个头文件"。这是课件反复强调 man 意义所在:它不只是查命令,更是查 C 系统编程接口的词典。

复制:cp

cp(copy)复制文件或目录。语法:cp [选项] 源文件或目录 目标文件或目录。

说明很重要:

  • cp 用于复制文件或目录。
  • 如果同时指定两个以上的源,并且最后一个参数是一个已存在的目录,则会把前面所有的源都复制到这个目录里去(这叫"复制到目录")。

常用选项:

  • -i / --interactive:目标文件已存在、准备覆盖之前先询问用户。
  • -r / --recursive:递归处理,把目录下的所有子文件、子目录一并复制;若源不是目录/符号链接,一律按普通文件处理。
  • -f / --force:强制复制,若目标已存在就直接覆盖。
# 复制普通文件
echo "你好比科技就业课" > myfile.txt     # 先用 echo + 重定向往文件写内容(重定向后面讲)
cat  myfile.txt                          # 查看内容:你好比科技就业课
cp myfile.txt myfile-backup.txt          # 复制出一份备份
cat  myfile-backup.txt                   # 内容一样
 
# 用通配符把多个文件复制到目录 *
mkdir dir
cp *.txt dir                             # * 是通配符,匹配所有以 .txt 结尾的文件名,一并拷进 dir
tree dir                                 # 看到 dir 下多了两个 .txt
 
# 目标文件存在就覆盖
echo "hello bite tech" > myfile.txt
cp myfile.txt myfile-backup.txt          # 直接覆盖
cp -i myfile.txt myfile-backup.txt       # 覆盖前询问:cp: overwrite 'myfile-backup.txt'? y
 
# 递归+强制复制整个目录
cp -rf test test-backup                  # 把 test 整个目录(含子目录)复制成 test-backup
tree test-backup                         # 目录结构完整复制

关于 echo "xxx" > file 这一行,先按"往文件里写文本"来理解,右侧那个 > 是输出重定向,它的含义是"把本来要打印到屏幕的内容,改道写进文件"。重定向在后面的段落里专门展开,这里你先会用即可。

移动与改名:mv

mv(move)可以移动文件,也可以给文件改名(move / rename)。语法:mv [选项] 源文件或目录 目标文件或目录。

它的行为由第二个参数(目标)的类型决定:

  • 第二个参数是"名称"(目标文件不存在或你只想改名):mv 完成重命名——此时源只能有一个,把源改名成目标名。
  • 第二个参数是"已存在的目录":mv 把源移动到该目录里——此时源可以有多个,统统移过去。

常用选项:

  • -i:若目标已存在,询问是否覆盖。
  • -f:强制,目标已存在也不询问直接覆盖。
# 改名
touch myfile.txt
mv myfile.txt yourfile.txt     # myfile.txt 改名为 yourfile.txt
 
# 目标同名 → 覆盖
touch myfile.txt
mv yourfile.txt myfile.txt     # 直接用 yourfile.txt 覆盖掉 myfile.txt 的内容
 
# 覆盖前询问
mv -i yourfile.txt myfile.txt   # mv: overwrite 'myfile.txt'? y
 
# 移动整个目录(到上级目录)
mkdir temp
mv myfile.txt temp             # 把文件移动进 temp 目录
mv temp ../                    # 把整个 temp 目录移动到上一级目录
ls -d ../temp                  # 用 -d 确认上一级的 temp 目录存在

一个容易混的点:mv 在"同文件系统内"只是改了目录项记录,非常快;但如果跨文件系统(比如从 /home 移到 /tmp,二者可能不在一块磁盘上),mv 会退化为"先复制再删除",速度会慢很多。这是为什么大文件经常建议用 cp+rm 或直接用 mv 前先确认在同一个分区。入门不深究,记住这个现象即可。

查看文件内容:cat / more / less / head / tail

这一组命令都是"看文件内容"的,但各有侧重,是日后的高频工具。

cat:一股脑全打印

cat(catenate,连接)功能:把目标文件的内容输出到屏幕。它能同时输出多个文件(把多个文件的内容拼接输出,这也是它名字"连接"的由来)。语法:cat [选项] [文件]。

常用选项:

  • -b:对非空输出行编号(空行不编号)。
  • -n:对输出的所有行编号(空行也算,从 1 开始)。
  • -s:把连续的多行空行压缩成一行(不输出连续空行)。
# 先造一个 21 行的文本
cnt=0; while [ $cnt -le 20 ]; do echo "hello bite"; let cnt++; done > temp.txt
cat temp.txt            # 打印全部内容
 
cat -b temp.txt         # 非空行编号,空行不编号
cat -n temp.txt         # 所有行编号,含空行
cat -s temp.txt         # 连续空行压缩成一行

注意上面那行造数据的命令,while ... done 是一个循环(这段先不深挖 shell 脚本语法,以后讲),重点看 > temp.txt——把循环的输出重定向到文件 temp.txt,这样省去手写 21 行。这就是"命令行构建多行文本"的常用手法。

more:看一屏停一屏

more 用来分页显示,适合长文件。当你 cat 一个几千行的文件时内容会瞬间刷屏根本看不完,more 就派上用场。它与 cat 类似,但会"逐屏展示,按任意键继续"。

常用操作(选项/按键):

  • more -n 文件:指定每次输出 n 行。比如 more -10 temp.txt 先显示 10 行。
  • 底部显示 --More--(0%),表示已看了百分之多少。
  • 按 q 退出 more;按空格/回车继续往下看。
# 造一个 2001 行的文件
cnt=0; while [ $cnt -le 2000 ]; do echo "hello bite"; let cnt++; done > temp.txt
more -10 temp.txt    # 一次显示 10 行,按回车继续

less:可以往回翻的 more

less 是比 more 更强大的分页工具,很多人认为是 Linux 查看文件内容的正统工具。它的强大在于:

  • 可以前后翻页:more 只能往后看,less 能往后也能往前(PageUp/PageDown、方向键)。
  • 搜索功能更强:既可以向下搜,也可以向上搜。
  • 查看大文件更省内存:less 在查看之前不会加载整个文件到内存,而是按需读取,所以几十 GB 的日志也能轻松打开。

常用选项/按键:

  • -i:搜索时忽略大小写。
  • -N:显示每行的行号。
  • /字符串:向下搜索字符串。
  • ?字符串:向上搜索字符串。
  • n:重复刚才的搜索(向下找下一个匹配)。
  • N:反向重复刚才的搜索(向上找上一个匹配)。
  • q:退出 less。
cnt=0; while [ $cnt -le 2000 ]; do echo "hello $cnt"; let cnt++; done > temp.txt
less -N temp.txt     # 带行号进入分页,PageUp/PageDown 翻页,输入 /56 向下搜到含 56 的行

head / tail:只看头或只看尾

head 显示文件开头,默认前 10 行。语法:head [参数] [文件]。选项 -n<行数> 指定看几行。

tail 显示文件末尾,默认后 10 行。语法:tail [参数] [文件]。选项 -n<行数> 指定看几行,-f 是循环读取(follow)——持续刷新尾部,专门用来实时盯日志(比如 /var/log/xxx.log)。

head temp.txt          # 默认前 10 行
head -5 temp.txt       # 前 5 行
tail temp.txt          # 默认后 10 行
tail -3 temp.txt       # 最后 3 行
tail -f 某日志文件.log  # 持续跟着日志尾部刷新,Ctrl+C 退出

综合思考题:如何显示文件的[180, 200]行?

思路拆解(分三步想清楚,这就是管道思维): ① tail -20 能给我们某段输出的后 20 行; ② 我们想要的是"从第 180 行到第 200 行",这一共是 200 - 180 + 1 = 21 行,而 180 到 199 是 20 行,说"取 180199"是 20 行,但题目要 180200 共 21 行; ③ 解法:先用 head -200 取前 200 行(恰好是 1~200),再把这 200 行交给 tail -20 取最后 20 行,于是得到 181~200。 如果你真想得到 180 行开始,注意 tail 的取法是基于行数的,所以标准答案是:

head -200 temp.txt | tail -20

它得到 181~200 行(课件样例输出正是 hello 180~hello 199 对应的那个区间,取决于文件从 0 还是 1 编号)。这个组合里出现了管道符 |(竖线),它的含义是"把左边命令的输出,直接作为右边命令的输入"。这里先记下这个用法,后面专门的管道小节你会彻底理解。

顺带强调边界:如果文件不足 200 行,head 会输出实际全部行,tail -20 也就只能取到剩下的;处理行数大于文件行数的输入时,C 系工具一般不会报错,只是输出能输出的部分。

pipe(管道)与 redirection(重定向):Linux 的灵魂

前面 >、| 都出现了,现在把这块一次讲透,因为它是你日后组合命令的根基。

先明确三个标准输入输出记号:

  • 标准输入(stdin):程序默认从"键盘"读数据。
  • 标准输出(stdout):程序默认把结果打印到"屏幕"。
  • 标准错误(stderr):程序默认把错误信息打印到"屏幕"(和 stdout 其实是两个独立的"管道",都可以分别重定向)。

重定向(redirect) 就是"把本来去 A 地方的数据,改道送到 B 地方"。常用符号:

  • >:把标准输出写入文件(覆盖原有内容)。echo "hi" > f.txt
  • >>:把标准输出追加到文件末尾(不覆盖)。echo "hi" >> f.txt
  • <:把文件内容作为程序的标准输入。grep abc < f.txt
  • 2>:把标准错误写入文件。ls 不存在的目录 2> err.txt
  • 2>&1:把标准错误"并入"标准输出(常配合 > 一起,把两者都写进同一个文件)。
# 重定向:写文件
echo "第一条" > a.txt      # a.txt 内容=第一条
echo "第二条" > a.txt      # 覆盖:a.txt 内容变成只有"第二条"
echo "第三条" >> a.txt     # 追加:a.txt 现在是"第二条"和"第三条"两行
 
# 重定向:把错误单独存到文件
ls 肯定不存在的目录 2> err.log   # 错误信息写进 err.log,屏幕上干干净净

管道(pipe)| 则是把前一个程序的标准输出,直接接到后一个程序的标准输入,实现"流水线"般的数据流传递。它和连接软管是一个意思——A 的出口接到 B 的进口。

cat a.txt | grep "第二条"       # 把 cat 的输出喂给 grep,grep 只打印含"第二条"的行
ls -l | head -5                 # 列出前 5 行的详细信息

一个著名的管道技巧,配合前面学的 head/tail 给"若干行"切片:

head -200 temp.txt | tail -20    # 打印文件的第 181~200 行

给 grep 搭配管道是运维日常:

ps aux | grep ssh                # 在进程列表里筛出与 ssh 相关的行

你会逐渐发现,管道 + 重定向 + grep/find/head/tail 的排列组合,能解决 90% 的日常排查需求,这就是"一个程序做一件事、用管道组合起来"的 UNIX 哲学。

日期与时间:date 与 cal

date:显示/设置时间,以及时间戳

date 用来显示系统时间,也可(在 root 权限下)设置时间。语法:date [选项] ... [+格式]。

显示格式:前面写一个 + 号,后面跟标记,常用:

  • %H:小时(00–23)
  • %M:分钟(00–59)
  • %S:秒(00–61,60/61 是闰秒占位)
  • %X:相当于 %H:%M:%S(时分秒)
  • %d:日(01–31)
  • %m:月(01–12)
  • %Y:完整年份(0000–9999)
  • %F:相当于 %Y-%m-%d(日期)
date                    # Wed Jan 11 18:22:50 CST 2023
date +%Y/%m/%d          # 2023/01/11
date +%Y/%m/%d-%H:%M:%S # 2023/01/11-18:23:20

注意上面输出里出现的 CST,全称 China Standard Time(中国标准时间,UTC+8),它表示你所在的时区。这对理解下面"时间戳 → 时间"的偏移很关键。

设置时间(只有 root 才能设置,普通用户只能查看):

date -s 20080523                 # 只设置日期,时间为 00:00:00
date -s 01:01:01                 # 只设置时间,日期不变
date -s "01:01:01 2008-05-23"    # 格式有多种,都可设全部时间
date -s "2008-05-23 01:01:01"
date -s "20080523010101"

时间戳(Unix timestamp):这是系统编程里极高频的概念,值得单独讲透。Unix 时间戳,是从 1970 年 1 月 1 日(UTC/GMT 的午夜,即格林尼治时间零点)开始,所经过的秒数(不考虑闰秒)。它也叫 Unix epoch / POSIX time。因为它是纯秒数、与时区无关,全世界的机器都能用同一个数精确对齐某个时刻,所以它是计算机内部表示时间的标准方式。

  • 时间 → 时间戳:date +%s
  • 时间戳 → 可视时间:date -d @时间戳
date +%s            # 例如 1673432636(自 1970-01-01 00:00:00 UTC 起的秒数)
date -d @0                              # 1970-01-01 00:00:00 UTC
date +%Y/%m/%d-%H:%M:%S -d @0           # 1970/01/01-08:00:00

注意到 date -d @0 显示的是 08:00:00 而不是 00:00:00 了吗?这正是上面提到的 CST 时区:时间戳 0 代表的是"世界标准时间(UTC)1970-01-01 00:00:00",而你身处 UTC+8 时区,所以加 8 小时之后显示成 1970/01/01-08:00:00。这个"8 小时偏移"是新手最容易困惑的地方,也是理解时间戳"与时区无关、换算由显示时决定"的最好例证。

date +%Y/%m/%d-%H:%M:%S -d @100000        # 1970-01-02 11:46:40(约 27.8 小时后)
date +%Y/%m/%d-%H:%M:%S -d @1000000000   # 2001/09/09-09:46:40(10 亿秒大约是 2001 年 9 月)

我还可以随手验证一个里程碑:十亿秒(1e9 秒 ≈ 31.7 年)恰好落在 2001 年,这也是不少程序里外的彩蛋边界。理解时间戳后你会发现,以后 C 语言的 time()、文件的 mtime、数据库的时间字段,底层几乎都是用这种秒数存的。

cal:看日历

cal(calendar,日历)用于查看公历(阳历)日历。语法:cal [参数] [年份]。一个参数表示年份(1–9999),两个参数表示"月份 年份"。

常用选项:

  • -3:显示前一个月、当前月、后一个月的日历。
  • -j:显示"一年中的第几天"(儒略日风格,从 1 月 1 日算起)。
  • -y:显示当前一整年的日历。
cal                 # 显示当前月
cal 1949            # 显示 1949 年全年(每三个月并排)
cal -3              # 显示上个月/本月/下个月
cal 10 2024         # 显示 2024 年 10 月

查找:find / which / whereis / grep

这一组都是"查找"类,但对象完全不同,别混。

find:按文件名、按条件全盘/树下搜索

find 在目录树中搜索文件并执行指定操作。它的选项极多、功能极强,是运维排障的核心工具。语法:find pathname -options。含义:在 pathname 指定的路径(树)里,按条件递归查找文件,可能真正访问磁盘,所以在大文件系统(比如 30G 以上)上跑很消耗资源,很多人会把它放后台执行。

常用选项(入门先掌握 -name,其余用到再查 man):

  • -name 文件名:按文件名精确/通配搜索(支持 * 通配符)。
find ~ -name test.c     # 在主目录整棵子树里找所有叫 test.c 的文件
find / -name "*.log"    # 全盘搜索(慢),找所有 .log;不想权限报错可加 2>/dev/null
find ~ -name "test*"    # 用通配符,匹配以 test 开头的名字

结合前面学的管道,你能做很多花样:

find /etc -name "*.conf" | head -20      # 列出 /etc 下前 20 个 conf 文件
find ~ -maxdepth 2 -name "*.c"           # 只往下找两层(maxdepth 限制深度,可以避免满树遍历)

find 还支持按大小 -size、按时间 -mtime、按类型 -type 等。今天先把 -name 这个"按名字找"用熟。

which:命令是啥,在哪

which 用来查"某个命令程序"在哪个路径下,顺带会告诉你它是不是别名。它只在 PATH(环境变量,系统找命令时按顺序搜索的一系列目录,用冒号分隔)里找。

which ls      # alias ls='ls --color=auto'    /usr/bin/ls   (先显示别名,再显示真实路径 /usr/bin/ls)
which pwd     # /usr/bin/pwd

注意 which ls 输出里先看到一行 alias ls='ls --color=auto'——这是说 ls 其实是别名(alias,下面马上讲),真实程序在 /usr/bin/ls。这解释了"为什么 which 能告诉你命令来源"。

whereis:连手册一起找

whereis 比 which 找得更全:会找到程序的源文件、二进制文件、帮助手册(man)。它通常直接查一个数据库,速度快。

whereis ls       # ls: /usr/bin/ls /usr/share/man/man1/ls.1.gz /usr/share/man/man1p/ls.1p.gz
whereis libc.so  # libc: /usr/lib64/libc.so /usr/lib64/libc.a /usr/share/man/man7/libc.7.gz

第二个例子有意思:你查 libc.so,它连二进制库文件和对应的 man 手册(libc.7.gz)都标出来了。

alias:给命令起绰号

alias 用来设置命令的别名——给一个长命令起个短名,或者给命令附加默认参数。语法:alias 别名='完整命令'。

alias hello='ls -a -l -n'   # 把 hello 定义为 "ls -a -l -n"
which hello                 # alias hello='ls -a -l -n'  /usr/bin/ls
hello                       # 执行 hello 就等价于执行 ls -a -l -n

几个关键点:

  • alias 只在当前这个 shell 会话里临时有效,关掉终端就没了。想永久保存要写进 shell 的启动配置文件(比如 ~/.bashrc)。
  • 写进配置文件后还要 source ~/.bashrc 让它立即生效。
  • 一个常用安全习惯:alias rm='rm -i',把危险的 rm 默认改成删前询问,保护新手(不过真正的高手在生产机器上往往反过来警觉)。
  • 查看当前所有别名:裸敲 alias。

grep:在文本里按行筛

grep(Global search for a Regular Expression, Print)在文件中搜索字符串,把命中的那一整行打印出来。它是"文本搜索"之王。语法:grep [选项] 搜寻字符串 文件。

常用选项:

  • -i:忽略大小写(大小写视为相同)。
  • -n:输出行号。
  • -v:反向选择——显示不包含搜索字符串的行。
cat temp.txt
# abcd
# ABCD
# hello
# bite
# 1234
 
grep "abcd" temp.txt     # 精确(区分大小写)匹配 → 输出 abcd
grep -i "abcd" temp.txt  # 忽略大小写 → 输出 abcd 和 ABCD
grep -n "abcd" temp.txt  # 带行号 → 1:abcd
grep -v "abcd" temp.txt  # 反向 → 输出不包含 abcd 的所有行(ABCD hello bite 1234)
grep -vn "abcd" temp.txt # 反向 + 行号 → 2:ABCD 3:hello 4: bite 5:1234
grep -vni "abcd" temp.txt# 反向+忽略大小写+行号 → 只剩 3:hello 4:bite 5:1234

grep 配管道极常用(几乎必然遇到):

ps -ef | grep ssh        # 找 ssh 相关进程(以后排查进程常用)
history | grep "ls"      # 在历史命令里找含 ls 的(前提是启用了 history)
tail -f /var/log/xxx.log | grep ERROR   # 实时盯日志,只显示 ERROR 行

进程与资源:top

top 动态显示系统进程与资源占用(类似 Windows 的任务管理器),实时刷新。

top -d 1 -n 5        # 每 1 秒刷新一次,一共刷新 5 次后自动停止
  • -d:刷新的时间间隔(秒)。
  • -n:刷新的次数,跑完自动退出。
  • 在 top 界面里按 q 退出,按 P 按 CPU 排序、按 M 按内存排序。

top 输出里你会看到一堆进程、以及 CPU/内存占用率。刚入门只需要会"打开、看谁最占资源、退出",以及能区分 top(动态实时)和 ps(静态瞬间快照)的差别。

压缩与打包:zip/unzip 与 tar

压缩打包是文件传输、备份最常用的操作。先辨析两个概念:打包(tar,合并文件,不减少内容长) 和 压缩(zip/gzip/bzip2,减少占用空间)。它们可单独用也可结合用。

zip / unzip:跨平台通用

zip 把文件或目录压缩成 .zip 格式(Windows 也认,互相传输方便)。语法:zip 压缩包.zip 文件或目录。选项 -r:递归处理目录下所有文件与子目录。

zip test2.zip test2/*         # 把 test2/* 里的东西压进 test2.zip
zip -r test2.zip test2        # 递归压缩整个 test2 目录
unzip test2.zip -d /tmp       # 解压到 /tmp 目录(-d 指定解压目标)
unzip -l test2.zip            # 只列出 zip 内容,不解压

rzsz:Windows 和 Linux 互传文件

rzsz(lrzsz)是一对工具,用于在 Windows 机器和远端 Linux 机器之间,通过 XShell 这类终端传输文件。安装后,在 XShell 里可以直接把本地文件拖拽上传到 Linux(或使用 rz 上传、sz 文件名 下载)。

# 安装(CentOS/RHEL 系用 yum,Debian/Ubuntu 系用 apt):
sudo yum install -y lrzsz      # 或 sudo apt install -y lrzsz
rz                             # 上传(弹窗选本地文件)
sz 远程机器上的文件             # 下载到本地

它是无数服务器运维人员日常"送文件/取文件"的起点工具。

tar:打包/解包的全能选手(重点)

tar(Tape Archive)本是"磁带归档",现在用它打包和解包文件,且可配合压缩算法生成 .tar.gz / .tar.bz2 等。语法:tar [-cxtzjvfC] 文件与目录...。它不靠减号也能用(比如 tar xvf),但新手建议理解参数再写。

参数表(逐个讲):

  • -c:create,建立打包 / 归档文件。
  • -x:eXtract,解开一个打包文件。
  • -t:查看 tar 文件里有哪些文件(不真正解开)。
  • -z:是否用 gzip 压缩(生成 .tar.gz / .tgz)。
  • -j:是否用 bzip2 压缩(生成 .tar.bz2)。
  • -v:verbose,压缩/解压过程中显示处理的文件(好用,但别在后台跑这个选项,会刷屏)。
  • -f:file,后面必须紧跟档名(-f 之后就是档案路径),且 -f 之后的参数不要再塞别的。
  • -C:解压到指定目录。
  • -p:保留文件的权限属性(备份时很重要)。

记住规律:要不要带 -z/-j 取决于你的档案后缀。看 .tar.gz 就要带 -z,看 .tar.bz2 就要带 -j,纯 .tar 就不带压缩选项。

# 打包:仅打包 (不压缩)
tar -cvf /tmp/etc.tar /etc
# 打包 + gzip 压缩
tar -zcvf /tmp/etc.tar.gz /etc
# 打包 + bzip2 压缩
tar -jcvf /tmp/etc.tar.bz2 /etc
 
# 查看 tar 包内的文件列表(用了 gzip 就得加 -z)
tar -ztvf /tmp/etc.tar.gz
 
# 解压(在目标目录内解开)
cd /usr/local/src
tar -zxvf /tmp/etc.tar.gz      # 解出的 etc 目录就在当前目录下
# 或解压到指定目录
tar -zxvf /tmp/etc.tar.gz -C /usr/local/src
 
# 只解压其中一个文件
cd /tmp
tar -zxvf /tmp/etc.tar.gz etc/passwd   # 只解包 etc/passwd 这一个文件
 
# 保留权限的解压(备份场景关键)
tar -zxvpf /tmp/etc.tar.gz /etc
 
# 按时间只备份比某个时间点新的文件
tar -N "2005/06/01" -zcvf home.tar.gz /home
 
# 排除某目录再打包
tar --exclude /home/dmtsai -zcvf myfile.tar.gz /home/ /etc

几个 tar 的常见坑,务必注意:

坑一:用绝对路径打包时的警告。tar -cvf /tmp/etc.tar /etc 常会打出一句:

tar: Removing leading `/' from member names

这是 tar 的安全特性:它把归档里记录的路径最前面的那个 / 去掉,让档案里的路径变成相对的。目的是防止解压时把文件直接按绝对路径写进系统目录、覆盖掉你真实的 /etc——那会是灾难。所以你看到这句不是错误,是保护。解压时它默认解到"当前目录下的相应相对路径"。

坑二:-f 后面的参数顺序不能乱。tar -xzf /tmp/etc.tar.gz 正确;写成 tar -zf /tmp/etc.tar.gz x 就错了,因为 -f 之后紧跟的就是档名。

坑三:看 .tar.gz 忘了加 -z。用 tar -tvf /tmp/etc.tar.gz 查 gzip 压缩包,tar 会因为认不出格式而报 gzip: stdin: not in gzip format 之类错误。所以后缀和参数必须配套。

一个高级技巧(管道打包):

cd /tmp
tar -cvf - /etc | tar -xvf -      # 把 /etc 打包到标准输出 '-',再管道给下一个 tar 解到当前目录

这里出现了三个符号的含义:-。当 -f 后面跟 - 时,表示"把档案写到标准输出(stdout)"而不是文件;第二个 tar -xvf - 里的 - 表示"从标准输入(stdin)读取";中间的 | 是把前者的 stdout 接到后者的 stdin。整体效果约等于 cp -r /etc /tmp。这个例子不仅帅,还帮你彻底打通了"标准输出/标准输入/管道"三者。

思考题:tar 中 -C 和解压前 cd 到一个目录有区别吗?

答案:本质都一样——都是让"解压的落脚点"变成目标目录,从而 tar 里的相对路径都在该目录下展开。区别在写法和心智:cd 目标 && tar -zxvf ... 是先切目录再解;tar -zxvf ... -C 目标 是原地让 tar 自己切。二者结果一致,但 -C 更安全——它不改变你当前 shell 的工作目录,一股脑解压也不会污染当前环境;所以脚本里更推荐 -C。另一个易错点是:-C 指定的目录必须已存在,否则会解压失败。

其它常用命令:bc / uname / lsb_release

  • bc:任意精度浮点计算器。bc 进去后直接敲 1.2 + 3.4 回车得到结果,quit 退出;也支持 echo "3.14*2" | bc 管道用法。
echo "3.5 * 2" | bc    # 7.0
  • uname:获取电脑与操作系统信息。uname -r 看内核版本,uname -a 详细输出所有信息(依次为:内核名称、主机名、内核版本号、内核版本、硬件名、处理器类型、硬件平台、操作系统名称)。
uname -r       # 内核版本号,如 3.10.0-1160.el7.x86_64
uname -a       # 全部信息
lsb_release -a # 查看更友好的操作系统版本信息

三个救命/省事热键:[Tab]、[Ctrl]-c、[Ctrl]-d

  • [Tab]:命令补全 + 文件名补全。敲一半命令或路径按 Tab,Shell 会帮你补全;有多个候选再按一下 Tab 会列出所有可选。这是提升命令行效率的第一神器,强烈建议养成"敲几个字母就按 Tab"的习惯。
  • [Ctrl]-c:让当前正在运行的程序立即停止(发送中断信号)。你 cat 一个大文件想停、ping 一直跑想停、不小心启动了 GUI 程序想停,都靠它。
  • [Ctrl]-d:End Of File(EOF,结束输入标志)。在交互式输入里,它表示"键盘输入结束",可以直接用来取代 exit——把一个交互式 shell(或某个期待 EOF 退出状态的程序)正常结束掉。比如你在终端登录后,敲 Ctrl-d 就能退出登录(等价于输 exit)。

关机:shutdown

服务器运维里偶尔要安全关机/重启。shutdown 语法:shutdown [选项]。

  • -h:把系统所有服务停掉后,立即关机(halt)。
  • -r:把服务停掉后,重启系统(reboot)。
  • -t 秒数:-t 后接秒数,表示"过多少秒后关机"。
shutdown -h now     # 立即关机
shutdown -r now     # 立即重启
shutdown -h 10      # 10 分钟后关机
shutdown -c         # 取消刚才安排的关机

作为刚入门的你,练习机器上尽量别乱关机/重启,一旦关掉就只能去机房/云控制台再开机了。通常我们学习的机器都是远端服务器,关机要慎之又慎。

权限:Linux 管理安全的灵魂

课件里"权限"是重头戏,我们放慢、拆透。

两种用户:root 与普通用户

Linux 下有两种用户:

  • 超级用户(root):可以在 Linux 系统里做任何事,不受限制。命令提示符是 #。
  • 普通用户:在 Linux 里做有限的事,受权限约束。命令提示符是 $。

切换用户的命令是 su(substitute user / switch user):

su 用户名      # 切换到指定用户
su root        # 切到 root(会提示输入 root 密码)
su -           # 切换到 root,并同时切换环境变量(推荐用 -,环境更干净)

你看 prompt 就知道当前是谁:root 看到 #,普通用户看到 $。 这是最直观的"我在哪、我是谁"体检表。

文件访问者分三类("人"的维度)

对一个文件/目录,能访问它的人被分成三组:

  • u(User):文件的拥有者 / 所有者 / owner("这块地的主人")。
  • g(Group):文件所属组的成员("和主人同住在一个集体里的亲属/同事")。
  • o(Others):其它用户("陌生人/外国人")。
  • 有时还提到 a(all):所有用户(u+g+o 三者的合称),chmod 里用 a 表示"全部人"。

文件类型与三种基本权限("事的维度")

前面"一切皆文件"讲过文件类型首字母。现在说三种基础权限(读/写/执行),注意它对文件和目录的含义完全不同,这是最重要的区分:

权限字母/数字对文件的意义对目录的意义
读r / 4(Read)可以读取文件内容(能 cat)可以浏览该目录的信息(能 ls 出里面的名字情况)
写w / 2(Write)可以修改文件内容可以在目录里创建、删除、移动文件
执行x / 1(eXecute)可以运行/执行该文件可以进入该目录(能 cd 进去,能访问里面的文件)
无该项权限- / 0——

画个重点帮你记:目录的"读"管你能不能"看清单",目录的"写"管你能不能"增删里面的文件",目录的"执行"管你能不能"进去"。 这三者缺一不可、彼此不重叠。

权限的两种表示法

  1. 字符法:rwxr-xr--,9 个字符从左到右分三段:属主(u)、属组(g)、其它(o),每段是 r/w/x 的组合。
    • 例如 -rw-rw-r--:属主 rw-(可读可写不可执行)、属组 rw-、其它 r--(只读)。
  2. 八进制数值法:把每段的 r=4、w=2、x=1 相加得到一个 0–7 的数字。rwx=7、rw-=6、r-x=5、r--=4、-wx=3、---=0。
    • 于是 rw-rw-r-- 写成三位八进制就是 664。这是最常用的快速写法。

ls -l 开头的 -rw-rw-r-- 和 stat 里的 0664 是同一个权限的两种表达,见到要能互翻。

改变权限:chmod

chmod(change mode)设置文件访问权限。只有文件的拥有者和 root 才能改变文件权限。格式:chmod [参数] 权限 文件名。选项 -R:递归修改目录里所有文件和子目录的权限。

chmod 有两种给权限的姿势:

姿势一:字符方式 u/g/o/a + - = 权限,其中:

  • 用户符号:u(拥有者)、g(所属组)、o(其它)、a(所有)。
  • 操作符:+ 增加权限、- 取消权限、= 直接设定(覆盖)。
chmod u+w /home/abc.txt    # 给拥有者增加写权限
chmod o-x /home/abc.txt    # 取消其他用户的可执行权限
chmod a=x /home/abc.txt    # 把所有用户(属主+组+其它)的权限都设定为 x(先清零再只给执行)
chmod u=rwx,g=rx,o=r abc.txt   # 分别设定:

姿势二:三位八进制数字:

chmod 664 /home/abc.txt    # 属主 rw-、属组 rw-、其它 r--
chmod 640 /home/abc.txt    # 属主 rw-、属组 r--、其它 ---
chmod 755 程序            # 属主 rwx、组 r-x、其它 r-x —— 可执行程序的经典权限
chmod -R 750 某目录       # 递归改整个目录树

修改拥有者:chown 与 chgrp

  • chown(change owner):改文件的拥有者。语法:chown [参数] 用户名 文件名。选项 -R 递归。
chown user1 f1               # 把 f1 的拥有者改成 user1
chown -R user1:group1 目录   # 递归改拥有者和所属组(chown 也可以一次改组:user:group)
  • chgrp(change group):改文件的所属组。语法:chgrp [-R] 组名 文件名。
chgrp users /abc/f2          # 把 /abc/f2 的所属组改成 users
chgrp -R 组名 目录            # 递归改目录及子项的组

umask:为什么新建文件的权限"不是 666"

一个很多新手困惑的问题:课件说"新建文件默认权限 = 666、新建目录默认权限 = 777",可你 ls -l 看自己刚 touch 出的文件却是 rw-rw-r--(664),往里 mkdir 出来的目录是 rwxr-xr-x(755),根本对不上。原因就在 umask(权限掩码)。

umask 功能:查看或设置"文件创建时的默认权限掩码"。真实计算公式是:

实际权限 = 默认权限 mask & ~umask

(& 是按位与,~ 是按位取反。也就是:因果关系是 "默认权限" 与 "umask 的反码" 做按位与。)

  • 默认(default)mask:新文件 0666、新目录 0777。
  • 超级用户默认 umask 通常是 0022,普通用户通常 0002。

来算普通用户(umask=0002)新建文件的权限: 0666 & ~0002 = 0666 & 0775 = 0664。为什么最后一位从 6 变成 4?因为 0002 意味着"要屏蔽掉其它用户(o)的 w(2)",于是 0666 去掉 o 的写权限,得 0664。directory 同理 0777 & 0775 = 0775,但目录通常还会补出执行位,所以你常看到 0755。总之结果就是:umask 决定"你新建出来的东西,默认把哪部分权限关掉"。

umask            # 查看当前掩码,例如 0022
umask 044        # 设置为 044(之后新建文件会屏蔽掉组和其它用户的读:变 622 / 733)
umask 755        # 设置为某种掩码
umask 022        # 很多服务器习惯设成 022

衙役法则:umask 是一个"管控默认安全级别"的旋钮,把数值调大 = 新文件权限更保守(更少默认权限);调小 = 更开放。生产机常设 0022(挡住对"组可写、其它可写"),绝不乱设成 0(那等于让所有新建文件人人可写,危险)。

file:识别文件真实类型

file 命令通过检查文件头部特征字节,判断一个文件的真实类型(不看扩展名)。语法:file [选项] 文件或目录...。

  • -c:详细显示命令执行过程(便于调试/分析,很少用)。
  • -z:尝试"解读压缩文件里面的内容"。
file a.out            # a.out: ELF 64-bit LSB executable ...(是个可执行程序)
file readme.md        # readme.md: ASCII text(文本)
file 照片.jpg         # JPEG image data ...
file archive.tar.gz   # gzip compressed data ...

为什么强调"真实类型"?因为 Linux 一本书不靠扩展名判断。一个名叫 .txt 的文件,file 一看是 ELF 可执行程序,你才知道它是程序。这在排查"这个文件到底是什么"时极有用。

sudo:给普通用户"借"一点 root 的能力

sudo(superuser do)让被授权的普通用户,以 root(或其他用户)身份临时执行某条命令,而不用切到 root、不用交出 root 密码。它解决了"又需要管理员权限、又不该所有操作都以 root 跑"的矛盾。

用法:

sudo 命令                # 以 root 身份执行一条命令
sudo -u 用户名 命令       # 以指定用户的身份执行命令

例如:

sudo -u root /usr/sbin/useradd u21   # 以 root 身份添加用户 u21
sudo yum install -y tree              # root 才有权限装的包,普通用户可借 sudo

关键机制:谁有资格用 sudo,由 /etc/sudoers 这个配置文件决定。文件里每一行授权规则形如:

允许的用户  以哪台主机=可以以哪个身份(执行权限) 执行哪些命令

即"接受权限的用户 登录的主机 = (执行命令的用户) 命令"这个模板。安全的编辑方式是用 visudo(它会检查语法),不要直接 vi 乱改。常见一行示例:whb ALL=(ALL) ALL 表示"用户 whb 可在任何主机,以任何身份,执行任何命令"。

新手常见坑:

  • 普通用户第一次用 sudo 会要求输入自己的密码(不是 root 密码),且输入时屏幕不显示任何字符(不回显),这很正常,不是坏了。
  • 若 username is not in the sudoers file,说明该用户没被授权,需要 root 用 visudo 把他加进去。这也是课件里说"普通用户暂时装不了 tree"的原因。
  • 授权规则写错会导致"所有 sudo 都报 permission denied"甚至无法 sudo,改配置文件务必用 visudo 校验语法。

目录权限的"不科学之处"与粘滞位

现在到权限这节课最值得深思、也是课件专门留矛盾的地方。

先说结论:当你拥有一个目录的写权限时,你就能在该目录里删除文件——无论那个文件是不是你创建的、你是否拥有它。 之所以这样,是因为删除操作的依据是"目录的写权限",而不是"文件自身的权限"。

这乍一看很不科学。课件里特意演了一遍:root 把 /home 的权限开成 0777(谁都可写),于是普通用户 litao 就能执行 rm /home/root.c 把 root 创建的文件删掉。这也印证了前面表格里"目录的 w 权限 = 能增删该目录里的文件"。

思考题:我(张三)创建的文件,凭什么被李四删掉?这符合逻辑吗?

答案:这确实"不科学",但它是 Linux 目录权限模型的设计造成的自然结果——删除取决于"目录的写权限"而非"文件本身的写权限"。因为目录本质上是个"登记表",表里记录了每个文件项(文件名、inode 等)。把这个登记表里某一行"划掉"(删除文件),本质上是在修改这张登记表,所以系统只检查"你有没有这张表的写权限(目录的 w)",而根本不看"这张表里某个名字对应的那个文件你写不写得动"。所以只要李四对这个目录有 w 权限,他就能删掉张三在里面的文件。这正是"目录写权限的威力"。

为了解决这个明显有风险的问题,Linux 引入了一个特殊权限位——粘滞位(Sticky Bit,以字母 t 标记)。

粘滞位的作用:当一个目录被设置了粘滞位后,该目录下的文件只能被以下三类人删除:

  1. 超级管理员(root);
  2. 该目录的所有者;
  3. 该文件的所有者。

也就是说,设置粘滞位后,李四就算对该目录有写权限,也不能删除张三的文件——只有张三、root、或目录的主人能删。这就把"目录开放写权限"这件事变得可控了。

栗子:/tmp 目录就是最经典的粘滞位目录——全世界的用户都能往 /tmp 写临时文件,但谁也不能随意删除别人的临时文件。

设置粘滞位:

chmod +t /home          # 给 /home 加粘滞位
ls -ld /home            # drwxrwxrwt. —— 权限最后一位变成 t,就是粘滞位生效的样子

课件验证:

[root@localhost ~]$ chmod +t /home/
[root@localhost ~]$ ls -ld /home/
drwxrwxrwt. 3 root root 4096 ... ..
[root@localhost ~]$ su - litao          # 切到 litao
[litao@localhost ~]$ rm /home/abc.c      # litao 想删 root 或别人的文件
rm: 无法删除 '/home/abc.c': 不允许的操作   # 提示权限不足,删除失败

注意粘滞位在 ls -ld 权限串里出现的两种形态:

  • 一般用户:drwxrwxrwt —— t 表示有执行权限的粘滞位目录。
  • 若原本就没有执行位,会显示大写的 T(表示设置了粘滞位但无执行权限,这种目录几乎不可用)。

关于权限的总结(课件原文提炼,务必背下来):

  • 目录的可执行权限,表示你可否在目录下执行命令。
  • 如果没有目录的 -x 权限,则无法对该目录执行任何命令,甚至无法 cd 进入——即使目录仍然有 -r 读权限(这是最容易犯错的地方:以为"有读就能进去"。其实想"进去"看的是 x,不是 r)。
  • 如果目录具备 -x 但没有 -r:可以 cd 进入、可以在其中执行命令,但因为没有读权限,即使执行 ls 也读不出目录下的文件清单。

我们用一个小实验验证"有 x 无 r"的目录:

mkdir d1
touch d1/a.txt
chmod 111 d1      # 只有 x(111=--x--x--x),没有 r 和 w
ls d1             # 可能报 permission denied,或列出了名字却无法 stat 内容
cd d1             # 能 cd 进去(有 x)
ls                # 读不到清单(没 r)
cat a.txt         # 即使知道文件名也打不开内容(对文件访问还需要文件自身的权限 + 一路上目录的 x)
chmod 755 d1      # 恢复

综合思考题:"目录有读权限就能进去读取里面的文件"对吗?给出一组反例。

答案:不对。进入目录靠的是执行权限(x),不是读权限(r)。反例:chmod 666 某目录(r+w,无 x)——你能读出该目录的文件名清单(因为 r),但无法 cd 进目录、也无法以任何方式访问其中文件的内容(因为每访问一个文件,路径上必须有 x 才能"穿过")。再一次:对目录而言,r = 看清单,w = 改清单(增删文件),x = 走进门。 三者独立,缺哪个都办不了对应的事。这也是"有读权限就想当然能进去"这个常见误区的根源。

了解更多:扩展命令一览

作为系统编程的储备,课件末尾给了几组扩展命令,你可以用 man 现查现学。这里把它们归类列出,方便你知道"还有哪些常用工具":

  • 安装和登录:login、shutdown、halt、reboot、install、mount、umount、chsh、exit、last
  • 文件处理:file、mkdir、grep、dd(块级复制)、find、mv、ls、diff(比较文件差异)、cat、ln(链接,后面马上讲)
  • 系统管理:df(磁盘剩余)、top、free(内存)、quota、at、lp、adduser、groupadd、kill(杀进程)、crontab(定时任务)
  • 网络操作:ifconfig、ip、ping、netstat、telnet、ftp、route、rlogin、rcp、finger、mail、nslookup
  • 系统安全:passwd(改密码)、su、umask、chgrp、chmod、chown、chattr、sudo、ps(看进程)、who
  • 其它:tar、unzip、gunzip、unarj、mtools、man、unencode、uudecode

你不需要现在全学会,只需要建立"有这些类别"的印象,用到哪个查哪个。

文件链接:深入了解 inode 与 ln

最后补一个概念,它关系到你前面 stat 里看到的 Links 和 inode,是理解"一切皆文件"和软硬链接区别的关键——硬链接与软链接(符号链接)。

先建立 inode(index node,索引节点) 概念:磁盘上的每个文件,都有一个 inode,它是文件的"元数据身份证",记录着文件的权限、所有者、大小、时间戳、数据块的位置,以及链接数(Links)。文件名本身不存内容——文件名到 inode 的映射,记在它所在的目录文件里。你可以把文件名想成"名牌/门牌",inode 想成"房子",目录就是这本"门牌登记册"。

硬链接(hard link):给同一个 inode 再起一个名字(在某个目录里加一条记录),两个名字都指向同一个 inode、也就是同一份数据。特点:链接数增加、inode 相同、删掉一个名字数据仍在(只要还有一个链接指向它)。硬链接不能跨文件系统,也不能对目录建硬链接。

软链接(symbolic link / soft link):是一个独立的、很小的文件,里面存着"它指向的那个文件的路径字符串"。它类似 Windows 的快捷方式。特点:是一个 l 类型的独立文件、有自己的 inode、它本身可以跨文件系统、可以指向目录。缺点是:如果指向的目标被删了,软链接就"悬空"(broken)了。

创建命令是 ln(link):

ln   源 链接名      # 默认创建硬链接
ln -s 源 链接名     # -s 创建软链接
echo "共享数据" > data.txt
ln data.txt hard.txt          # 创建硬链接 hard.txt
ln -s data.txt soft.txt       # 创建软链接 soft.txt
 
ls -li data.txt hard.txt soft.txt
# 观察:data.txt 与 hard.txt 的 inode 相同(都是同一文件),soft.txt 有自己的 inode 且类型是 l
# 观察:data.txt / hard.txt 的 Links 数变成 2(硬链接让链接数+1),soft.txt 不影响链接数
 
cat data.txt; cat hard.txt; cat soft.txt   # 三者都能读到"共享数据"
 
rm data.txt
cat hard.txt     # 仍能读到"共享数据"(硬链接还在)
cat soft.txt     # 报错:No such file or directory(软链接悬空了)

这就是你今天要把握的软/硬链接的本质差别:

对比硬链接软链接
本质同一 inode 的另一个名字独立的、存目标路径的小文件
inode与目标相同自己独立
删除目标后只要还有其它链接,数据仍在立即"悬空",访问报错
跨文件系统不能可以
指向目录不能可以
类比同一个人穿两件马甲墙上的"入口→出口"路标

思考题:新建文件的 inode 链接数通常是多少?为什么会变化?

答案:普通文件默认为 1(只有一条目录记录指向它)。当你对这个文件再创建一个硬链接,链接数就变成 2(目录里多了第二条记录指到同一个 inode);每建一个硬链接 +1,每删一个链接 -1,当降到 0 时,inode 和它的数据块才会被真正回收(文件才从磁盘上消失)。而软链接不增加目标文件链接数,因为软链接只是存了路径字符串的独立小文件。目录的链接数则特殊:一个目录起初是 2(它自身在父目录里的条目 + 自己里的 .),每创建一个子目录 +1。你可以用 stat 或 ls -ld 验证这些数。

从 shell 命令到系统调用:这门课的小序章

说句题外话,把这门课串起来:今天的 ls、cat、cp 这些命令,底层其实都调用了 Linux 的系统调用(system call,内核提供给用户程序的接口,比如 open / read / write / close)。往后学"基础 IO"时,你会亲手用 C 语言调这些接口,而到那时,今天学的命令就是你理解它们的活标本——比如 cp 内部就是"打开源文件、打开目标文件、循环 read+write、关闭"。今天你把命令用熟,正是为下一步"看穿命令背后的内核"做的铺垫。


到这里,你已经把 Linux 的"入场券"领齐了:从它怎么来的(UNIX → Linux、开源与 GPL)、长什么样(发行版、目录树、一切皆文件、路径)、怎么进去翻看(pwd、cd、ls、cat/more/less/head/tail)、怎么增删改(touch、mkdir/rmdir、rm、cp、mv、ln)、怎么查(man、find、which、whereis、grep、file)、怎么带时间(date、cal、stat)、怎么通讯和打包(重定向、管道、zip、tar)、再到权限这套安全模型(root/普通用户、u/g/o、rwx、chmod/chown/chgrp/umask/sudo、粘滞位)。

这些命令看似各自孤立,实则环环相扣:ls -l 的权限串引出了 chmod;"一切皆文件"引出了文件类型与 inode;重定向与管道让你能把一堆命令串成流水线;权限模型又是你日后在真实服务器上"能做什么、被拒绝什么"的总裁判。我建议你别光读,把每一条命令都亲手敲一遍——把 rm -rf 的报错、把 chmod 111 之后的"进得去却看不见"、把软链接断掉后的报错,都真实地踩一遍。踩过的坑,才真正长在你身上。

下一步,我们会把"命令背后的 Shell 原理"和"基础 IO"讲深,让你从"会用命令"走向"看得懂命令在操作系统里干了什么"。你现在手里的这些命令,就是继续往下走的扶手。

准备好了吗?