目录
1.文件 IO
1.1系统调用
1.2 C 标准库文件 IO 函数
1.3 open/close 函数
1.4 文件描述符表
1.7 read/write 函数
1.8 缓冲区
1.9 错误处理函数
2.0 阻塞、非阻塞
2.1 lseek 函数
辅助学习资料
参考书 1 :《 Unix 环境高级编程》 W.Richard Stevens [ 美 ] 本讲课堂义作为 APUE 的引导。适合初学 Linux 的学员。
《 TCP/IP 详解》 (3 卷 ) ,《 UNIX 网络编程》(2 卷)
参考书 2 :《 Linux 系统编程》 RobertLoVe [ 美 ]
参考书 3 :《 Linux/UNIX 系统编程手册》 Michael Kerrisk [ 德 ]
参考书 4 :《 Unix 内核源码剖析》 青柳隆宏 [ 日 ]
业内知名:《 Linux 内核源代码情景分析》、《 Linux 内核设计与实现》、《深入理解 Linux内核》
1.文件 IO
1.1系统调用
什么是系统调用:
由操作系统实现并提供给外部应用程序的编程接口。 (Application Programming Interface ,API)。是应用程序同系统之间数据交互的桥梁。C 标准函数和系统函数调用关系。一个 helloworld 如何打印到屏幕。
1.2 C 标准库文件 IO 函数
fopen 、 fclose 、 fseek 、 fgets 、 fputs 、 fread 、 fwrite......
r 只读、 r+ 读写
w 只写并截断为 0 、 w+ 读写并截断为 0
a 追加只写、 a+ 追加读写
1.3 open/close 函数
函数原型:
int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);int close(int fd);
常用参数
O_RDONLY 、 O_WRONLY 、 O_RDWR
O_APPEND 、 O_CREAT 、 O_EXCL 、 O_TRUNC 、 O_NONBLOCK
创建文件时,指定文件访问权限。权限同时受 umask 影响。结论为:文件权限 = mode & ~umask
使用头文件: <fcntl.h>
open 常见错误 :
1. 打开文件不存在
2. 以写方式打开只读文件 ( 打开文件没有对应权限 )
3. 以只写方式打开目录
文件描述符:
PCB 进程控制块
可 使 用 命 令
locate sched.h
查 看 位 置 :
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h
struct task_struct { 结构体
1.4 文件描述符表
结构体 PCB 的成员变量 file_struct *file 指向文件描述符表。从应用程序使用角度,该指针可理解记忆成一个字符指针数组,下标 0/1/2/3/4... 找到文件结构体。本质是一个键值对 0、 1 、 2... 都分别对应具体地址。但键值对使用的特性是自动映射,我们只操作键不直接使用值。新打开文件返回文件描述符表中未使用的最小文件描述符。
STDIN_FILENO 0
STDOUT_FILENO 1
STDERR_FILENO 2
1.5 最大打开文件数
一个进程默认打开文件的个数 1024 。
命令查看 ulimit -a 查看 open files 对应值。默认为 1024
可以使用 ulimit -n 4096 修改
当然也可以通过修改系统配置文件永久修改该值,但是不建议这样操作。
cat /proc/sys/fs/file-max 可以查看该电脑最大可以打开的文件个数。受内存大小影响。
1.6 FILE 结构体
主要包含文件描述符、文件读写位置、 IO 缓冲区三部分内容。
struct file {
...
文件的偏移量;
文件的访问权限;
文件的打开标志;
文件内核缓冲区的首地址;
struct operations * f_op;
...
};
查看方法:
(1) /usr/src/linux-headers-3.16.0-30/include/linux/fs.h
(2) lxr :百度 lxr → lxr.oss.org.cn → 选择内核版本 ( 如 3.10) → 点击 File Search 进行搜
索
→ 关键字:“ include/linux/fs.h ” → Ctrl+F 查找 “ struct file { ”
→ 得到文件内核中结构体定义
→ “ struct file_operations ”文件内容操作函数指针
→ “ struct inode_operations ”文件属性操作函数指针
1.7 read/write 函数
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
read 与 write 函数原型类似。使用时需注意: read/write 函数的第三个参数。练习:编写程序实现简单的 cp 功能。
程序比较:如果一个只读一个字节实现文件拷贝,使用 read 、 write 效率高,还是使用对应的标库函数(fgetc 、 fputc) 效率高呢?
strace 命令
shell 中使用 strace 命令跟踪程序执行,查看调用的系统函数。
1.8 缓冲区
read 、 write 函数常常被称为 Unbuffered I/O 。指的是无用户及缓冲区。但不保证不使用内核缓冲区。
1.9 错误处理函数
错误号: errnoperror 函数: void perror(const char *s);strerror 函数:char *strerror(int errnum);查看错误号:/usr/include/asm-generic/errno-base.h/usr/include/asm-generic/errno.h#define EPERM1 /* Operation not permitted */#define ENOENT 2 /* No such file or directory */#define ESRCH3/* No such process */#define EINTR4/* Interrupted system call */#define EIO5 /* I/O error */#define ENXIO6 /* No such device or address */#define E2BIG7 /* Argument list too long */#define ENOEXEC 8 /* Exec format error */#define EBADF9 /* Bad file number */#define ECHILD10 /* No child processes */#define EAGAIN 11 /* Try again */#define ENOMEM 12 /* Out of memory */#define EACCES13 /* Permission denied */#define EFAULT14 /* Bad address */#define ENOTBLK 15 /* Block device required */#define EBUSY16 /* Device or resource busy */#define EEXIST17 /* File exists */#define EXDEV18 /* Cross-device link */#define ENODEV 19 /* No such device */#define ENOTDIR 20 /* Not a directory */#define EISDIR21 /* Is a directory */#define EINVAL22 /* Invalid argument */#define ENFILE23 /* File table overflow */#define EMFILE24 /* Too many open files */#define ENOTTY 25 /* Not a typewriter */#define ETXTBSY 26 /* Text file busy */#define EFBIG27 /* File too large */#define ENOSPC 28 /* No space left on device */#define ESPIPE29 /* Illegal seek */#define EROFS30 /* Read-only file system */#define EMLINK 31 /* Too many links */#define EPIPE32 /* Broken pipe */#define EDOM33 /* Math argument out of domain of func */#define ERANGE 34 /* Math result not representable */
2.0 阻塞、非阻塞
读常规文件是不会阻塞的,不管读多少字节, read 一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用 read 读终端设备就会阻塞,如果网络上没有接收到数据包,调用 read 从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
现在明确一下阻塞( Block )这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep )状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用 sleep 指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running )状态,在 Linux 内核中,处于运行状态的进程分为两种情况:正在被调度执行。CPU 处于该进程的上下文环境中,程序计数器( eip )里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但 CPU 暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。
阻塞读终端:【 block_readtty.c 】
非阻塞读终端 : 【 nonblock_readtty.c 】
非阻塞读终端和等待超时:【 nonblock_timeout.c 】
注意,阻塞与非阻塞是对于文件而言的。而不是 read 、 write 等的属性。 read 终端,默认阻塞读。
总结 read 函数返回值:
1. 返回非零值: 实际 read 到的字节数
2. 返回 -1 : 1 ): errno != EAGAIN ( 或 != EWOULDBLOCK) read 出错
2 ): errno == EAGAIN ( 或 == EWOULDBLOCK) 设置了非阻塞读,并且没有数据到达。
3. 返回 0 :读到文件末尾。
2.1 lseek 函数
文件偏移
Linux 中可使用系统函数 lseek 来修改文件偏移量 ( 读写位置 ) 每个打开的文件都记录着当前读写位置,打开文件时读写位置是 0 ,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以 O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek和标准 I/O 库的 fseek 函数类似,可以移动当前读写位置(或者叫偏移量。回忆 fseek 的作用及常用参数。 SEEK_SET 、 SEEK_CUR 、 SEEK_END int fseek(FILE *stream, long offset, int whence); 成功返回 0 ;失败返回 -1
特别的:超出文件末尾位置返回 0 ;往回超出文件头位置,返回 -1 off_t lseek(int fd, off_t offset, int whence); 失败返回 -1 ; 成功:返回的值是较文件起始位
置向后的偏移量。特别的:lseek 允许超过文件结尾设置偏移量,文件会因此被拓展。 注意文件“读”和“写”使用同一偏移位置。 【lseek.c 】
lseek 常用应用:
1. 使用 lseek 拓展文件: write 操作才能实质性的拓展文件。单 lseek 是不能进行拓展的。一般:write(fd, "a", 1)
od -tcx filename 查看文件的 16 进制表示形式
od -tcd filename 查看文件的 10 进制表示形式
2. 通过 lseek 获取文件的大小: lseek(fd, 0, SEEK_END);
【 lseek_test.c 】
【最后注意】: lseek 函数返回的偏移量总是相对于文件头而言。
fcntl 函数
改变一个【已经打开】的文件的 访问控制属性。重点掌握两个参数的使用,F_GETFL 和 F_SETFL 。
【 fcntl.c 】
ioctl 函数
对设备的 I/O 通道进行管理,控制设备特性。 ( 主要应用于设备驱动程序中 ) 。通常用来获取文件的【物理特性】(该特性,不同文件类型所含有的值各不相同)
【 ioctl.c 】
传入传出参数
传入参数 :
const 关键字修饰的 指针变量 在函数内部读操作。 char *strcpy(cnost char *src, char *dst);
传出参数 :
1. 指针做为函数参数
2. 函数调用前,指针指向的空间可以无意义,调用后指针指向的空间有意义, 且作为函数的返回值传出
3. 在函数内部写操作。
传入传出参数:
1. 调用前指向的空间有实际意义 2. 调用期间在函数内读、写 ( 改变原值 ) 操作 3. 作为函数返回值传出。
扩展阅读:
关于虚拟 4G 内存的描述和解析:
一个进程用到的虚拟地址是由内存区域表来管理的,实际用不了 4G 。而用到的内存区域,会通过页表映射到物理内存。所以每个进程都可以使用同样的虚拟内存地址而不冲突,因为它们的物理地址实际上是不同的。内核用的是 3G 以上的 1G 虚拟内存地址,其中 896M 是直接映射到物理地址的, 128M 按需映射 896M 以上的所谓高位内存。各进程使用的是同一个内核。首先要分清“可以寻址”和“实际使用”的区别。其实我们讲的每个进程都有 4G 虚拟地址空间,讲的都是“可以寻址” 4G ,意思是虚拟地址的 0-3G 对于一个进程的用户态和内核态来说是可以访问的,而 3-4G 是只有进程的内核态可以访问的。并不是说这个进程会用满这些空间。其次,所谓“独立拥有的虚拟地址”是指对于每一个进程,都可以访问自己的 0-4G 的虚拟地址。虚拟地址是“虚拟”的,需要转化为“真实”的物理地址。好比你有你的地址簿,我有我的地址簿。你和我的地址簿都有 1、 2 、 3 、 4 页,但是每页里面的实际内容是不一样的,我的地址簿第 1 页写着 3 你的地址簿第 1 页写着 4 ,对于你、我自己来说都是用第 1 页(虚拟),实际上用的分别是第 3 、 4 页(物理),不冲突。内核用的 896M 虚拟地址是直接映射的,意思是只要把虚拟地址减去一个偏移量( 3G ) 就等于物理地址。同样,这里指的还是寻址,实际使用前还是要分配内存。而且 896M 只是个最大值。如果物理内存小,内核能使用(分配)的可用内存也小。