《Linux系统编程(第2版)》——1.4 Linux编程的概念

简介: 在UNIX系统中,访问设备是通过设备文件来实现,把设备当作文件系统中的普通文件。设备文件支持打开、读和写操作,允许用户空间程序访问和控制系统上的(物理和虚拟)设备。UNIX设备通常可以划分成两组:字符设备(character devices)和块设备(block device)。

本节书摘来自异步社区《Linux系统编程(第2版)》一书中的第1章,第1.4节,作者:【美】Robert Love著,更多章节内容可以访问云栖社区“异步社区”公众号查看

1.4 Linux编程的概念

本节给出了Linux系统提供的服务的简要概述。所有的UNIX系统,包括Linux,提供了共同的抽象和接口集合。实际上,UNIX本身就是由这些共性定义的,比如对文件和进程的抽象、管道和socket的管理接口等等,都构成了UNIX系统的核心。

本概述假定你对Linux环境很熟悉:会使用shell的基础命令、能够编译简单的C程序。它不是关于Linux或其编程环境的,而是关于Linux系统编程的基础。

1.4.1 文件和文件系统
文件是Linux系统中最基础最重要的抽象。Linux遵循一切皆文件的理念(虽然没有某些其他系统如Plan 9那么严格)[2]。因此,很多交互操作是通过读写文件来完成,即使所涉及的对象看起来并非普通文件。

文件必须先打开才能访问。文件打开方式有只读、只写和读写模式。文件打开后是通过唯一描述符来引用,该描述符是从打开文件关联的元数据到文件本身的映射。在Linux内核中,文件用一个整数表示(C语言的int类型),称为文件描述符(file descriptor,简称fd)。文件描述符在用户空间共享,用户程序通过文件描述符可以直接访问文件。Linux系统编程的大部分工作都会涉及打开、操纵、关闭以及其他文件描述符操作。

普通文件
我们经常提及的“文件”即Linux中的普通文件(regular files)。普通文件包含以字节流(即线性数组)组织的数据。在Linux中,文件没有高级组织结构或格式。文件中包含的字节可以是任意值,可以以任意方式进行组织。在系统层,除了字节流,Linux对文件结构没有特定要求。有些操作系统,如VMS,提供高度结构化的文件,支持如records(记录)这样的概念,而Linux没有这么处理。

在Linux中,可以从文件中的任意字节开始读写。对文件的操作是从某个字节开始,即文件“地址”。该地址称为文件位置(file location)或文件偏移(file offset)。文件位置是内核中与每个打开的文件关联的元数据中很重要的一项。第一次打开文件时,其偏移为0。通常,随着按字节对文件的读写,文件偏移也随之增加。文件偏移还可以手工设置成给定值,该值甚至可以超出文件结尾。在文件结尾后面追加一个字节会使得中间字节都被填充为0。虽然支持通过这种在文件末尾追加字节的操作,但是不允许在文件的起始位置之前写入字节。这种操作看起来就很荒谬,实际上也并无用处。文件位置的起始值为0,不能是负数。在文件中间位置写入字节会覆盖该位置原来的数据。因此,在中间写入数据并不会导致原始数据向后偏移。绝大多数文件写操作都是发生在文件结尾。文件位置的最大值只取决于存储该值的C语言类型的大小,在现代Linux操作系统上,该值是64位。

文件的大小是通过字节来计算,称为文件长度。换句话说,文件长度即组成文件的线性数组的字节数。文件长度可以通过truncation(截断)操作进行改变。比起原始文件大小,文件被截断后的大小可以更小,这相当于删除文件末尾字节。容易让人困惑的是,从truncation操作的名称而言,文件被截断后的大小可以大于原始文件大小。在这种情况下,新增的字节(附加到文件末尾)是以“0”来填充。文件可以为空(即长度为0),不含任何可用字节。如同文件位置的最大值,文件长度的最大值只受限于Linux内核用于管理文件的C语言类型的大小。但是,不同的文件系统也可能规定自己的文件长度最大值,即为文件长度限制设置更小值。

