第七八章学习笔记
第七八章学习笔记
知识点总结
第七章
- 文件操作级别:
文件操作级别 文件操作分为五个级别,按照从低到高的顺序排列如下。- (1)硬件级别∶硬件级别的文件操作包括∶
- fdisk∶将硬盘、U盘或SDC盘分区。
- mkfs∶格式化磁盘分区,为系统做好准备。
- fsck∶检查和维修系统。
- 碎片整理∶压缩文件系统中的文件。
其中大多数是针对系统的实用程序。普通用户可能永远都不需要它们,但是它们是创建和维护系统不可缺少的工具。- (2)操作系统内核中的文件系统函数
每个操作系统内核均可为基本文件操作提供支持。
下面列出了类 Unix 系统内核中的一些函数,其中前缀k表示内核函数。
- (2)操作系统内核中的文件系统函数
kumount(),kumount()
(mount/umount file systems)
kmkdir(),krmdir()
(make/remove directory)
kchair(),kgetCwd()
(change directory,get CWD pathname)
klink(),kunlink()
(hard link/unlink files)
kchmod(),kchown(),kutime() (change r|w|x permissions,owner,time)
kcreat(),kopen()
(create/open file for R,W,RW,APPEND)
kread(),kwrite() (read/write opened files)
klseek(),kclose()
(Lseek/close file descriptors)
keymlink(),kreadlink ()
(create/read symbolic 1ink files)
kstat(),kfstat(),klatat() (get file status/information)
kopendir(),kreaddir()
(open/read directories)
- (3)系统调用∶用户模式程序使用系统调用来访问内核函数。例如,下面的程序可读取文件的第二个1024字节。
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd, n;
char buf[1024];
if (fd = open(argv[1], O_RDONLY) <0)
exit(1);
int k = lseek(fd, 1024, SEEK_SET);
n = read(fd, buf, 1024);
close(fd);
printf("%d\n", n);
return 0;
}
其中,fd描述符和FILE*是有区别的
习惯上,标准输入(standard input)的文件描述符是 0,标准输出(standard output)是 1,标准错误(standard error)是 2。当打开一个新的文件,文件描述符将会是3。
文件描述符的分配规律:从当前未使用的最小的整数开始分配;
文件描述符的缺点:不能移植到UNIX以外的系统上去,也不直观。
简单归纳:fd只是一个整数,在open时产生,起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针file。
open:文件描述符的操作(如:open)返回的是一个文件描述符(int fd),内核会在每个进程空间中维护一个文件描述符表,所有打开的文件都将通过,此表中的文件描述符来引用。
fopen:流(如:fopen)返回的是一个文件指针(即指向FILE结构体的指针),FILE结构是包含有文件描述符的,fopen可以看做是open(fd直接操作的系统调用)的封装,它的优点是带有I/O缓存。
C语言的文件指针与文件描述符的相互转换可通过fdopen和fileno两个函数实现。它们都包含在头文件stdio.h中。
open()、read()、lseek()和 close()函数都是C语言库函数。每个库函数都会发出一个系统调用,使进程进入内核模式来执行相应的内核函数,例如open可进入kopen(),read可进入kread()函数,等等。
当进程结束执行内核函数时,会返回到用户模式,并得到所需的结果。在用户模式和内核模式之间切换需要大量的操作(和时间)。因此,内核和用户空间之间的数据传输成本昂贵。对于读/写文件,最好的方法是匹配内核的功能。内核会按数据块大小(从1KB到8KB)来读取/写入文件。(在Linux 中,硬盘的默认数据块大小是4KB,软盘的是1KB)
-
(4) I/O库函数:
用户通常需要读/写单独的字符、行或数据结构记录等。如果只有系统调用,用户模式程序则必须自己从缓冲区执行这些操作。C语言库提供了一系列标准的I/O函数,同时也提高了运行效率。 -
(5) 用户命令:
在Linux系统中,用户可以使用命令行工具或图形界面工具对文件进行操作。例如,常用的文件操作命令包括:cp
(复制文件)、mv
(移动文件)、rm
(删除文件)、mkdir
(创建目录)等。 -
(6) sh脚本:
sh脚本是一种用于自动化执行任务和操作文件的脚本语言。在脚本中,可以使用Linux系统中的各种文件操作命令、系统调用和I/O库函数。sh脚本通常用于批处理、自动化脚本等场景。
- 文件I/O操作:
文件I/O操作涉及用户模式和内核模式之间的交互。
-
用户模式(User Mode):
- 用户模式是计算机系统中运行普通应用程序的模式。
- 在用户模式下,应用程序只能访问有限的资源,并且无法直接访问操作系统的内核代码或执行敏感的系统级操作。
-
内核模式(Kernel Mode):
- 内核模式是计算机系统中运行操作系统内核的模式。
- 在内核模式下,操作系统具有更高的特权级别,可以访问系统的所有资源和设备,并执行各种系统级操作。
-
系统调用(System Call):
- 系统调用是一种用户模式程序与内核模式之间的接口,用于请求操作系统内核提供服务或执行特权操作。
- 通过系统调用,用户程序可以向内核发起文件I/O操作请求,例如打开、读取或写入文件。
补充:
-
文件描述符(File Descriptor):
- 文件描述符是用户模式程序在操作系统内核中表示打开文件的抽象标识。
- 文件描述符是一个非负整数,可以用作标识符来引用文件。
-
文件I/O操作流程:
- 用户模式程序通过系统调用(如open、read、write、close等)向内核提出文件I/O请求。
- 内核在接收到相应的系统调用后,验证请求的合法性,并执行相应的文件操作。
- 内核根据文件描述符进行文件定位、读取或写入数据,并处理错误检测和异常情况。
- 最后,内核将结果返回给用户模式程序。
-
缓冲区(Buffer):
- 缓冲区是在文件I/O操作中用于临时存储数据的内存区域。
- 用户模式程序可以使用缓冲区来批量读取或写入数据,减少对内核的频繁访问。
-
阻塞与非阻塞IO:
- 阻塞I/O操作会导致用户模式程序在执行文件操作时被阻塞,直到操作完成为止。
- 非阻塞I/O操作允许用户模式程序在执行文件操作时继续执行其他任务,而不必等待操作完成。这通常需要结合使用特殊的系统调用或异步I/O机制。
- 低级别文件操作:
- 文件描述符:Shell中,每个打开的文件都会关联一个文件描述符。其中,
0
表示标准输入,1
表示标准输出,2
表示标准错误输出。 - 文件打开与关闭:使用
exec
命令打开或关闭文件描述符,并将其与特定文件相关联。 - 文件锁定:使用
flock
命令对文件进行加锁,以防止多个进程同时访问和修改文件。
- 文件描述符:Shell中,每个打开的文件都会关联一个文件描述符。其中,
-
分区:
- 分区是将物理硬盘划分为逻辑上独立的部分的过程。
- 分区可以将硬盘划分成多个逻辑驱动器,每个驱动器可以独立使用,并拥有自己的文件系统。
- 在Linux中,可以使用工具如
fdisk
或parted
来进行分区。
-
格式化分区:
- 格式化是在分区上创建文件系统的过程。
- 格式化将分区标记为特定的文件系统类型,并在分区上创建文件系统的数据结构,以便在其上存储文件和目录。
- 在Linux中,可以使用工具如
mkfs
来格式化分区,并指定所需的文件系统类型,如ext4、NTFS等。
-
挂载分区:
- 挂载是将文件系统连接到指定的挂载点(目录)的过程,使得文件系统中的文件和目录可以在这个挂载点下访问。
- 在Linux中,可以使用
mount
命令将格式化后的分区挂载到指定的目录上。 - 挂载分区通常在系统启动时自动进行,可以在
/etc/fstab
文件中配置挂载选项。
第八章
使用系统调用进行文件操作
- 常用的系统调用
-
文件操作:
open
:打开文件。close
:关闭文件。read
:从文件中读取数据。write
:向文件中写入数据。lseek
:在文件中定位文件指针。
-
进程控制:
fork
:创建一个新进程。exec
:执行一个新程序。wait
:等待子进程退出。exit
:终止当前进程。
-
管道和IPC(进程间通信):
pipe
:创建一个管道。shmget
:获取共享内存段标识符。shmat
:将共享内存附加到进程地址空间。msgget
:获取消息队列标识符。msgsnd
:发送消息到消息队列。msgrcv
:从消息队列接收消息。
-
网络通信:
socket
:创建一个套接字。bind
:将套接字与特定IP地址和端口绑定。listen
:监听传入的连接请求。accept
:接受传入的连接请求。connect
:建立与远程主机的连接。send
:发送数据。recv
:接收数据。
-
线程操作:
pthread_create
:创建一个新线程。pthread_join
:等待指定的线程终止。pthread_mutex_lock
:获取互斥锁。pthread_mutex_unlock
:释放互斥锁。pthread_cond_wait
:条件变量等待。
- 链接文件
链接文件是指在文件系统中将一个文件名关联到另一个文件上的文件。常见的链接文件有硬链接文件和符号链接文件。
-
硬链接文件:
- 通过创建一个新的目录项(文件名)来指向已存在的文件,使得多个文件名指向同一个物理文件,即在文件系统中出现了两个或多个文件名指向的是同一个文件。
- 该文件系统中的硬链接都是相等的,没有源文件,只存在目录项与文件内容之间的映射关系,可以通过任何一个文件名访问该文件,并且每个硬链接都可以直接访问文件内容。
- 删除一个硬链接文件并不影响文件内容的可访问性,只有当所有的目录项都被删除,才会真正删除文件内容。
- 硬链接文件不能跨越文件系统边界,即不能将文件链接到另一个文件系统上的目录中。
-
符号链接文件:
- 符号链接文件也称为软链接文件,是一种特殊的文件,其中包含了另一个文件的路径名,通过这个路径名引用另一个文件。
- 符号链接文件在文件系统中是一个独立的文件,它指向另一个文件或目录,可以跨越不同文件系统,在文件系统卸载后仍然可以使用。
- 当符号链接文件所指向的源文件被删除时,该软链接就变成了垃圾文件(挂起的符号链接)。
stat
系统调用,用于获取文件的元数据信息(metadata),例如文件大小、创建时间、修改时间等。
函数原型:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
参数说明:
pathname
:要获取元数据信息的文件路径名。statbuf
:保存文件元数据信息的结构体指针。
返回值:
- 成功时返回0,失败时返回-1,并设置errno变量表示错误类型。
stat
函数的作用是读取指定文件的属性信息,并将结果存储在通过statbuf
参数传递的结构体中。该函数支持任何类型的文件,包括常规文件、目录文件、符号链接、管道和设备文件等。调用该函数需要有读取文件的权限。
stat
函数返回的结构体statbuf
中包含了大量的文件元数据信息,例如文件类型和权限、文件大小、设备ID和节点ID等。这些属性信息可以帮助应用程序判断文件的状态,对文件进行读写操作,或者在用户界面中显示文件信息。
open-close-open
系统调用是一种常见的技巧,用于避免竞争条件(race condition)的发生。
当多个进程同时对同一个文件进行读写操作时,就可能会发生竞争条件。例如,一个进程打开文件后,另一个进程删除了该文件,此时第一个进程还在使用该文件,就会导致错误的结果。为了避免这种情况,可以使用open-close-open
系统调用。
具体来说,open-close-open
系统调用的步骤如下:
- 使用
open
系统调用打开指定文件,得到文件描述符。 - 关闭文件描述符,使用
close
系统调用关闭文件。 - 再次使用
open
系统调用打开指定文件,得到新的文件描述符。
这样做的目的是在两次打开文件之间增加一个尽可能短的时间间隔,确保第二次打开时文件的状态不会受到其他进程的影响,避免竞争条件的发生。同时,由于第一次打开的文件描述符已经关闭,因此在第二次打开时会创建一个新的文件描述符,可以避免使用被删除文件的描述符。
需要注意的是,使用open-close-open
系统调用并不能完全解决竞争条件问题,只是一种减少风险的技巧。在实际使用中,还需要结合其他技术手段,例如文件锁机制、多进程间的同步和互斥等,来保证数据的一致性和完整性。
read()
系统调用,用于从文件描述符中读取数据。
函数原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数说明:
fd
:待读取数据的文件描述符。buf
:存放读取数据的缓冲区指针。count
:要读取的字节数。
返回值:
- 成功时返回实际读取到的字节数,如果已经到达文件末尾则返回0。
- 失败时返回-1,并设置
errno
变量表示错误类型。
read()
函数读取count
个字节的数据到用户空间缓冲区buf
中,并返回实际读取到的字节数。如果文件结束或者已经读取到了文件末尾,则返回0。 如果出现了错误,则返回-1,并设置errno
变量表示错误类型。
在读取一些特殊设备时(如终端或网络套接字),read()
函数通常是阻塞的,即调用进程会一直等待直到有数据可以读取为止。在读取普通文件时,read()
函数通常是非阻塞的,即无论是否有数据可读,都会立即返回。如果需要设置为阻塞模式,则可以使用fcntl()
函数的F_SETFL
命令将文件描述符的标志设置为阻塞。
write()
系统调用,用于将数据写入文件描述符。
函数原型:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数说明:
fd
:待写入数据的文件描述符。buf
:存放写入数据的缓冲区指针。count
:要写入的字节数。
返回值:
- 成功时返回实际写入的字节数。
- 失败时返回-1,并设置
errno
变量表示错误类型。
write()
系统调用将buf
中的count
个字节写入文件描述符fd
所指向的文件中。在网络编程中,可以使用write()
将数据写入套接字(socket)中,发送给远程主机。
如果所有数据都成功写入,则返回实际写入的字节数。否则,返回-1,并设置errno
变量表示错误类型。注意,返回值并不一定等于count
所指定的字节数,因为有可能出现信号中断、磁盘空间不足或设备故障等情况。
需要注意的是,write()
函数不保证写入所有数据,它只会尽量将数据写入内核缓冲区并返回实际写入的字节数。如果想要保证数据全部写入,则需要进行多次调用,直到所有数据都写完为止。另外,write()
在写操作时可能会被阻塞,直到有足够的空间可以写入为止。可以使用fcntl()
函数将文件描述符的标志设置为非阻塞模式,来避免阻塞等待。
- 简单的文件操作示例程序,包括打开、读取、写入和关闭文件等基本操作:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_LEN 256
int main() {
char file_name[MAX_LEN] = "test.txt";
char buffer[MAX_LEN];
char *data = "Hello, world!";
int fd;
ssize_t bytes_read, bytes_written;
// 打开文件
fd = open(file_name, O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 写入数据
bytes_written = write(fd, data, strlen(data));
if (bytes_written == -1) {
perror("write");
exit(EXIT_FAILURE);
}
printf("Write %zd bytes to file '%s'.\n", bytes_written, file_name);
// 重置文件指针位置
if (lseek(fd, 0, SEEK_SET) == -1) {
perror("lseek");
exit(EXIT_FAILURE);
}
// 读取数据
bytes_read = read(fd, buffer, MAX_LEN);
if (bytes_read == -1) {
perror("read");
exit(EXIT_FAILURE);
}
printf("Read %zd bytes from file '%s':\n%s\n", bytes_read, file_name, buffer);
// 关闭文件
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}
return 0;
}
这个程序首先使用open()
函数打开文件,其中使用了一组标志来指定打开方式和文件权限。然后使用write()
函数将数据写入文件中,再使用lseek()
函数将文件指针重置到文件开头,最后使用read()
函数读取文件中的数据,并输出到控制台上。最后使用close()
函数关闭文件
苏格拉底挑战
使用系统调用进行文件操作
低级别文件操作
问题与解决
GPT有时会出现“断片”现象,要及时补充足够多的条件