大型网站建设公司有哪些,网站开发设计框图,帮人做微信是哪个网站,百度推广需要自己做网站吗参考引用 UNIX 环境高级编程 (第3版)黑马程序员-Linux 系统编程 1. UNIX 基础知识
1.1 UNIX 体系结构#xff08;下图所示#xff09;
从严格意义上说#xff0c;可将操作系统定义为一种软件#xff0c;它控制计算机硬件资源#xff0c;提供程序运行环境#xff0c;通常… 参考引用 UNIX 环境高级编程 (第3版)黑马程序员-Linux 系统编程 1. UNIX 基础知识
1.1 UNIX 体系结构下图所示
从严格意义上说可将操作系统定义为一种软件它控制计算机硬件资源提供程序运行环境通常将这种软件称为内核 (kernel)因为它相对较小而且位于环境的核心 内核的接口被称为系统调用 (system call下图中的阴影部分)公用函数库构建在系统调用接口之上应用程序既可使用公用函数库也可使用系统调用shell 是一个特殊的应用程序为运行其他应用程序提供了一个接口 1.2 文件和目录
1.2.1 文件系统
UNIX 文件系统是目录和文件的一种层次结构所有东西的起点是称为根 (root) 的目录这个目录的名称是一个字符 “/”目录 (directory) 是一个包含目录项的文件。在逻辑上可认为每个目录项都包含一个文件名以及说明该文件属性的信息 文件属性是指文件类型 (是普通文件还是目录等) 、文件大小、文件所有者、文件权限 (其他用户能否访问该文件) 以及文件最后的修改时间等stat 和 fstat 函数返回包含所有文件属性的一个信息结构
1.2.2 文件名
目录中的各个名字称为文件名 (flename) 只有斜线 (/) 和空字符这两个字符不能出现在文件名中斜线用来分隔构成路径名的各文件名空字符则用来终止一个路径名 为了可移植性POSIX.1 推荐将文件名限制在以下字符集之内: 字母 (a~z、A~Z)、数字 (0~9)、句点 (.)、短横线 (-) 和下划线 (_)创建新目录时会自动创建了两个文件名. (称为点) 和 …(称为点点) 点指向当前目录点点指向父目录在最高层次的根目录中点点与点相同
1.2.3 路径名
由斜线分隔的一个或多个文件名组成的序列 (也可以斜线开头) 成路径名 (pathmamme) 以斜线开头的路径名称为绝对路径名否则称为相对路径名相对路径名指向相对于当前目录的文件文件系统根的名字 (/) 是一个特殊的绝对路径名它不包含文件名
1.2.4 工作目录
每个进程都有一个工作目录 (working directory)有时称其为当前工作目录 (curent working directory)所有相对路径名都从工作目录开始解释进程可以用 chdir 函数更改其工作目录相对路径名 doc/memo/joe 指的是当前工作目录中的 doc 目录中的 memo 目录中的文件 (或目录) joe 从该路径名可以看出doc 和 memo 都应当是目录但是却不能分辨 joe 是文件还是目录 路径名 /urs/lib/lint 是一个绝对路径名它指的是根目录中的 usr 目录中的 lib 目录中的文件 (或目录) lint
1.3 输入和输出
1.3.1 文件描述符
文件描述符 (file descriptor) 通常是一个小的非负整数内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时它都返回一个文件描述符
1.3.2 标准输入、标准输出和标准错误
每当运行一个新程序时所有的 shell 都为其打开 3 个文件描述符即标准输入、标准输出以及标准错误
1.3.3 不带缓冲的 I/O
函数 open、read、write、lseek 以及 close 提供了不带缓冲的 I/O这些函数都使用文件描述符
1.3.4 标准 I/O
标准 I/O 函数为那些不带缓冲的 I/O 函数提供了一个带缓冲的接口最熟悉的标准 I/O 函数是 printf
1.4 程序和进程
1.4.1 程序
程序 (program) 是一个存储在磁盘上某个目录中的可执行文件。内核使用 exec 函数将程序读入内存并执行程序
1.4.2 进程和进程 ID
程序的执行实例被称为进程 (process)某些操作系统用任务 (task) 表示正在被执行的程序UNIX 系统确保每个进程都有一个唯一的数字标识符称为进程 (process ID)。进程 ID 总是一个非负整数
1.4.3 进程控制
有 3 个用于进程控制的主要函数fork、exec 和 waitpidexec 函数有 7 种变体但经常把它们统称为 exec 函数
1.4.4 线程和线程 ID
通常一个进程 (process) 只有一个控制线程 (thread)某一时刻执行的一组机器指令。对于某些问题如果有多个控制线程分别作用于它的不同部分那么解决起来就容易得多。另外多个控制线程也可以充分利用多处理器系统的并行能力一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性。因为它们能访问同一存储区所以各线程在访问共享数据时需要采取同步措施以避免不一致性与进程相同线程也用 ID 标识。但是线程只在它所属的进程内起作用。一个进程中的线程 ID 在另一个进程中没有意义。当在一进程中对某个特定线程进行处理时可以使用该线程的 ID 引用它
1.5 出错处理
当 UNIX 系统函数出错时通常会返回一个负值而且整型变量 errno 通常被设置为具有特定信息的值。而有些函数对于出错则使用另一种约定而不是返回负值。例如大多数返回指向对象指针的函数在出错时会返回一个 null 指针POSIX.1 和 ISO C 将 errno 定义为一个符号它扩展成为一个可修改的整形左值 它可以是一个包含出错编号的整数也可以是一个返回出错编号指针的函数 在支持线程的环境中多个线程共享进程地址空间每个线程都有属于它自己的局部 errno 以避免一个线程干扰另一个线程对于 errno 应当注意两条规则 第一如果没有出错其值不会被例程清除。因此仅当函数的返回值指明出错时才检验其值第二任何函数都不会将 errno 值设置为 0而且在 errno.h 中定义的所有常量都不为 0
1.6 用户标识
1.6.1 用户 ID
口令文件登录项中的用户 ID (user ID) 是一个数值它向系统标识各个不同的用户。系统管理员在确定一个用户的登录名的同时确定其用户 ID。用户不能更改其用户 ID通常每个用户有一个唯一的用户 ID用户 ID 为 0 的用户为根用户 (root) 或超级用户 (superuser)。在口令文件中通常有一个登录项其登录名为 root称这种用户的特权为超级用户特权。某些操作系统功能只向超级用户提供超级用户对系统有自由的支配权
1.6.2 组 ID
口令文件登录项也包括用户的组 ID (group ID)它是一个数值。组 ID 也是由系统管理员在指定用户登录名时分配的。一般来说在口令文件中有多个登录项具有相同的组 ID。组被用于将若干用户集合到项目或部门中去。这种机制允许同组的各个成员之间共享资源组文件将组名映射为数值的组 ID组文件通常是 /etc/group 对于磁盘上的每个文件文件系统都存储该文件所有者的用户 ID 和组 ID。存储这两个值只需 4 个字节 (假定每个都以双字节的整型值存放)。在检验权限期间比较字符串较之比较整型数更消耗时间但是对于用户而言使用名字比使用数值方便所以口令文件包含了登录名和用户 ID 之间的映射关系而组文件则包含了组名和组 D 之间的映射关系 1.7 信号 信号 (signal) 用于通知进程发生了某种情况。例如若某一进程执行除法操作其除数为 0则将名为 SIGEPE (浮点异常) 的信号发送给该进程。进程有以下 3 种处理信号的方式 (1) 忽略信号。有些信号表示硬件异常例如除以 0 或访问进程地址空间以外的存储单元等因为这些异常产生的后果不确定所以不推荐使用这种处理方式(2) 按系统默认方式处理。对于除数为 0系统默认方式是终止该进程(3) 提供一个函数信号发生时调用该函数这被称为捕捉该信号。通过提供自编的函数就能知道什么时候产生了信号并按期望的方式处理它 很多情况都会产生信号。终端键盘上有两种产生信号的方法 中断键 (通常是 Delete 键或 CrlC) 和退出键 (通常是 Ctrl\)它们被用于中断当前运行的进程调用 kill 函数。在一个进程中调用此函数就可向另一个进程发送一个信号。当然这样做也有些限制当向一个进程发送信号时必须是那个进程的所有者或者是超级用户
1.8 时间值
UNIX 系统使用过两种不同的时间值 (1) 日历时间。该值是自协调世界时 (Coordinated Universal TimeUTC) 1970 年 1 月 1 日 00:00:00 这个特定时间以来所经过的秒数累计值 (早期的手册称UTC 为格林尼治标准时间)。这些时间值可用于记录文件最近一次的修改时间等 系统基本数据类型 time_t 用于保存这种时间值 (2) 进程时间。也被称为 CPU 时间用以度量进程使用的中央处理器资源。进程时间以时钟滴答计算。每秒钟曾经取为 50、60 或 100 个时钟滴答 系统基本数据类型 clock_t 保存这种时间值 当度量一个进程的执行时间时UNIX 系统为一个进程维护了 3 个进程时间值 时钟时间 时钟时间又称为墙上时钟时间 (wall clock time)它是进程运行的时间总量其值与系统中同时运行的进程数有关 用户 CPU 时间 用户 CPU 时间是执行用户指今所用的时间量 系统 CPU 时间 系统 CPU 时间是为该进程执行内核程序所经历的时间用户 CPU 时间和系统 CPU 时间之和常被称为 CPU 时间
1.9 系统调用和库函数 什么是系统调用 由操作系统实现并提供给外部应用程序的编程接口 (Application Programming InterfaceAPI)是应用程序同系统之间数据交互的桥梁所有的操作系统都提供多种服务的入口点由此程序向内核请求服务。各种版本的 UNIX 实现都提供良好定义、数量有限、直接进入内核的入口点这些入口点被称为系统调用 (system call) 通用库函数可能会调用一个或多个内核的系统调用但是它们并不是内核的入口点 例如printf 函数会调用 write 系统调用以输出一个字符串但函数 strcpy (复制一个字符串) 和 atoi (将 ASCII 转换为整数) 并不使用任何内核的系统调用 系统调用和库函数都以 C 函数的形式出现两者都为应用程序提供服务 可以替换库函数但系统调用通常是不能被替换的系统调用通常提供一种最小接口而库函数通常提供比较复杂的功能 C 标准库函数和系统函数/调用关系一个 “hello” 如何打印到屏幕的案例 其中系统调用相当于对系统函数man page 中的函数进行了一个浅封装 2. UNIX 标准及实现
2.1 UNIX 标准化
2.1.1 IOS C
ISO C 标准现在由 ISO/TEC 的 C 程序设计语言国际标准工作组维护和开发该工作组称为 ISO/IEC JTC1/SC22/WG14简称 WG14。ISO C 标准的意图是提供 C 程序的可移植性使其能适合于大量不同的操作系统而不只是适合 UNIX 系统ISO C 标准定义的头文件 2.1.2 IEEE POSIX.1
POSIX.1 是一个最初由 IEEE(Institute of Electricaland Electronics Engineers电气和电子工程师学会) 制订的标准族。POSIX.1 指的是可移植操作系统接口 (Portable Operating System Interface)。它原来指的只是 IEEE 标准 1003.1-1988 (操作系统接口)后来则扩展成包括很多记为 1003 的标准及标准草案如 shell 和实用程序 (1003.2本教程使用 1003.1) 由于 1003.1 标准说明了一个接口而不是一种实现所以并不区分系统调用和库函数所有在标准中的例程都被称为函数 POSIX.1 标准定义的必需的头文件 2.2 UNIX 系统实现
2.2.1 4.4 BSD
BSD (Berkeley Sofware Distibution) 是由加州大学伯克利分校的计算机系统研究组研究开发和分发的4.2BSD 于 1983 年问世4.3BSD 则于 1986 年发布4.4BSD 于 1994 年发布
2.2.2 FreeBSD
FreeBSD 基于 4.4BSD-Lite 操作系统。在加州大学伯克分校的计算机系统研究组决定终止其在 UNIX 操作系统的 BSD 版本的研发工作而且 386BSD 项目被忽视很长时间之后为了继续坚持 BSD 系列形成了 FreeBSD 项目
2.2.3 Linux
Linux 是由 Linus Torvalds 在 1991 年为替代 MNIX 而研发的Linux 是一种提供类似于UNIX 的丰富编程环境的操作系统在 GNU 公用许可证的指导下 Linux 是免费使用的
2.2.4 Mac OS X
与其以前的版本相比Mac OS X 使用了完全不同的技术。其核心操作系统称为 “Darwin”,基于 Mach 内核、FreeBSD 操作系统以及具有面向对象框架的驱动和其他内核扩展的结合
2.2.5 Solaris
Solaris 是由 Sun Microsystems (现为 Oracle) 开发的 UNIX 系统版本
2.3 基本系统数据类型 头文件 sys/types.h 中定义了某些与实现有关的数据类型它们被称为基本系统数据类型 一些常用的基本系统数据类型 3. 文件 I/O
3.1 引言
可用的文件 I/O 函数打开open文件、读read文件、写write文件等UNIX 系统中的大多数文件 I/O 只需用到 5 个函数open、read、write、lseek 以及close 本章描述的函数经常被称为不带缓冲的 I/O (unbuffered I/O与标准 I/O 函数相对照) 不带缓冲指的是每个 read 和 write 都调用内核中的一个系统调用这些不带缓冲的 I/O 函数不是 ISO C 的组成部分但它们是 POSIX1 的组成部分 3.2 文件描述符 对内核而言所有打开的文件都通过文件描述符引用 文件描述符是一个非负整数当打开一个现有文件或创建一个新文件时内核向进程返回一个文件描述符当读、写一个文件时使用 open 或 creat 返回的文件描述符标识该文件将其作为参数传送给 read 或 write 按照惯例UNIX 系统 shell 把 文件描述符 0 与进程的标准输入关联文件描述符 1 与进程的标准输出关联文件描述符 2 与进程的标准错误关联 在符合 POSIX.1 的应用程序中幻数 0、1、2 虽然已被标准化但应当把它们替换成符号常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 以提高可读性。这些常量都在头文件 unistd.h 中定义 文件描述符是指向一个文件结构体的指针 PCB 进程控制块本质是结构体成员是文件描述符表 3.3 函数 open 和 openat打开或创建一个文件
3.3.1 函数 open 和 openat 参数解析
#include sys/types.h
#include sys/stat.h
#include fcntl.h // 定义 flags 参数int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode); // 仅当创建新文件时才使用第三个参数表明文件权限int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);pathname要打开或创建文件的路径名flags用来说明此函数的多个选项用以下一个或多个常量进行 “或” 运算构成 flags 参数 O_RDONLY只读打开、O_WRONLY只写打开、O_RDWR读、写打开、O_EXEC只执行打开、O_SEARCH只搜索打开用于目录O_APPEND每次写时都追加到文件末尾O_CREAT若此文件不存在则创建它与第三个参数 mode 同时使用O_EXCL如果同时指定了 O_CREAT而文件已经存在则出错O_NONBLOCK为文件的本次打开操作和后续的 I/O 操作设置非阻塞方式O_TRUNC如果此文件存在而且为只写或读-写成功打开则将其长度截断为 0 函数返回值 若成功返回文件描述符若出错返回 -1 dirfd 参数把 open 和 openat 函数区分开共有 3 种可能性 path 参数指定的是绝对路径名在这种情况下dirfd 参数被忽略openat 函数就相当于 open 函数path 参数指定的是相对路径名dirfd 参数指出了相对路径名在文件系统中的开始地址dirfd 参数是通过打开相对路径名所在的目录来获取path 参数指定了相对路径名dirfd 参数具有特殊值 AT_FDCWD。在这种情况下路径名在当前工作目录中获取openat 函数在操作上与 open 函数类似 openat 函数是 POSIX.1 最新版本中新增的一类函数之一希望解决两个问题 第一让线程可以使用相对路径名打开目录中的文件而不再只能打开当前工作目录 同一进程中的所有线程共享相同的当前工作目录因此很难让同一进程的多个不同线程在同一时间工作在不同的目录中 第二可以避免 time-of-check-to-time-of-use (TOCTTOU) 错误 TOCTTOU 错误的基本思想是如果有两个基于文件的函数调用其中第二个调用依赖于第一个调用的结果那么程序是脆弱的。因为两个调用并不是原子操作在两个函数调用之间文件可能改变了这样也就造成了第一个调用的结果就不再有效使得程序最终的结果是错误的
3.3.2 文件名和路径名截断
在 POSIX.1 中常量 _POSIX_NO_TRUNC 决定是要截断过长的文件名或路径名还是返回一个出错。根据文件系统的类型此值可以变化。可以用 fpathconf 或 pathconf 来查询目录具体支持何种行为到底是截断过长的文件名还是返回出错若 _POSIX_NO_TRUNC 有效则在整个路径名超过 PATH_MAX或路径名中的任一文件名超过 NAME_MAX 时出错返回并将 errno 设置为 ENAMETOOLONG
3.4 函数 close关闭一个打开文件
#include unistd.hint close(int fd);函数返回值 若成功返回 0若出错返回 -1 关闭一个文件时还会释放该进程加在该文件上的所有记录锁 当一个进程终止时内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显式地用 close 关闭打开文件
3.5 函数 creat创建一个新文件
#include sys/types.h
#include sys/stat.h
#include fcntl.hint creat(const char *pathname, mode_t mode);函数返回值 若成功返回为只写打开的文件描述符若出错返回 -1 此函数等效于 open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)creat 的一个不足之处是它以只写方式打开所创建的文件。在提供 open 的新版本之前如果要创建一个临时文件并要先写该文件然后又读该文件则必须先调用 creat、close然后再调用 open。现在则可用上述方式调用 open 实现 3.3-3.5 案例
案例 1
// open.c
#include unistd.h
#include fcntl.h
#include stdio.hint main(int argc, char *argv[]) {int fd;fd open(./AUTHORS.txt, O_RDONLY);printf(fd %d\n, fd);close(fd); return 0;
}$ gcc open.c -o open$ ./open
# 输出如下表示文件存在并正确打开
fd 3案例 2
// open2.c
#include unistd.h
#include fcntl.h
#include stdio.hint main(int argc, char *argv[]) {int fd;fd open(./AUTHORS.cp, O_RDONLY | O_CREAT, 0644); // rw-r--r--printf(fd %d\n, fd);close(fd);return 0;
}$ gcc open2.c -o open2$ ./open2
fd 3$ ll
# 创建了一个新文件 AUTHORS.cp且文件权限对应于 0644
-rw-r--r-- 1 yue yue 0 9月 10 22:19 AUTHORS.cp案例 3
// open3.c
#include unistd.h
#include fcntl.h
#include stdio.hint main(int argc, char *argv[]) {int fd;// 如果文件存在以只读方式打开并且截断为 0// 如果文件不存在则把这个文件创建出来并指定权限为 0644fd open(./AUTHORS.cp, O_RDONLY | O_CREAT | O_TRUNC, 0644); // rw-r--r--printf(fd %d\n, fd);close(fd);return 0;
}$ gcc open3.c -o open3$ ./open3
# 输出如下表示文件存在并正确打开
fd 3$ ll
# 首先在 AUTHORS.cp 文件中输入内容然后经过 O_TRUNC 截断后为 0
-rw-r--r-- 1 yue yue 0 9月 10 22:19 AUTHORS.cp案例 4
创建文件时指定文件访问权限 mode权限同时受 umask 影响。结论为 文件权限 mode ~umask
$ umask
0002 # 表明默认创建文件权限为 ~umask 775第一个 0 表示八进制// open4.c
#include unistd.h
#include fcntl.h
#include stdio.hint main(int argc, char *argv[]) {int fd;fd open(./AUTHORS.cp2, O_RDONLY | O_CREAT | O_TRUNC, 0777); // rwxrwxrwxprintf(fd %d\n, fd);close(fd);return 0;
}$ gcc open4.c -o open4$ ./open4
fd 3$ ll
# 创建了一个新文件 AUTHORS.cp2且文件权限为 mode ~umask 775rwxrwxr-x
-rwxrwxr-x 1 yue yue 0 9月 10 22:38 AUTHORS.cp2*案例 5
open 函数常见错误 打开文件不存在 // open5.c
#include unistd.h
#include fcntl.h
#include stdio.h
#include errno.h
#include string.hint main(int argc, char *argv[]) {int fd;fd open(./AUTHORS.cp4, O_RDONLY);printf(fd %d, errno %d : %s\n, fd, errno, strerror(errno));close(fd);return 0;
}$ gcc open5.c -o open5$ ./open5
fd -1, errno 2 : No such file or directory以写方式打开只读文件打开文件没有对应权限 // open6.c
#include unistd.h
#include fcntl.h
#include stdio.h
#include errno.h
#include string.hint main(int argc, char *argv[]) {int fd;fd open(./AUTHORS.cp3, O_WRONLY); // AUTHORS.cp3 文件权限为只读printf(fd %d, errno %d : %s\n, fd, errno, strerror(errno));close(fd);return 0;
}$ gcc open6.c -o open6$ ./open6
fd -1, errno 13 : Permission denied以只写方式打开目录 $ mkdir mydir # 首先创建一个目录// open7.c
#include unistd.h
#include fcntl.h
#include stdio.h
#include errno.h
#include string.hint main(int argc, char *argv[]) {int fd;fd open(mydir, O_WRONLY);printf(fd %d, errno %d : %s\n, fd, errno, strerror(errno));close(fd);return 0;
}$ gcc open7.c -o open7$ ./open7
fd -1, errno 21 : Is a directory3.6 函数 lseek显式的为一个打开文件设置偏移量
#include sys/types.h
#include unistd.hoff_t lseek(int fd, off_t offset, int whence);每个打开文件都有一个与其相关联的 “当前文件偏移量”通常是一个非负数用以度量从文件开始处计算的字节数 lseek 中的 l 表示长整型 函数返回值 若成功返回新的文件偏移量若出错返回 -1 按系统默认的情况当打开一个文件时除非指定 O_APPEND 选项否则该偏移量被设置为 0 对参数 offset 的解释与参数 whence 的值有关 若 whence 是 SEEK_SET则将该文件的偏移量设置为距文件开始处 offset 个字节 SEEK_SET0 绝对偏移量 若 whence 是 SEEK_CUR则将该文件的偏移量设置为其当前值加 offsetoffset 可正可负 SEEK_CUR1 相对于当前位置的偏移量 若 whence 是 SEEK_END则将该文件的偏移量设置为文件长度加 offsetoffset 可正可负 SEEK_END2 相对文件尾端的偏移量 lseek 仅将当前的文件偏移量记录在内核中它并不引起任何 I/O 操作。然后该偏移量用于下一个读或写操作 文件偏移量可以大于文件的当前长度在这种情况下对该文件的下一次写将加长该文件并在文件中构成一个空洞这一点是允许的。位于文件中但没有写过的字节都被读为 0
案例 1
文件的读和写使用同一偏移位置#include stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include fcntl.hint main(void) {int fd, n;char msg[] Its a test for lseek\n;char ch;fd open(lseek.txt, O_RDWR | O_CREAT, 0644);if (fd 0) {perror(open lseek.txt error);exit(1);}// 使用 fd 对打开的文件进行写操作读写位置位于文件结尾处write(fd, msg, strlen(msg));// 若注释下行代码由于文件写完之后未关闭读、写指针在文件末尾所以不调节指针直接读取不到内容lseek(fd, 0, SEEK_SET); // 修改文件读写指针位置位于文件开头while ((n read(fd, ch, 1))) {if (n 0) {perror(read error);exit(1);} write(STDOUT_FILENO, ch, n); // 将文件内容按字节读出写出到屏幕}close(fd);return 0;
}案例 2
使用 lseek 获取文件大小// lseek_size.c
#include stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include fcntl.hint main(int argc, char *argv[]) {int fd open(argv[1], O_RDWR);if (fd -1) {perror(open error);exit(1);}int length lseek(fd, 0, SEEK_END);printf(file size: %d\n, length);close(fd);return 0;
}$ gcc lseek_size.c -o lseek_size
$ ./lseek_size fcntl.c # fcntl.c 文件大小为 678
678案例 3
使用 lseek 扩展文件大小 要想使文件大小真正扩展必须引起 IO 操作 // 修改案例 2 中下行代码扩展 111 大小
// 这样并不能真正扩展使用 cat 命令查看文件大小未变化
int length lseek(fd, 111, SEEK_END);// 在 printf 函数下行写如下代码引起 IO 操作
write(fd, \0, 1); // 结果便是在扩展的文件尾部追加文件空洞可使用 truncate 函数直接扩展文件#include stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include fcntl.hint main(int argc, char*argv[]) {int ret truncate(dict.cp, 250);printf(ret %d\n, ret);return 0;
}lseek 读取的文件大小总是相对文件头部而言。用 lseek 读取文件大小实际用的是读写指针初、末位置的偏移差一个新开文件读、写指针初位置都在文件开头。如果用这个来扩展文件大小必须引起 IO 才行于是就至少要写入一个字符 3.7 函数 read从打开文件中读数据
#include unistd.h// ssize_t 表示带符号整型void* 表示通用指针
// 参数1文件描述符参数2存数据的缓冲区参数3缓冲区大小
ssize_t read(int fd, void *buf, size_t count);函数返回值 若 read 成功则返回读到的字节数若已到文件尾返回 0若出错返回 -1若返回 -1并且 errno EAGIN 或 EWOULDBLOCK说明不是 read 失败而是 read 在以非阻塞方式读一个设备文件/网络文件并且文件无数据 有多种情况可使实际读到的字节数少于要求读的字节数 1、读普通文件时在读到要求字节数之前已到达了文件尾端 例如若在到达文件尾端之前有 30 个字节而要求读 100 个字节则 read 返 30。下一次再调用 read 时它将返回 0 (文件尾端) 2、当从终端设备读时通常一次最多读一行3、当从网络读时网络中的缓冲机制可能造成返回值小于所要求读的字节数4、当从管道或 FIFO 读时如若管道包含的字节少于所需的数量那么 read 将只返回实际可用的字节数5、当从某些面向记录的设备 (如磁带) 读时一次最多返回一个记录6、当一信号造成中断而已经读了部分数据量时
3.8 函数 write向打开文件写数据
#include unistd.h// 参数1文件描述符参数2待写出数据的缓冲区参数3数据大小
ssize_t write(int fd, const void *buf, size_t count);函数返回值 若 write 成功则返回已写的字节数返回值通常与参数 count 值相同否则表示出错若出错返回 -1 write 出错的一个常见原因是磁盘已写满或者超过了一个给定进程的文件长度限制 对于普通文件写操作从文件的当前偏移量处开始。如果在打开该文件时指定了 O_APPEND 选项则在每次写操作之前将文件偏移量设置在文件的当前结尾处。在一次成功写之后该文件偏移量增加实际写的字节数
阻塞和非阻塞 阻塞 (Block)当进程调用一个阻塞的系统函数时该进程被置于睡眠 (Sleep) 状态这时内核调度其它进程运行直到该进程等待的事件发生了 (比如网络上接收到数据包或者调用 sleep 指定的睡眠时间到了) 它才有可能继续运行。与睡眠状态相对的是运行 (Running) 状态在 Linux 内核中处于运行状态的进程分为两种情况 正在被调度执行。CPU 处于该进程的上下文环境中程序计数器里保存着该进程的指令地址通用寄存器里保存着该进程运算过程的中间结果正在执行该进程的指令正在读写该进程的地址空间就绪状态。该进程不需要等待什么事件发生随时都可以执行但 CPU 暂时还在执行另一个进程所以该进程在一个就绪队列中等待被内核调度 读常规文件是不会阻塞的不管读多少字节read 一定会在有限的时间内返回。从终端设备或网络读则不一定如果从终端输入的数据没有换行符调用 read 读终端设备就会阻塞如果网络上没有接收到数据包调用 read 从网络读就会阻塞至于会阻塞多长时间也是不确定的如果一直没有数据到达就一直阻塞在那里。同样写常规文件是不会阻塞的而向终端设备或网络写则不一定 /dev/tty – 终端文件
阻塞读终端
// block_readtty.c
#include unistd.h
#include stdlib.h
#include stdio.hint main(void) {char buf[10];int n;n read(STDIN_FILENO, buf, 10);if (n 0){perror(read STDIN_FILENO);exit(1);}write(STDOUT_FILENO, buf, n);return 0;
}$ gcc block_readtty.c -o block
$ ./block # 此时程序在阻塞等待输入下面输入 hello 后回车即结束
hello
hello非阻塞读终端
// nonblock_readtty.c
#include unistd.h
#include fcntl.h
#include stdlib.h
#include stdio.h
#include errno.h
#include string.h#define MSG_TRY try again\n
#define MSG_TIMEOUT time out\nint main(void) {char buf[10];int fd, n, i;// 设置 /dev/tty 非阻塞状态默认为阻塞状态fd open(/dev/tty, O_RDONLY | O_NONBLOCK); if(fd 0) {perror(open /dev/tty);exit(1);}printf(open /dev/tty ok... %d\n, fd);for (i 0; i 5; i) {n read(fd, buf, 10);if (n 0) { // 说明读到了东西break;}if (errno ! EAGAIN) { perror(read /dev/tty);exit(1);} else {write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));sleep(2);}}if (i 5) {write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));} else {write(STDOUT_FILENO, buf, n);}close(fd);return 0;
}$ gcc block_readtty.c -o block
$ ./block # 此时程序在阻塞等待输入下面输入 hello 后回车即结束
hello
hello3.9 I/O 的效率
使用 read/write 函数实现文件拷贝
// 将一个文件的内容复制到另一个文件中通过打开两个文件循环读取第一个文件的内容并写入到第二个文件中
#include stdio.h
#include stdlib.h
#include unistd.h
#include fcntl.hint main(int argc, char* argv[]) {char buf[1]; // 定义一个大小为 1 的字符数组用于存储读取或写入的数据int n 0;// 打开第一个参数所表示的文件以只读方式打开int fd1 open(argv[1], O_RDONLY);if (fd1 -1) {perror(open argv1 error);exit(1);}// 打开第二个参数所表示的文件以可读写方式打开如果文件不存在则创建如果文件存在则将其清空int fd2 open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664);if (fd2 -1) {perror(open argv2 error);exit(1);}// 循环读取第一个文件的内容每次最多读取 1024 字节// 将返回的实际读取字节数赋值给变量 nwhile ((n read(fd1, buf, 1024)) ! 0) {if (n 0) {perror(read error);break;}// 将存储在 buf 数组中的数据写入文件描述符为 fd2 的文件write(fd2, buf, n);}close(fd1);close(fd2);return 0;
}使用 fputc/fgetc 函数实现文件拷贝
// 使用了 C 标准库中的文件操作函数 fopen()、fgetc() 和 fputc() 来实现文件的读取和写入
#include stdio.h
#include stdlib.h
#include unistd.h
#include fcntl.hint main(int argc, char* argv[]) {FILE *fp, *fp_out;int n 0;fp fopen(hello.c, r);if (fp NULL) {perror(fopen error);exit(1);}fp_out fopen(hello.cp, w);if (fp_out NULL) {perror(fopen error);exit(1);}// 判断是否读取到文件结束符 EOFwhile ((n fgetc(fp)) ! EOF) {fputc(n, fp_out); // 将读取的字符写入输出文件}fclose(fp);fclose(fp_out);return 0;
}read/write每次写一个字节会不断的进行内核态和用户态的切换所以非常耗时fgetc/fputc有个 4096 缓冲区所以不是一个字节一个字节地写内核和用户切换就比较少预读入缓输出机制 系统函数并不一定比库函数快能使用库函数的地方就使用库函数 标准 I/O 函数自带用户缓冲区系统调用无用户级缓冲系统缓冲区是都有的 Linux 上用不同缓冲长度进行读操作的时间结果 大多数文件系统为改善性能都采用某种预读入 (read ahead) 缓输出技术。当检测到正进行顺序读取时系统就试图读入比应用所要求的更多数据并假想应用很快就会读这些数据。预读的效果可以从下图看出缓冲区长度小至 32 字节时的时钟时间与拥有较大缓冲区长度时的时钟时间几乎一样 3.10 文件共享 UNIX 系统支持在不同进程间共享打开文件 内核使用 3 种数据结构表示打开文件它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响 (1) 每个进程在进程表中都有一个记录项记录项中包含一张打开文件描述符表可将其视为一个矢量每个描述符占用一项。与每个文件描述符相关联的是: 文件描述符标志指向一个文件表项的指针 (2) 内核为所有打开文件维持一张文件表。每个文件表项包含 文件状态标志 (读、写、添写、同步和非阻塞等)当前文件偏移量指向该文件 v 节点表项的指针 (3) 每个打开文件 (或设备) 都有一个 v 节点 (v-node) 结构。v 节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件v 节点还包含了该文件的 i 节点 (i-node索引节点)。这些信息是在打开文件时从磁盘上读入内存的所以文件的所有相关信息都是随时可用的 打开文件的内核数据结构 文件描述符标志和文件状态标志在作用范围方面的区别前者只用于一个进程的一个描述符而后者则应用于指向该给定文件表项的任何进程中的所有描述符 3.11 原子操作 一般而言原子操作 (atomic operation) 指的是由多步组成的一个操作。如果该操作原子地执行则要么执行完所有步骤要么一步也不执行不可能只执行所有步骤的一个子集 3.11.1 追加到一个文件 考虑一个进程它要将数据追加到一个文件尾端 对单个进程这段程序能正常工作但若有多个进程同时使用这种方法将数据追加写到同一文件则会产生问题 if(lseek(fd, OL, 2) 0)err_sys(lseek error);
if(write(fd, buf, 100) ! 100)err_sys(write error);假定有两个独立的进程 A 和 B 都对同一文件进行追加写操作每个进程都已打开该文件但未使用 O_APPEND 标志 此时每个进程都有它自己的文件表项但是共享一个 v 节点表项假定进程 A 调用了 lseek它将进程 A 的该文件当前偏移量设置为 1500 字节 (当前文件尾端处)然后内核切换进程进程 B 执行 lseek也将其对该文件的当前偏移量设置为 1500 字节 (当前文件尾端处)然后 B 调用 write它将 B 的该文件当前文件偏移量增加至 1600。因为该文件的长度已经增加了所以内核将 v 节点中的当前文件长度更新为 1600然后内核又进行进程切换使进程 A 恢复运行。当 A 用 write 时就从其当前文件偏移量 (1500) 处开始将数据写入到文件这样也就覆盖了进程 B 刚才写入到该文件中的数据 问题出在逻辑操作 “先定位到文件尾端然后写”它使用了两个分开的函数调用 解决方法使这两个操作对于其他进程而言成为一个原子操作。任何要求多于一个函数调用的操作都不是原子操作因为在两个函数调用之间内核有可能会临时挂起进程UNIX 系统为这样的操作提供了一种原子操作方法即在打开文件时设置 O_APPEND 标志。这样做使得内核在每次写操作之前都将进程的当前偏移量设置到该文件的尾端处于是在每次写之前就不再需要调用 lseek
3.11.2 函数 pread 和 pwrite
#include unistd.hssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);pread 函数返回值 若成功则返回读到的字节数若已读到文件尾返回 0若出错返回 -1 pwrite 函数返回值 若成功则返回已写的字节数若出错返回 -1 调用 pread 相当于调用 lseek 后调用 read但是 pread 又与这种顺序调用有下列重要区别 调用 pread 时无法中断其定位和读操作不更新当前文件偏移量
3.12 函数 dup 和 dup2复制一个现有的文件描述符
#include unistd.hint dup(int fd);
int dup2(int fd, int fd2);函数返回值 若成功返回新的文件描述符若出错返回 -1 由 dup 返回的新文件描述符一定是当前可用文件描述符中的最小数值 对于 dup2可以用 fd2 参数指定新描述符的值 如果 fd2 已经打开则先将其关闭如果 fd fd2则 dup2 返回 fd2而不关闭它否则fd2 的 FD_CLOEXEC 文描述符标志就被清除这样 fd2 在进程调用 exec 时是打开状态 复制一个描述符的另一种方法是使用 fcntl 函数以下函数调用等价 dup(fd);
fcntl(fd, F_DUPFD, 0);// 以下情况并不完全等价
// (1) dup2 是一个原子操作而 close 和 fcnt1 包括两个函数调用// 有可能在 close 和 fcntl 之间调用了信号捕获函数它可能修改文件描述符
// (2) dup2 和 fcntl 有一些不同的 errno
dup2(fd, fd2);close(fd2);
fcntl(fd, F_DUPFD, fd2);3.13 函数 sync、fsync 和 fdatasync
传统的 UNIX 系统实现在内核中设有缓冲区高速缓存或页高速缓存大多数磁盘 I/O 都通过缓冲区进行 向文件写入数据时内核通常先将数据复制到缓冲区然后排入队列晚些时候再写入磁盘这种方式被称为延写通常当内核需要重用缓冲区来存放其他磁盘块数据时它会把所有延迟写数据块写入磁盘为了保证磁盘上实际文件系统与缓冲区中内容的一致性UNIX 系统提供了 sync、fsync 和 fdatasync 三个函数
#include unistd.hint fsync(int fd);
int fdatasync(int fd);void sync(void);函数返回值 若成功返回 0若出错返回 -1 sync 只是将所有修改过的块缓冲区排入写队列然后就返回它并不等待实际写磁盘操作结束 称为 update 的系统守护进程周期性地调用 (一般每隔30秒) sync 函数这就保证了定期冲洗内核的块缓冲区 fsync 函数只对由文件描述符 fd 指定的一个文件起作用并且等待写磁盘操作结束才返回 fsync 可用于数据库这样的应用程序这种应用程序需要确保修改过的块立即写到磁盘上 fdatasync 函数类似于 fsync但它只影响文件的数据部分 除数据外fsync 还会同步更新文件的属性
3.14 函数 fcntl (改变已打开文件的属性)
#include unistd.h
#include fcntl.h// 参数 3 可以是整数或指向一个结构的指针
int fcntl(int fd, int cmd, ... /* int arg */ );函数返回值 若成功则依赖于 cmd 复制一个已有的描述符F_DUPFD 或 F_DUPFD_CLOEXEC返回新的文件描述符获取/设置文件描述符标志F_GETFD 或 F_SETFD返回相应的标志获取/设置文件状态标志F_GETFL 或 F_SETFL返回相应的标志获取/设置异步 I/O 所有权F_GETOWN 或 F_SETOWN返回一个正的进程 ID 或负的进程组 ID获取/设置记录锁F_GETLK、F_SETLK 或 F_SETLKW 若出错返回 -1
案例
// 终端文件默认是阻塞读的这里用 fcntl 将其更改为非阻塞读
#include unistd.h
#include fcntl.h
#include errno.h
#include stdio.h
#include stdlib.h
#include string.h#define MSG_TRY try again\nint main(void) {char buf[10];int flags, n;flags fcntl(STDIN_FILENO, F_GETFL);if (flags -1) {perror(fcntl error);exit(1);}flags | O_NONBLOCK; // 与或操作打开 flagsint ret fcntl(STDIN_FILENO, F_SETFL, flags);if (ret -1) {perror(fcntl error);exit(1);}tryagain:n read(STDIN_FILENO, buf, 10);if (n 0) {if (errno ! EAGAIN) {perror(read /dev/tty);exit(1);}sleep(3);write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));goto tryagain;}write(STDOUT_FILENO, buf, n);return 0;
}3.15 函数 ioctl
#include sys/ioctl.hint ioctl(int fd, unsigned long request, ...);函数返回值 若出错返回 -1若成功返回其他值 对设备的 I/O 通道进行管理控制设备特性(主要应用于设备驱动程序中)通常用来获取文件的物理特性 (该特性不同文件类型所含有的值各不相同)
3.16 传入、传出参数
#include string.hchar* strcpy(char* dest, const char* src);
char* strcpy(char* dest, const char* src, size_t n);传入参数src 指针作为函数参数通常有 const 关键字修饰指针指向有效区域在函数内部做读操作 传出参数dest 指针作为函数参数在函数调用之前指针指向的空间可以无意义但必须有效在函数内部做写操作函数调用结束后充当函数返回值
#include string.hchar* strtok(char* str, const char* delim);
char* strtok_r(char* str, const char* delim, char** saveptr);传入传出参数saveptr 指针作为函数参数在函数调用之前指针指向的空间有实际意义在函数内部先做读、后做写操作函数调用结束后充当函数返回值