同一个文件可以由多个进程或同一个进程多次打开。系统会为每个打开的文件实例提供唯一文件描述符。因此,进程可以共享文件描述符,支持多个进程使用同一个文件描述符。Linux内核没有限制文件的并发访问。不同的进程可以同时读写同一个文件。对文件并发访问的结果取决于这些操作的顺序,通常是不可预测的。用户空间的程序往往需要自己协调,确保对文件的同步访问是合理的。

文件虽然是通过文件名访问,但文件本身其实并没有直接和文件名关联。相反地,与文件关联的是索引节点inode(最初称为信息节点 ,是information node的缩写),inode是文件系统为该文件分配的唯一整数值(但是在整个系统中不一定是唯一的)。该整数值称为inode number,通常简称为i-number或ino。索引节点中会保存和文件相关的元数据,如文件修改时间戳、所有者、类型、长度以及文件数据的位置——但不含文件名!索引节点就是UNIX文件在磁盘上的实际物理对象,也是在Linux内核中通过数据结构表示的概念实体。

目录和链接
通过索引节点编号访问文件很繁琐(而且潜在安全漏洞),因此文件通常是通过文件名(而不是索引节点号)从用户空间打开。目录用于提供访问文件需要的名称。目录是可读名称到索引编号之间的映射。名称和索引节点之间的配对称为链接(link)。映射在物理磁盘上的形式,如简单的表或散列,是通过特定文件系统的内核代码来实现和管理的。从概念上看,可以把目录看作普通文件,其区别在于它包含文件名称到索引节点的映射。内核直接通过该映射把文件名解析为索引节点。

如果用户空间的应用请求打开指定文件,内核会打开包含该文件名的目录,搜索该文件。内核根据文件名获取索引节点编号。通过索引节点编号可以找到该节点。索引节点包含和文件关联的元数据,其中包括文件数据在磁盘上的存储位置。

刚开始,磁盘上只有一个目录,称为根目录,以路径/表示。然而,系统上通常有很多目录,内核怎么知道到哪个目录查找指定文件呢?

如前所述,目录和普通文件相似。实际上,它们有关联的索引节点。因此,目录内的链接可以指向其他目录的索引节点。这表示目录可以嵌套到其他目录中,形成目录层。这样,就可以支持使用UNIX用户都熟悉的路径名来查找文件,如/home/blackbeard/landscaping.txt。

当内核打开类似的路径名时,它会遍历路径中的每个目录项(directory entry,在内核中称为dentry),查找下一个入口项的索引节点。在前面的例子中,内核起始项是/,先获取home的索引节点,然后获取blackbeard的索引节点,最后获取concorde.png的索引节点。该操作称为目录解析或路径解析。Linux内核也采用缓存(称为dentry cache)储存目录的解析结果,基于时间局部性原理,可以为后续访问更快地提供查询结果。

从根目录开始的路径称为完整路径,也叫绝对路径。有些路径不是绝对路径,而是相对路径(如todo/plunder)。当提供相对路径时,内核会在当前工作目录下开始路径解析。内核在当前工作目录中查找todo目录。在这里,内核获取索引节点plunder。相对路径和当前工作目录的组合得到绝对路径。

虽然目录是作为普通文件存储的,但内核不支持像普通文件那样打开和操作目录。相反地,目录必须通过特殊的系统调用来操作。这些系统调用只支持两类操作:添加链接和删除链接。如果支持用户空间绕过内核操作目录,有可能出现一个简单的错误就会造成文件系统崩溃的巨大悲剧。

硬链接
从概念上看,以上介绍的内容都无法避免多个名字解析到同一个索引节点上。而事实上,多个名字确实可以解析到同一个索引节点。当不同名称的多个链接映射到同一个索引节点时,我们称该链接为硬链接(hard links)。

在复杂的文件系统结构中,硬链接支持多个路径指向同一份数据。硬链接可以在同一个目录下,也可以在不同的目录中。不管哪一种情况,内核都可以把路径名解析到正确的索引节点。举个例子,某个指向特定数据块的索引节点,其硬链接可以是/home/bluebeard/treasure.txt 和/home/blackbeard/to_steal.txt。

