7 进程环境
7.8 存储空间分配 [20201201]
malloc
分配指定字节数的存储区。此存储区中的初始值不确定。
1 |
|
大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度、指向下一个分配块的指针等。这就意味着,如果超过一个已分配区的尾端或者在已分配区起始位置之前进行写操作,则会改写另一块的管理记录信息。这种类型的错误是灾难性的,但是因为这种错误不会很快就暴露出来,所以也就很难发现。
在动态分配的缓冲区前或后进行写操作,破坏的可能不仅仅是该区的管理记录信息。在动态分配的缓冲区前后的存储空间很可能用于其他动态分配的对象。这些对象与破坏它们的代码可能无关,这造成寻求信息破坏的源头更加困难。
其他可能产生的致命性的错误是:释放一个已经释放了的块;调用free时所用的指针不是3个alloc函数的返回值等。如若一个进程调用malloc函数,但却忘记调用free函数,那么该进程占用的存储空间就会连续增加,这被称为泄漏(leakage)。如果不调用free函数释放不再使用的空间,那么进程地址空间长度就会慢慢增加,直至不再有空闲空间。此时,由于过度的换页开销,会造成性能下降。
因为存储空间分配出错很难跟踪,所以某些系统提供了这些函数的另一种实现版本。每次调用这3个分配函数中的任意一个或free时,它们都进行附加的检错。在调用连接编辑器时指定一个专用库,在程序中就可使用这种版本的函数。此外还有公共可用的资源,在对其进行编译时使用一个特殊标志就会使附加的运行时检查生效。
3 文件I/O
3.2 文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。
按照惯例,UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联。这是各种 shell以及很多应用程序使用的惯例,与UNIX内核无关。尽管如此,如果不遵循这种惯例,很多UNIX系统应用程序就不能正常工作。
在符合POSIX.1的应用程序中,幻数0、1、2虽然已被标准化,但应当把它们替换成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO以提高可读性。这些常量都在头文件<unistd.h>中定义。
文件描述符的变化范围是0~OPEN_MAX-1。早期的UNIX系统实现采用的上限值是19(允许每个进程最多打开20个文件),但现在很多系统将其上限值增加至63。
3.3 函数open和openat [20201208]
open
打开或创建一个文件。
1 |
|
3.6 函数lseek [20201208]
lseek
显式地为一个打开文件设置偏移量。
1 |
|
- 可以确定打开文件的当前偏移量。
- 可以确定所涉及的文件是否可以设置偏移量。
通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。因为偏移量可能是负值,所以在比较 lseek 的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于−1。
因为偏移量(off_t)是带符号数据类型(见图2-21),所以文件的最大长度会减少一半。例如,若off_t是32位整型,则文件最大长度是$2^{31} -1$个字节。
lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。然后,该偏移量用于下一个读或写操作。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。
3.7 函数read [20201208]
read
从打开文件中读数据。
1 |
|
read 从文件中读 但是由文件标识符标识 读到进程地址空间 中间从内核地址空间转手
- 读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前有30个字节,而要求读100个字节,则read返回30。下一次再调用read时,它将返回0(文件尾端)。
- 当从终端设备读时,通常一次最多读一行。
- 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
- 当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
- 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
- 当一信号造成中断,而已经读了部分数据量时。读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。
3.10 文件共享 [20210112]
内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
- 每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
- 文件描述符标志(close_on_exec);
- 指向一个文件表项的指针。
- 内核为所有打开文件维持一张文件表。每个文件表项包含:
- 文件状态标志(读、写、添写、同步和非阻塞等);
- 当前文件偏移量;
- 指向该文件v节点表项的指针。每个打开文件(或设备)都有一个 v 节点(v-node)结构。v 节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。例如,i 节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。
创建v节点结构的目的是对在一个计算机系统上的多文件系统类型提供支持。
Linux没有将相关数据结构分为i节点和v节点,而是采用了一个与文件系统相关的i节点和一个与文件系统无关的i节点。
假定第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。
可能有多个文件描述符项指向同一文件表项。在3.12 节中讨论dup函数时,我们就能看到这一点。在fork后也发生同样的情况,此时父进程、子进程各自的每一个打开文件描述符共享同一个文件表项(见8.3节)。
dup
复制一个现有的文件描述符。
1 |
|
保存文件打开状态以便之后还可以恢复
14 高级I/O
14.8 存储映射I/O [20201215]
mmap
将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中取数据时,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区时,相应字节就自动写入文件。
1 |
|
映射文件的起始偏移量受系统虚拟存储页长度的限制,如果映射区的长度不是页长的整数倍时:假定文件长为 12 字节,系统页长为 512 字节,则系统通常提供 512字节的映射区,其中后500字节被设置为0。可以修改后面的这500字节,但任何变动都不会在文件中反映出来。于是,不能用mmap将数据添加到文件中,我们必须先加长该文件。
与mmap和memcpy相比,read和write执行了更多的系统调用,并做了更多的复制。read和write将数据从内核缓冲区中复制到应用缓冲区(read),然后再把数据从应用缓冲区复制到内核缓冲区(write)。而mmap和memcpy则直接把数据从映射到地址空间的一个内核缓冲区复制到另一个内核缓冲区。当引用尚不存在的内存页时,这样的复制过程就会作为处理页错误的结果而出现(每次错页读发生一次错误,每次错页写发生一次错误)。如果系统调用和额外的复制操作的开销和页错误的开销不同,那么这两种方法中就会有一种比另一种表现更好。
两个进程共享
- 在共享存储区中需要创建ID标识
- mmap不需要
方便做权限管理
- 文件的权限就是映射区的权限
调试程序便利
- 程序崩溃可检查
理解存储扩充
- 文件是对内存的扩充?×
- 内存对文件做了缓冲?√
- 现代操作系统需要吗?×
14.3 记录锁 [20201222]
记录锁(record locking)的功能是:当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。对于 UNIX 系统而言,“记录”这个词是一种误用,因为 UNIX 系统内核根本没有使用文件记录这种概念。一个更适合的术语可能是字节范围锁(byte-range locking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。
flock
对整个文件加锁,不能对文件中的一部分加锁。
4 文件和目录
4.3 文件类型 [20201229]
!!!Linux下有许多文件(文件类型):正规文件 目录文件 管道 符号连接 块设备 字符设备 Socket接口…
普通文件 regular file
- 最常用的文件类型;
- 包含了某种形式的数据;
- 至于这种数据是文本还是二进制数据,对于UNIX内核而言并无区别;
- 对普通文件内容的解释由处理该文件的应用程序进行。
一个值得注意的例外是二进制可执行文件。为了执行程序,内核必须理解其格式。所有二进制可执行文件都遵循一种标准化的格式,这种格式使内核能够确定程序文本和数据的加载位置。
目录文件 directory file
- 包含了其他文件的名字以及指向与这些文件有关信息的指针。
对于一个目录文件,具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件。
块特殊文件 block special file (块设备)
- 提供对设备带缓冲的访问;
- 每次访问长度固定。
字符特殊文件 character special file (字符设备)
- 提供对设备不带缓冲的访问;
- 每次访问长度可变。
命名管道 named pipe/FIFO
- 用于进程间通信。
套接字 socket
- 用于进程间的网络通信;
- 用于在一台宿主机上进程之间的非网络通信。
符号链接 symbolic link
- 指向另一个文件的文件。
系统中的所有设备要么是块特殊文件,要么是字符特殊文件。
4.15 函数link、linkat、unlink、unlinkat和remove [20201229]
link
创建一个指向现有文件的链接。
1 |
|
使用link重命名文件
unlink
删除一个现有的目录项。
1 |
|
删除目录项,并将由pathname所引用文件的链接计数减1。如果对该文件还有其他链接,则仍可通过其他链接访问该文件的数据。如果出错,则不对该文件做任何更改。
为了解除对文件的链接,必须对包含该目录项的目录具有写和执行权限。正如4.10节所述,如果对该目录设置了粘着位,则对该目录必须具有写权限,并且具备下面三个条件之一:
- 拥有该文件;
- 拥有该目录;
- 具有超级用户权限。
只有当链接计数达到0时,该文件的内容才可被删除。另一个条件也会阻止删除文件的内容——只要有进程打开了该文件,其内容也不能删除。关闭一个文件时,内核首先检查打开该文件的进程个数;如果这个计数达到0,内核再去检查其链接计数;如果计数也是0,那么就删除该文件的内容。
unlink的这种特性经常被程序用来确保即使是在程序崩溃时,它所创建的临时文件也不会遗留下来。进程用open或creat创建一个文件,然后立即调用unlink,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时(在这种情况下,内核关闭该进程所打开的全部文件),该文件的内容才被删除。
如果pathname是符号链接,那么unlink删除该符号链接,而不是删除由该链接所引用的文件。给出符号链接名的情况下,没有一个函数能删除由该链接所引用的文件。
如果文件系统支持的话,超级用户可以调用unlink,其参数pathname指定一个目录,但是通常应当使用rmdir函数,而不使用unlink这种方式。
remove
解除对一个文件或目录的链接。
对于文件,remove 的功能与unlink相同。对于目录,remove的功能与rmdir相同。
1 |
|
4.18 创建和读取符号链接 [20201229]
symlink
创建一个符号链接。
1 |
|
函数创建了一个指向actualpath的新目录项sympath。在创建此符号链接时,并不要求actualpath已经存在。并且,actualpath和sympath并不需要位于同一文件系统中。
创建特殊类型的文件
4.21 函数mkdir、mkdirat和rmdir
mkdir
创建一个新的空目录。
1 |
|
其中,.和..目录项是自动创建的。所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。常见的错误是指定与文件相同的mode(只指定读、写权限)。但是,对于目录通常至少要设置一个执行权限位,以允许访问该目录中的文件名。
rmdir
删除一个空目录。
1 |
|
如果调用此函数使目录的链接计数成为 0,并且也没有其他进程打开此目录,则释放由此目录占用的空间。如果在链接计数达到0时,有一个或多个进程打开此目录,则在此函数返回前删除最后一个链接及.和..项。另外,在此目录中不能再创建新文件。但是在最后一个进程关闭它之前并不释放此目录。
mknod
在早期版本中,进程要调用mknod函数创建一个新目录,但是只有超级用户进程才能使用mknod函数。
4.22 读目录
opendir [20201229]
1 |
|
fdopendir可以把打开文件描述符转换成目录处理函数需要的DIR结构。
readdir [20210105]
1 |
|
由opendir和fdopendir返回的指向DIR结构的指针由另外5个函数使用。opendir执行初始化操作,使第一个readdir返回目录中的第一个目录项。DIR结构由fdopendir创建时,readdir返回的第一项取决于传给fdopendir函数的文件描述符相关联的文件偏移量。注意,目录中各目录项的顺序与实现有关。它们通常并不按字母顺序排列。
0 其它
mount与umount [20201208]
多文件系统支持。
mount
挂载一个文件系统。
使用权限是超级用户或/etc/fstab中允许的使用者。
在Linux和Unix系统上,所有文件都是作为一个大型树(以/为根)的一部分访问的。要访问
CD-ROM上的文件,需要将CD-ROM设备挂装在文件树中的某个挂装点。如果发行版安装了自动
挂装包,那么这个步骤可自动进行。在Linux中,如果要使用硬盘、光驱等储存设备,就得
先将它加载,当储存设备挂上了之后,就可以把它当成一个目录来访问。挂上一个设备使用
mount命令。在使用mount这个指令时,至少要先知道下列三种信息:要加载对象的文件系统
类型、要加载对象的设备名称及要将设备加载到哪个目录下。
umount
卸载一个文件系统。
使用权限是超级用户或/etc/fstab中允许的使用者。
umount命令是mount命令的逆操作,它的参数和使用方法和mount命令是一样的。Linux挂装
CD-ROM后,会锁定CD—ROM,这样就不能用CD-ROM面板上的Eject按钮弹出它。但是,当不再
需要光盘时,如果已将/cdrom作为符号链接,请使用umount/cdrom来卸装它。仅当无用户正
在使用光盘时,该命令才会成功。该命令包括了将带有当前工作目录当作该光盘中的目录的
终端窗口。
proc文件系统
与其它常见的文件系统不同的是,/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。
基于/proc文件系统如上所述的特殊性,其内的文件也常被称作虚拟文件,并具有一些独特的特点。例如,其中有些文件虽然使用查看命令查看时会返回大量信息,但文件本身的大小却会显示为0字节。此外,这些特殊文件中大多数文件的时间及日期属性通常为当前系统时间和日期,这跟它们随时会被刷新(存储于RAM中)有关。
ptrace
ptrace 提供了一种机制使得父进程可以观察和控制子进程的执行过程,ptrace 还可以检查和修改子进程的可执行文件在内存中的image及子进程所使用的寄存器中的值。通常来说,主要用于实现对进程插入断点和跟踪子进程的系统调用。
文件名构成 路径名路径分割符
link unlink symlink 文件连接
mount umount 安装
目录操作
[20201229] 今天实验要求掌握的内容:
1.文件类型:正规文件,目录文件,块设备,字符设备,管道,符号连接,socket
2.文件名相关操作:link,unlink,symlink。如何删除文件,如何利用link给文件换名。
3.多文件系统支持:mount,umount
8 进程控制
8.3 函数fork [20210112]
fork
现有的进程创建一个新进程。
1 | #include <unistd.h> |
fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是 0,而父进程的返回值则是新建子进程的进程 ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程 ID。fork 使子进程得到返回值 0 的理由是:一个进程只会有一个父进程,所以子进程总是可以调用 getppid 以获得其父进程的进程 ID(进程ID 0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0)。
子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。
在重定向父进程的标准输出时,子进程的标准输出也被重定向。实际上,fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。我们说“复制”是因为对每个文件描述符来说,就好像执行了dup函数。父进程和子进程每个相同的打开描述符共享一个文件表项。
重要的一点是,父进程和子进程共享同一个文件偏移量。考虑下述情况:一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父进程和子进程都向标准输出进行写操作。如果父进程的标准输出已重定向(很可能是由 shell 实现的),那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量。在这个例子中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写到标准输出上,并且知道其输出会追加在子进程所写数据之后。如果父进程和子进程不共享同一文件偏移量,要实现这种形式的交互就要困难得多,可能需要父进程显式地动作。
如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步(如使父进程等待子进程),那么它们的输出就会相互混合(假定所用的描述符是在fork之前打开的)。
在fork之后处理文件描述符有以下两种常见的情况:
- 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新。
- 父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。
父进程和子进程之间的具体区别:
- fork的返回值不同;
- 进程ID不同;
- 父进程ID不同:子进程的父进程ID是创建它的进程的ID,父进程的父进程ID不变;
- 子进程的tms_utime、tms_stime、tms_cutime和tms_ustime的值设置为0;
- 子进程不继承父进程设置的文件锁;
- 子进程的未处理闹钟被清除;
- 子进程的未处理信号集设置为空集。
fork的两种用法:
- 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的—父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
- 一个进程要执行一个不同的程序。这对 shell 是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
上机程序
20201215
1 | //p1.c |
1 | //p2_read.c |
1 | //p2_write.c |
1 | //my_share.dat |
20201222
1 |
|
20210105
1 | void f(char *my_dir_name, int space_number) |