要从目录中删除文件,需要从目录结构中取消链接(unlink)该文件,这只需要从目录中删除该文件名和索引节点就可以。然而,由于Linux支持硬链接,文件系统不能对每个unlink操作执行删除索引节点及其关联数据的操作。否则,如果该索引节点在文件系统中还有其他的硬链接怎么办?为了确保在删除所有的链接之前不会删除文件,每个索引节点包含链接计数(link count),记录该索引节点在文件系统中的链接数。当unlink某个路径时,其链接计数会减1;只有当链接计数为0时,索引节点及其关联的数据才会从文件系统中真正删除。

符号链接
硬链接不能跨越多个文件系统,因为索引节点编号在自己的文件系统之外没有任何意义。为了跨越文件系统建立链接,UNIX系统实现了符号链接(symbolic links,简称symlinks)。

符号链接类似于普通文件,每个符号链接有自己的索引节点和数据块,包含要链接的文件的绝对路径。这意味着符号链接可以指向任何地方,包括不同的文件系统上的文件和路径,甚至指向不存在的文件和目录。指向不存在的文件的符号链接称为坏链接(broken link)。

比起硬链接,符号链接会带来更多的开销,因为有效解析符号链接需要解析两个文件:一是符号链接本身,二是该链接所指向的文件。硬链接不会带来这些额外开销——因为访问在文件系统中被多次链接的文件和单次链接的文件没有区别。虽然符号链接的开销很小,但还是被认为是个负面因素。

符号链接没有硬链接那么“透明”。使用硬链接是完全透明的——所需要做的仅仅是确定文件是否被多次链接!但是,操作符号链接需要特定的系统调用。由于符号链接的结构很简单,它通常是作为文件访问的快捷方式,而不是作为文件系统内部链接,因此这种缺乏透明性通常被认为是个正面因素。

特殊文件
特殊文件(special file)是指以文件来表示的内核对象。这些年来,UNIX系统支持了不少不同的特殊文件。Linux只支持四种特殊文件:块设备文件、字符设备文件、命名管道以及UNIX域套接字。特殊文件是使得某些抽象可以适用于文件系统,贯彻一切皆文件的理念。Linux提供了系统调用来创建特殊文件。

在UNIX系统中,访问设备是通过设备文件来实现,把设备当作文件系统中的普通文件。设备文件支持打开、读和写操作,允许用户空间程序访问和控制系统上的(物理和虚拟)设备。UNIX设备通常可以划分成两组:字符设备(character devices)和块设备(block device)。每种设备都有自己的特殊文件。

字符设备是作为线性字节队列来访问。设备驱动程序把字节按顺序写入队列,用户空间程序按照写入队列的顺序读取数据。键盘就是典型的字符设备。举个例子,当用户输入“peg”,应用程序将顺序从键盘设备中读取p、e和g。如果没有更多的字符读取时,设备会返回end-of-file(EOF)。漏读数据或以其他顺序读取都是不可能的。字符设备通过字符设备文件(character device file)进行访问。

和字符设备不同,块设备是作为字节数组来访问。设备驱动把字节映射到可寻址的设备上,用户空间可以按任意顺序随意访问数组中的任何字节——可能读取字节12,然后读取字节7,然后又读取字节12。块设备通常是存储设备。硬盘、软盘、CD-ROM驱动和闪存都是典型的块设备。这些块设备通过块设备文件(block device file)来访问。

命名管道(named pipes),通常称为FIFO(是“先进先出first in, first out”的简称),是以文件描述符作为通信信道的进程间通信(IPC)机制,它可以通过特殊文件来访问。普通管道是将一个程序的输出以“管道”形式作为另一个程序的输入,普通管道是通过系统调用在内存中创建的,并不存在于任何文件系统中。命名管道和普通管道一样,但是它是通过FIFO特殊文件来访问的。不相关的进程可以访问该文件并进行交互。

套接字(socket)是最后一种特殊文件。socket是进程间通信的高级形式,支持不同进程间的通信,这两个进程可以在同一台机器,也可以在不同机器。实际上,socket是网络和互联网编程的基础。socket演化出很多不同的变体,包括UNIX域套接字,它是本地机器进行交互的socket格式。虽然socket在互联网上的通信会使用主机名和端口号来标识通信目标,UNIX域套接字使用文件系统上的特殊文件进行交互,该文件称为socket文件。

文件系统和命名空间
如同所有的UNIX系统,Linux提供了全局统一的文件和目录命名空间。有些操作系统会把不同的磁盘和驱动划分成独立的命名空间——比如,通过路径A:plank.jpg可以访问软盘上的文件,虽然硬盘驱动安装在C:目录下。在UNIX,该软盘上的文件可以在其他介质上,通过路径/media/floppy/plank.jpg访问,甚至可以通过/home/captain/stuff/plank.jpg访问。也就是说,在UNIX系统中,命名空间是统一的。

文件系统是以合理有效的层次结构组织的文件和目录的集合。在文件和目录的全局命名空间中,可以分别添加和删除文件系统,这些操作称为挂载(mounting)和卸载(unmounting)。每个文件系统都需要挂载到命名空间的特定位置,该位置即挂载点(mount point)。在挂载点可以访问文件系统的根目录。举个例子,把CD挂载到/media/cdrom,CD上文件系统的根目录就可以通过/media/cdrom访问。第一个被挂载的文件系统是在命名空间的根目录/下,称为根文件系统(root filesystem)。Linux系统必定有个根文件系统,而其他文件系统的挂载点则是可选的。

通常而言,文件系统都是存在物理介质上(即保存在磁盘上),不过Linux还支持只保存在内存上的虚拟文件系统,以及存在于网络中的其他机器上的网络文件系统。物理文件系统保存在块存储设备中,如CD、软盘、闪存或硬盘中。在这些设备中,有些是可以分区的,表示可以切分成可独立操作的多个文件系统。Linux支持的文件系统类型很宽泛,囊括所有一般用户有可能遇到的——包括媒体文件系统(如ISO9660)、网络文件系统(NFS)、本地文件系统(ext4)、其他UNIX系统的文件系统(XFS)以及非UNIX系统的文件系统(FAT)。

块设备的最小寻址单元称为扇区(sector)。扇区是设备的物理属性。扇区大小一般是2的指数倍,通常是512字节。块设备无法访问比扇区更小的数据单元,所有的I/O操作都发生在一个或多个扇区上。

文件系统中的最小逻辑寻址单元是块(block)。块是文件系统的抽象,而不是物理介质的抽象。块大小一般是2的指数倍乘以扇区大小。在Linux,块通常比扇区大,但是必须小于页(page),页是内存的最小寻址单元(内存管理单元是个硬件)[3]。常见的块大小是512B、1KB和4KB。

从历史角度看,UNIX系统只有一个共享的命名空间,对系统上所有的用户和进程都可见。Linux独辟蹊径,支持进程间独立的命名空间,允许每个进程都可以持有系统文件和目录层次的唯一视图[4]。默认情况下,每个进程都继承父进程的命名空间,但是进程也可以选择创建自己的命名空间,包含通过自己的挂载点集和独立的根目录。

1.4.2 进程
如果说文件是UNIX系统最重要的抽象概念,进程则仅次于文件。进程是执行时的目标代码:活动的、正在运行的程序。但是进程不仅包含目标代码,它还包含数据、资源、状态和虚拟计算机。

进程的生命周期是从可执行目标代码开始,这些机器可运行的代码是以内核能够理解的形式存在,在Linux下,最常见的格式称为“可执行和可链接的格式(Executable and Linkable Format,ELF)”。可执行性格式包含元数据、多个代码段和数据段。代码段是线性目标代码块,可以加载到线性内存块中。数据段中的所有数据都一视同仁,有相同的权限,通常也用于相同的目的。

最重要和通用的段是文本段、数据段和bss段。文本段包含可执行代码和只读数据如常量,通常标记为只读和可执行。数据段包含初始化的数据,如包含给定值的C变量,通常标记为可读写。bss段包含未初始化的全局数据。因为C标准规定了C变量的默认值全部为0,因此没有必要在磁盘上把0保存到目标代码中。相反地,根据目标代码可以很容易地列举出bss段中未初始化的变量,内核在加载到内存时可以映射bss段中的全0页面(页面中全部都是0),bss段的设计完全是出于性能优化。bss这个取名存在历史遗留原因,是block started by symbol的简称。ELF可执行性程序的其他通用段都是绝对地址段(包含不可再定位的符号)和未定义地址段(包罗万象)。

进程还和系统资源关联,系统资源是由内核决定和管理的。一般来说,进程只通过系统调用请求和管理资源。资源包括计时器、挂起的信号量、打开的文件、网络连接、硬件和IPC机制。进程资源以及该进程相关的数据和统计保存在内核中该进程的进程描述符中。

进程是一种虚拟抽象。进程内核同时支持抢占式多任务和虚拟内存,为每个进程提供虚拟处理器和虚拟内存视图。从进程角度看,系统看起来好像完全由进程控制。也就是说,虽然某个进程可以和其他进程一起调度,该进程在运行时看起来似乎独立控制整个系统。系统内核会无缝、透明地抢占和重新调度进程,所有进程共享系统处理器,而进程感不到其中的区别。同样,每个进程都获得独立的线性地址空间,好像它独立控制整个系统内存。通过虚拟内存和分页,内核支持多个进程共享系统,每个进程的操作都运行在独立的地址空间中。内核通过现代处理器的硬件支持来管理这种虚拟化方式,支持操作系统并发管理多个独立的进程的状态。

线程
每个进程包含一个或多个执行线程(通常简称线程threads)。线程是进程内的活动单元,换句话说,线程是负责执行代码和管理进程运行状态的抽象。

绝大多数进程只包含一个线程,这些进程被称为单线程;包含多个线程的进程称为多线程。从传统上讲,由于UNIX系统一直很简洁,进程创建很快并拥有健壮的IPC机制,这些都减少了对线程的需求。因此,UNIX进程绝大部分是单线程的。

线程包括栈(正如非线程系统的进程栈一样,用于存储局部变量)、处理器状态、目标代码的当前位置(通常是保存在处理器的指令指针中)。进程的其他部分由所有线程共享,最主要是进程地址空间。在这种情况下,线程在维护虚拟进程抽象时,也共享虚拟内存抽象。

在Linux系统内部,Linux内核实现了独特的线程模型:它们其实是共享某些资源的普通进程。在用户空间,Linux依据POSIX 1003.1c实现线程模型(称为Pthreads)。目前Linux线程实现称为POSIX Threading Library(NPTL),它是glibc的一部分。我们将在第7章对线程进行更多的讨论。

进程层次结构
每个进程都由唯一的正整数标识,称为进程ID(pid)。第一个进程的pid是1,后续的每个进程都有一个新的、唯一的pid。

在Linux中,进程有严格的层次结构,即进程树。进程树的根是第一个进程,称为init进程,通常是init程序。新的进程是通过系统调用fork()创建的。fork()会创建调用进程的副本。原进程称为父进程,fork()创建的新进程称为子进程。除了第一个进程外,每个进程都有父进程。如果父进程先于子进程终止,内核会将init进程指定为它的父进程。

当进程终止时,并不会立即从系统中删除。相反地,内核将在内存中保存该进程的部分内容,允许父进程查询其状态,这被称为等待终止进程。一旦父进程确定某个子进程已经终止,该子进程就会完全被删除。如果一个进程已经终止,但是父进程不知道其状态,该进程称为僵尸进程(zombie)。init进程会等待所有的子进程结束,确保子进程永远不会处于僵死状态。

1.4.3 用户和组
Linux中通过用户和组进行权限认证,每个用户和一个唯一的正整数关联,该整数称为用户ID(uid)。相应地,每个进程和一个uid关联,用来识别运行这个进程的用户,称为进程的真实uid(real uid)。在Linux内核中,uid是用户的唯一标识。但是,用户一般通过用户名而不是id来表示。用户名及其对应的uid保存在/etc/passwd中,而系统库会把用户名映射到对应的uid上。

在登录过程中,用户向login程序提供用户名和密码。如果提供的用户名和密码都正确,login程序会根据/etc/passwd为用户生成login shell,并把用户id作为该shell进程的uid。子进程继承父进程的uid。

超级用户root的uid是0。root用户有特殊的权限,几乎可以执行所有的操作。举个例子,只有root用户可以修改进程的uid。因此,login进程是以root身份运行的。

除了真实uid以外,每个进程还包含有效的uid(effective uid),保留uid(saved uid)和文件系统uid(filesystem uid)。真实uid总是启动进程的用户uid,有效的uid在不同情况下会发生变化,从而支持进程切换成其他用户权限来执行。保留uid保存原来的有效uid,其值决定了用户将切换成哪个有效uid。文件系统uid通常和有效uid等效,用于检测文件系统的访问权限。

每个用户属于一个或多个组,包括在/etc/passwd中给出的基础组(primary group)或登录组(login group),也可能是/etc/group中给出的很多其他附加组(supplemental group)。因此,每个进程和相应的组ID(gid)关联,也包括真实gid、有效gid、保留gid、文件系统gid。进程通常是和用户的登录组关联,而不是和附加组关联。

一些安全机制只允许进程在满足特定标准时才执行某些操作。对于这一点,UNIX的安全机制非常简单:uid为0的进程可以访问,而其他进程不能访问。最近,Linux采用更通用的安全系统来取代UNIX这种安全机制。通过安全系统,不是做简单的二元判断,而是允许内核执行更细粒度的访问控制。

1.4.4 权限
Linux的标准文件权限和安全机制与UNIX的一致。

每个文件都有文件所有者、所属组以及三个权限位集合。每个权限位描述了所有者、所属组以及其他人对文件的读、写和执行的权限。这三类每个对应3个位,共9位。文件所有者和权限信息保存在文件的索引节点中。

对于普通文件,权限非常清晰:三位分别表示读、写和执行权限。特殊文件的读写权限和普通文件的一样,虽然特殊文件的读写内容由特殊文件自己确定。特殊文件忽略执行权限。对于目录,读权限表示允许列出目录的内容,写权限表示允许在目录中添加新的链接,执行权限表示允许在路径中输入和使用该目录。
除了UNIX权限外,Linux还支持访问控制表(ACL)。ACL支持更详细更精确的权限和安全控制方式,其代价是复杂度变大以及更大的磁盘存储开销。

1.4.5 信号
信号是一种单向异步通知机制。信号可能是从内核发送到进程,也可能是从进程到进程,或者进程发送给自己。信号一般用于通知进程发生了某些事件,如段错误或用户按下Ctrl-C。

Linux内核实现了约30种信号(准确数值和每个体系结构有关)。每个信号是由一个数值常量和文本名表示。举个例子,SIGHUP用于表示终端挂起,在x86-64体系结构上值为1。

信号会“干扰”正在执行的进程,不管当前进程正在做什么,都会立即执行预定义的操作。除了SIGKILL(进程中断)和SIGSTOP(进程停止),当进程接收到信号时,可以控制正在执行的操作。进程可以接受默认的信号处理操作,可能是中断进程、中断并coredump进程、停止进程或者什么都不做,具体的操作取决于信号值。此外,进程还可以选择显式忽略或处理信号。忽略的信号会被丢弃,不做处理。处理信号会执行用户提供的信号处理函数,程序接收到信号时会立即跳到处理函数执行。当信号处理函数返回时,程序控制逻辑将返回之前终端的指令处继续执行。由于信号的异步性,信号处理函数需要注意不要破坏之前的代码,只执行异步安全(async-safe,也称为信号安全)的函数。

1.4.6 进程间通信
允许进程交换信息并通知彼此所发生的事件是操作系统最重要的工作之一。Linux内核实现了绝大多数UNIX进程间通信(IPC)机制——包括System V和POSIX共同定义和标准化的机制——实现自定义的机制。

Linux支持的进程间通信机制包括管道、命名管道、信号量、消息队列、共享内存和快速用户空间互斥(futex)。

1.4.7 头文件
Linux系统编程离不开大量的头文件。内核本身和glibc都提供了用于系统编程的头文件。这些头文件包括标准C库(如)以及一些UNIX的贡献(如< unistd.h>)。

1.4.8 错误处理
毋庸置疑,检测错误和处理错误都是极其重要的。在系统编程中,错误是通过函数的返回值和特殊变量errno描述。glibc为库函数和系统调用提供透明errno支持。本书中给出的绝大多数接口都使用这种机制来报告错误。

函数通过特殊返回值(通常是-1,具体值取决于函数)通知调用函数发生了错误。错误值告诉调用函数发生了错误,但是并没有给出错误发生的原因。变量errno用于定位错误的原因。

变量errno在< errno.h>中定义如下:
screenshot

errno的值只有当errno设置函数显示错误后(通常返回-1)才生效,而在程序的后续执行过程中都可以修改其值。

可以直接读写errno变量,它是可修改的左值。errno的值和特定错误的文本描述一一对应。预处理器#define也和数值errno值一一对应。举个例子,预处理器定义EACCES等于1,表示“权限不足”。
C库提供了很多函数,可以把errno值转换成对应的文本。只有错误报告以及类似的操作时才需要。检测错误和处理错误可以直接通过预处理器定义和errno进行处理。

第一个这样的函数是perror():

screenshot

该函数向stderr(标准错误输出)打印以str指向的字符串为前缀,紧跟着一个冒号,然后是由errno表示的当前错误的字符串。为了使输出的错误信息有用,执行失败的函数名称应该包含在字符串中。例如:

screenshot

C库还提供了strerror()和strerror_r()函数,原型如下:
screenshot

前一个函数返回由errnum描述的错误的字符串指针。字符串可能不会被应用程序修改,但是会被后续的perror()和strerror()函数调用修改。因此,strerror函数不是线程安全的。

相反,strerror_r()函数是线程安全的。它向buf指向的长度为len的缓冲区中写入数据。strerror_r()函数在成功时返回1,失败时返回-1。有意思的是,这个函数在错误时也设置errno。

对于某些函数,在返回类型范围内返回的值都是合法的。在这些情况下,在调用前,errno必须设置成0,且调用后还会检查(这些函数保证在真正错误时返回非0的errno值)。例如:

screenshot

检查errno时常犯的一个错误是忘记任何库函数或系统调用都可能修改它。举个例子,以下代码是有bug的:

screenshot

在跨函数调用时,如果需要保留errno值,就需要保存该值:

screenshot

如本节前面所介绍的,在单线程程序中,errno是个全局变量。然而,在多线程程序中,每个线程都有自己的errno,因此它是线程安全的。

相关文章
|
29天前
|
算法 Linux C++
【Linux系统编程】解析获取和设置文件信息与权限的Linux系统调用
【Linux系统编程】解析获取和设置文件信息与权限的Linux系统调用
29 0
|
29天前
|
算法 Linux C++
【Linux系统编程】深入解析Linux中read函数的错误场景
【Linux系统编程】深入解析Linux中read函数的错误场景
204 0
|
29天前
|
Linux API C语言
【Linux系统编程】深入理解Linux 组ID和附属组ID的查询与设置
【Linux系统编程】深入理解Linux 组ID和附属组ID的查询与设置
34 0
【Linux系统编程】深入理解Linux 组ID和附属组ID的查询与设置
|
29天前
|
存储 算法 Linux
【Linux系统编程】深入理解Linux目录扫描函数:scandir目录函数(按条件扫描目录
【Linux系统编程】深入理解Linux目录扫描函数:scandir目录函数(按条件扫描目录
37 0
|
29天前
|
存储 算法 Linux
【Linux系统编程】Linux 文件系统探究:深入理解 struct dirent、DIR 和 struct stat结构
【Linux系统编程】Linux 文件系统探究:深入理解 struct dirent、DIR 和 struct stat结构
43 0
|
1天前
|
NoSQL Linux 程序员
【linux进程信号(一)】信号的概念以及产生信号的方式
【linux进程信号(一)】信号的概念以及产生信号的方式
|
1天前
|
存储 Linux Shell
【linux进程(一)】深入理解进程概念--什么是进程?PCB的底层是什么?
【linux进程(一)】深入理解进程概念--什么是进程?PCB的底层是什么?
|
2天前
|
Linux 编译器 C语言
Linux(3)Device Tree概念1(上)
Linux(3)Device Tree概念1
16 0
|
16天前
|
Linux 开发者
Linux文件编程(open read write close函数)
通过这些函数,开发者可以在Linux环境下进行文件的读取、写入和管理。 买CN2云服务器,免备案服务器,高防服务器,就选蓝易云。百度搜索:蓝易云
85 4
|
22天前
|
传感器 Linux API
嵌入式Linux串口编程简介
嵌入式Linux串口编程简介
19 1