1. 系统调用
应用程序 ------------+
| |
v |
各种库 |
(C/C++标准库、Shell命令和脚本、 |
X11图形程序及库) |
| |
v |
系统调用 <-------------+
(内核提供给外界访问的接口函数,
调用这些函数将使进程进入内核态)
|
v
内核
(驱动程序、系统功能程序)
Unix/Linux 大部分系统功能是通过系统调用实现的,如 open/close。
fopen/fclose 是 C 库的函数;非系统级的;
Unix/Linux 的系统调用已被封装成 C 函数的形式,但它们并不是标准 C 的一部分。
标准库函数大部分时间运行在用户态,但部分函数偶尔也会调用系统调用,进入内核态;如 malloc/free。
程序员自己编写的代码也可以调用系统调用,与操作系统内核交互,进入内核态;如 brk/sbrk/mmap/munmap。
系统调用在内核中实现,其外部接口定义在C库中;该接口的实现借助软中断进入内核。
linux 目录结构:
Home 目录:作用是为每个账户提供账户信息,如果我们的 Linux 中有多个账户,那么在 Home 目录中可以看到和账户名对应的目录
(如果两个账户有不同的桌面,那么这些桌面的配置文件就放在相关的目录中);Home 目录可以理解为 Windows 中的“我的文档”目录;
Var 目录:包含有变动的文件,比如脱机目录(用于有效邮件、新闻、打印机等)、日志文件、格式化的手册页和临时文件,
Usr 目录:可以理解为 Windows 中的“Temp”目录;
Bin 目录:启动期间,它为普通用户提供使用的命令,Bin 目录可以理解为 Windows 中的 “System32” 和 “Program Files” 目录;
Etc 和 Dev 目录:都是与设备有关的目录,里面保存了设备的驱动程序等信息。这两个目录可以理解为 Windows 中的“drivers”目录;
Lib 目录:系统的共享目录,里面保存的文件能供系统中的多个程序调用。该目录可以理解为 Windows 中相关程序的 dll 文件集合;
Boot 目录:包含启动时转载程序所用文件,比如 LILO;
内核镜像通常保存在这里,如果有多个内核镜像,这个目录可能会增长得很快,最好把它单独保存在一个文件系统内。
该目录可以理解为 Windows 中根目录下的相关启动文件的集合。
2. time 命令:测试运行时间
real : 总执行时间
user : 用户空间执行时间(输入输出的等待时间不算)
sys : 内核空间执行时间
范例:
# time a.out
real 0m0.255s // m 分,s 秒
user 0m0.100s
sys 0m0.155s
strace 命令:跟踪系统调用
3. 一切皆文件
Linux 环境中的文件具有特别重要的意义,因为它为操作系统服务和设备,提供了一个简单而统一的接口。
在 Linux 中,(几乎)一切皆文件。
程序完全可以象访问普通磁盘文件一样,访问串行口、网络、打印机或其它设备。
大多数情况下只需要使用五个基本系统调用:
open/close/read/write/ioctl,
即可实现对各种设备的输入和输出。
Linux 中的任何对象都可以被视为某种特定类型的文件,可以访问文件的方式访问之。
广义的文件:
1) 目录文件
# vim day01
2) 设备文件
A. 控制台: /dev/console
B. 声卡: /dev/audio
C. 标准输入输出: /dev/tty
D. 空设备: /dev/null
例如:
标准输入输出:
# cat /dev/tty
输入:Hello, World !
输出:Hello, World !
输出重定向:
# echo Hello, World ! > /dev/tty
输出:Hello, World !
把 hello, world ! 写道 text.txt 文档里
# echo Hello, World ! > test.txt
# cat test.txt // 查看 text.txt 文件
内容:Hello, World !
清空 test.txt
# cat /dev/null > test.txt
# cat test.txt
内容为空;
查找名字含有 per 的文件,且权限不够的不显示:
# find / -name perl 2> /dev/null
4. 文件相关系统调用命令
open - 打开/创建文件
creat - 创建空文件
close - 关闭文件
read - 读取文件
write - 写入文件
lseek - 设置读写位置
fcntl - 修改文件属性
unlink - 删除硬链接
rmdir - 删除空目录
remove - 删除硬链接 (unlink) 或空目录 (rmdir)
unix 下没有文件名的概念,其实硬链接相当于文件名;
磁盘上的一个文件可能对应多个硬链接,硬链接通过 i 节点访问源文件;
文件被真正删除的条件是与之相关的所有硬链接文件都被删除;
软连接相当于硬链接的快捷方式,软链接又叫符号连接;
硬链接只是一个文件名,即目录中的一个条目。
软链接则是一个独立的文件,其内容是另一个文件的路径信息。
不同文件系统之间不可以建立硬链接;
但是可以建立软件连接;
ln:创建硬链接(hard link)
ln -s:创建软连接(symbolic link)
注意:
如果被 unlink/remove 删除的是文件的最后一个硬链接,并且没有进程正打开该文件,
那么该文件在磁盘上的存储区域将被立即标记为自由。
反之,如果有进程正打开该文件,那么该文件在磁盘上的存储区域,
将在所有进程关闭该文件之后被标记为自由。
a -> +-----+
X b -> | ... |
X c -> +-----+
如果被 unlink/remove 删除的是一个软链接文件,
那么仅软链接文件本身被删除,其目标不受影响。
(软链接:本身是个文件,里面存放着硬链接;)
+------+
a -> | ... | // a 是硬链接
+------+
+------+
X b -> | a | // b 和 c 是软链接
+------+
+------+
X c -> | a |
+------+
5. 文件描述符:
非负的整数,表示一个打开的文件。
由系统调用 (open/creat) 返回,被内核空间 (后续系统调用) 引用。
内核缺省为每个进程打开三个文件描述符(类似于句柄):
0 - 标准输入
1 - 标准输出
2 - 标准出错
其他描述符从 3 开始往后定义;
在 unistd.h 中被定义为如下三个宏:
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
范例:redir.c
#include <stdio.h>
int main () {
int data;
fscanf (stdin, "%d", &data);
fprintf (stdout, "%d", data);
fprintf (stderr, "%d", data);
return 0;
}
// stdout:有缓冲区,遇到换行之类的才输出;
// stderr:显示错误,无缓冲区;
下面重定向输出:
# a.out 0<i.txt 1>o.txt 2>e.txt // 一个大于号是覆盖,两个大于号是追加
把标准输入定向到 i.txt 文件里;(注意:输入是小于号,其他是大于号)
把标准输出定向到 o.txt 文件里;
把标准出错定向到 e.txt 文件里;
文件描述符的范围介于 0 到 OPEN_MAX 之间,
传统 Unix 中 OPEN_MAX 宏被定义为 63,现代 Linux 使用更大的上限,一般为 256。
一个进程可以同时打开的文件描述符个数,
受 limits.h 中定义的 OPEN_MAX 宏的限制,
POSIX 要求不低于 16,传统 Unix 是 63,现代 Linux 是 256。
6. open/creat/close
#include <fcntl.h>
int open (
const char* pathname, // 路径
int flags, // 模式
mode_t mode // 权限(仅创建文件有效)
); // 创建/读写文件时都可用此函数
int creat (
const char* pathname, // 路径
mode_t mode // 权限
); // 常用于创建文件
int open (
const char* pathname, // 路径
int flags // 模式
); // 常用于读写文件
成功返回文件描述符,失败返回 -1。
open/creat 所返回的一定是当前未被使用的最小文件描述符。
例如:假如 3 和 4 都被使用了,然后 3 释放了,当再次需要 open/creat 的时候,
是 3 被调用,而不是 5;
creat 函数是通过调用 open 实现的:
int creat (const char* pathname, mode_t mode) {
return open (pathname,
O_WRONLY | O_CREAT | O_TRUNC, mode);
}
flags 为以下值的位或:
常用:
O_RDONLY - 只读 \
|
O_WRONLY - 只写 > 只选一个
|
O_RDWR - 读写 /
O_APPEND - 追加
O_CREAT - 创建,不存在即创建(已存在即直接打开,并保留原内容,除非 . . . ),
有此位 mode 参数才有效。
O_EXCL - 排斥,已存在即失败。 \
> 只选一个,配合 O_CREAT 使用;
O_TRUNC - 清空,已存在即清空 /
(有 O_WRONLY/O_RDWR)。
不常用:
O_NOCTTY - 非控,若 pathname 指向控制终端,则不将该终端作为控制终端。
O_NONBLOCK - 非阻,若 pathname 指向 FIFO/块/字符文件,则该文件的打开及后续操作均为非阻塞模式。
O_SYNC - 同步,write 等待数据和属性,被物理地写入底层硬件后再返回。
O_DSYNC - 数同,write 等待数据,被物理地写入底层硬件后再返回。
O_RSYNC - 读同,read 等待对所访问区域的所有写操作,全部完成后再读取并返回。
O_ASYNC - 异步,当文件描述符可读/写时,向调用进程发送 SIGIO 信号。
#include <unistd.h>
int close (
int fd // 文件描述符
);
成功返回 0,失败返回 -1。
fopen 和 open 之间的等价关系:
r O_RDONLY
r+ O_RDWR
w O_WRONLY | O_CREAT | O_TRUNC, 0666
w+ O_RDWR | O_CREAT | O_TRUNC, 0666
a O_WRONLY | O_CREAT | O_APPEND, 0666
a+ O_RDWR | O_CREAT | O_APPEND, 0666
0666:0 代表八进制;下面三位分别是:属主,同组,其他用户;
6 是由 4 2 1 组成,4 代表可读,2 代表可写,1 代表可执行;
操作系统可通过权限掩码 (当前为 0002,有的是 0022),屏蔽程序所创建文件的某些权限位。如:
$ umask // 查看当前掩码
0002 // 用于屏蔽其他用户的写权限
0666 (-rw-rw-rw-) & ~0002 = 0664 (-rw-rw-r--)
范例:open.c
#include <stdio.h>
#include <fcntl.h>
int main () {
int fd1 = open ("open.txt", O_RDWR | O_CREAT | O_TUNC, 0666); // 可读写,且不存在就创建,存在就清空
if (fd1 == -1) {
perror ("open");
return -1;
}
printf ("%d\n", fd1);
close (fd1);
return 0;
}
这个程序输出后的权限为:-rw-rw-r--
而不是我们期望的:-rw-rw-rw-
原因就是我们上面讲到的系统权限掩码在起作用;自动屏蔽其他用户的写权限;
7. write
#include <unistd.h>
ssize_t write (
int fd, // 文件描述符
const void* buf, // 缓冲区
size_t count // 期望写入的字节数
);
成功返回实际写入的字节数(0 表示未写入,不是失败),失败返回 -1。
size_t: unsigned int,无符号整数
ssize_t: int,有符号整数
范例:write.c
#include <stdio.h>
#include <fcntl.h> // open
#include <string.h> // strlen
#include <unistd.h> // write
int main () {
int fd = open ("write.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fd == -1) {
perror ("open");
return -1;
}
const char* text = "hello word";
printf ("要写入的内容:%s\n", text);
// 要写入的字节数:字符个数乘以字节数,扩大安全性;因为有的机器上一个 char 不一定是一个字节,有可能是 2 字节;
size_t towrite = strlen (text) * sizeof (char);
ssize_t written = write (fd, text, towrite);
if (written == -1) {
perror ("write");
return -1;
}
printf ("期望写入%d字节,实际写入%d字节。\n", towrite, written);
close (fd);
return 0;
}
这个层面上,不分写入的是文本格式还是二进制,都是二进制;
如果遇到实际写入的字节小于期望写入的字节,我们就继续写。
8. read
#include <unistd.h>
ssize_t read (
int fd, // 文件描述符
void* buf, // 缓冲区
size_t count // 期望读取的字节数
);
成功返回实际读取的字节数(返回 0 表示读到文件尾),失败返回 -1。
范例:read.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main () {
int fd = open ("read.txt", O_RDONLY);
if (fd == -1) {
perror ("open");
return -1;
}
char text[256] = {}; // 初始化为 0,让下面的 printf 可以正常输出字符串;
size_t toread = sizeof (text);
ssize_t readed = read (fd, text, toread);
if (-1 == readed) {
perror ("read");
return -1;
}
printf ("期望读取%d字节,实际读取%d字节.\n", toread, readed);
printf ("读取的内容为:%s\n", text);
return 0;
}
练习:二进制读写
范例:binary.c
#include <stdio.h>
#include <fcntl.h>
int main (void) {
int fd = open ("binary.dat", O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fd == -1) {
perror ("open");
return -1;
}
char name[256] = "张飞";
if (write (fd, name, sizeof (name)) == -1) {
perror ("write");
return -1;
}
unsigned int age = 38;
if (write (fd, &age, sizeof (age)) == -1) {
perror ("write");
return -1;
}
double salary = 20000;
if (write (fd, &salary, sizeof (salary)) == -1) {
perror ("write");
return -1;
}
struct Employee {
char name[256];
unsigned int age;
double salary;
} employee = {"赵云", 25, 8000};
// 直接把 employee 写入:
if (write (fd, &employee, sizeof (employee)) == -1) {
perror ("write");
return -1;
}
close (fd);
if ((fd = open ("binary.dat", O_RDONLY)) == -1) {
perror ("open");
return -1;
}
if (read (fd, name, sizeof (name)) == -1) {
perror ("read");
return -1;
}
printf ("姓名:%s\n", name);
if (read (fd, &age, sizeof (age)) == -1) {
perror ("read");
return -1;
}
printf ("年龄:%u\n", age);
if (read (fd, &salary, sizeof (salary)) == -1) {
perror ("read");
return -1;
}
printf ("工资:%.2lf\n", salary);
if (read (fd, &employee, sizeof (employee)) == -1) {
perror ("read");
return -1;
}
printf ("员工:%s %u %.2lf\n", employee.name, employee.age,
employee.salary);
close (fd);
return 0;
}
练习:文本读写
范例:text.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main (void) {
int fd = open ("text.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror ("open");
return -1;
}
char name[256] = "张飞";
unsigned int age = 38;
double salary = 20000;
char buf[1024];
sprintf (buf, "%s %u %.2lf\n", name, age, salary);
if (write (fd, buf, strlen (buf) * sizeof (buf[0])) == -1) {
perror ("write");
return -1;
}
struct Employee {
char name[256];
unsigned int age;
double salary;
} employee = {"赵云", 25, 8000};
// 把 employee 存入 buf 再写入:
sprintf (buf, "%s %u %.2lf", employee.name, employee.age,
employee.salary);
if (write (fd, buf, strlen (buf) * sizeof (buf[0])) == -1) {
perror ("write");
return -1;
}
close (fd);
if ((fd = open ("text.txt", O_RDONLY)) == -1) {
perror ("open");
return -1;
}
memset (buf, 0, sizeof (buf)); // 初始化为 0;
if (read (fd, buf, sizeof (buf)) == -1) {
perror ("read");
return -1;
}
sscanf (buf, "%s%u%lf%s%u%lf", name, &age, &salary,
employee.name, &employee.age, &employee.salary);
printf ("姓名:%s\n", name);
printf ("年龄:%u\n", age);
printf ("工资:%.2lf\n", salary);
printf ("员工:%s %u %.2lf\n", employee.name, employee.age,
employee.salary);
close (fd);
return 0;
}
练习:带覆盖检查的文件复制。
代码:copy.c
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
int main (int argc, char* argv[]) {
if (argc < 3) { // 通过命令行确定源文件和目标文件
fprintf (stderr, "用法:%s <源文件> <目的文件>\n", argv[0]);
return -1;
}
int src = open (argv[1], O_RDONLY); // 打开源文件
if (src == -1) {
perror ("open");
return -1;
}
// 复制的时候,我们不仅需要复制内容,还需要复制状态属性;通过 stat 实现;
struct stat st; // 代表文件的状态;
if (fstat (src, &st) == -1) { // 得到文件状态/属性
perror ("fstat");
return -1;
}
// 以源文件的属性打开目标文件:
int dst = open (argv[2], O_WRONLY | O_CREAT | O_EXCL, st.st_mode);
if (dst == -1) {
if (errno != EEXIST) { // EEXIST 表示文件已存在
perror ("open");
return -1;
}
printf ("文件%s已存在,是否覆盖?(y/n) ", argv[2]);
int ch = getchar ();
if (ch != 'y' && ch != 'Y') // 不覆盖
return 0;
if ((dst = open (argv[2], O_WRONLY | O_CREAT | O_TRUNC, st.st_mode)) == -1) {
perror ("open");
return -1;
}
}
// 读文件
unsigned char buf[1024];
ssize_t bytes;
while ((bytes = read (src, buf, sizeof (buf))) > 0) // 防止文件大于缓冲区,需要多次读取;
if (write (dst, buf, bytes) == -1) { // 如果写入设备特殊,也需要多次写入;
perror ("write");
return -1;
}
if (bytes == -1) {
perror ("read");
return -1;
}
close (dst);
close (src);
return 0;
}
gcc 输入:
$ ./a.out a.out b.out
$ diff a.out b.out // 判断两个文件是否一样
9. 系统 I/O 与标准 I/O
当系统调用函数被执行时,需要切换用户态和内核态,频繁调用会导致性能损失。
标准库做了必要的优化,内部维护一个缓冲区,只在满足特定条件时才将缓冲区与系统内核同步,
借此降低执行系统调用的频率,减少进程在用户态和内核态之间来回切换的次数,提高运行性能。
范例:stdio.c
#include <stdio.h>
int main (void) {
FILE* fp = fopen ("stdio.dat", "wb"); // 二进制方式写入
if (! fp) {
perror ("fopen");
return -1;
}
unsigned int i;
for (i = 0; i < 100000; i++)
fwrite (&i, sizeof (i), 1, fp);
fclose (fp);
return 0;
}
gcc stdio.c -o stdio
范例:sysio.c
#include <stdio.h>
#include <fcntl.h>
int main (void) {
int fd = open ("sysio.dat", O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fd == -1) {
perror ("open");
return -1;
}
unsigned int i;
for (i = 0; i < 100000; i++)
write (fd, &i, sizeof (i));
close (fd);
return 0;
}
gcc sysio.c -o sysio
# time ./sysio
real 0m17.442s
user 0m0.000s
sys 0m0.284s
# time ./stdio
real 0m0.056s
user 0m0.000s
sys 0m0.009s
小结:能用标准库的尽量用标准库,没法用的时候再用系统库;
10. 人为设置文件位置:lseek
每个打开的文件都有一个与其相关的 “文件位置”。
文件位置通常是一个非负整数,用以度量从文件头开始计算的字节数。
读写操作都从当前文件位置开始,并根据所读写的字节数,增加文件位置。
打开一个文件时,除非指定了 O_APPEND,否则文件位置一律被设为 0。
lseek 函数仅将文件位置记录在内核中,并不引发任何 I/O 动作。
如果在文件尾继续把文件指针往后移动,是可以的,那么此时将在文件中形成空洞。
文件空洞不占用磁盘空间,但被算在文件大小内。
lseek (fd, 8, SEEK_END); // 默认移动顺序是从前往后;形成八个字节空洞;
#include <sys/types.h>
#include <unistd.h>
off_t lseek (
int fd, // 文件描述符
off_t offset, // 偏移量
int whence // 起始位置
);
成功返回当前文件位置(相对于文件头 0),失败返回 -1。
whence取值:
SEEK_SET - 从文件头(文件的第一个字节)。
SEEK_CUR - 从当前位置(上一次读写的最后一个字节的下一个位置)。
SEEK_END - 从文件尾(文件的最后一个字节的下一个位置)。
范例:seek.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
int main (void) {
int fd = open ("seek.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror ("open");
return -1;
}
const char* text = "Hello, World !"; // H 位置为 0
if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
// 写完后,当前位置在 ! 后面那个位置
perror ("write");
return -1;
}
// 从当前位置向文件头偏移 7 个位置,也就到了 W 位置;
if (lseek (fd, -7, SEEK_CUR) == -1) { // 返回当前位置,所以不可能返回 -1
perror ("lseek");
return -1;
}
off_t pos = lseek (fd, 0, SEEK_CUR); // off_t 为常整型
if (pos == -1) {
perror ("lseek");
return -1;
}
printf ("当前文件位置:%ld\n", pos); // 输出 7
// 起初文件头为 0,从 H 开始,移动后到 W 位置,所以输出为 7
text = "Linux";
if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
// 再次写入就会覆盖 W 以后 5 个位置,输出:Hello Linux !
perror ("write");
return -1;
}
// 文件动效果:从文件尾往后偏移 8,超出文件长度,形成 8 个字节的洞
if (lseek (fd, 8, SEEK_END) == -1) {
perror ("lseek");
return -1;
}
text = "<-这里有个洞洞!";
if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
// 此时 seek.txt 文件内容为:Hello Linux !xxxxxxxx<-这里有个洞洞!
perror ("write");
return -1;
}
// 求文件大小
off_t size = lseek (fd, 0, SEEK_END);
if (size == -1) {
perror ("lseek");
return -1;
}
printf ("文件大小:%d字节\n", size);
close (fd);
return 0;
}
思考:既然 lseek 系统调用相当于标 C 库函数 fseek,
那么是否存在与标 C 库函数 ftell 相对应的系统调用?
答:不存在,因为通过 lseek(fd, 0, SEEK_CUR) 就可以获得当前文件位置,不需要 ftell 功能。
思考:如何获取文件的大小?
答:通过 lseek(fd, 0, SEEK_END) 可以获得文件的大小。
11. 打开文件的内核数据结构
通过 ls -i 可查看文件的 i 节点号。
i 节点记录了文件的属性和数据在磁盘上的存储位置。
目录也是文件,存放路径和 i 节点号的映射表。
图示:open.bmp
范例:bad.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main (void) {
const char* text = "Hello, World !";
/* 如果未打开就直接写入,而且描述符 3 是无效的,那么下面这段代码会出错;
if (write (3, text, strlen (text)) == -1) {
// 如果描述符为 1,则正常输出,会在屏幕上输出:Hello, World !
perror ("write");
return -1;
}
*/
/* 如果关闭标准输出,那么 printf 这段代码无法输出;
close (1);
printf ("%s\n", text);
*/
if (close (STDOUT_FILENO) == -1) { // STDOUT_FILENO 相当于 1
perror ("close");
return -1;
}
// 输出重定向,先关闭 1,那么调用 creat 的时候,就把标准输出 1 定向到 bad.txt 文件里;
close (1);
if (creat ("bad.txt", 0644) == -1) {
perror ("creat");
return -1;
}
printf ("%s", text); // printf 原来输出到屏幕(1 对应屏幕),现在输出到 bad.txt 文件里;
return 0;
}
相当于:
$ echo hello, word ! 1>bad.txt
$ echo hello, word >bad.txt // 如果不写 1 就不能写感叹号,bashell 不识别;
12. 复制一个已打开的文件描述符 dup/dup2
#include <unistd.h>
int dup (int oldfd);
int dup2 (int oldfd, int newfd);
成功返回文件描述符 oldfd 的副本,失败返回 -1。
返回的一定是当前未被使用的最小文件描述符。
dup2 可由第二个参数指定描述符的值。
若指定描述符已打开,则先关闭之。
所返回的文件描述符副本,与源文件描述符,对应同一个文件表。
注意区分通过 dup 获得的文件描述符副本,
和两次 open 同一个文件的区别:
dup 只复制文件描述符,不复制文件表:
fd1 \
> 文件表 -> v节点 -> i节点
fd2 /
open 创建新文件表,并为其分配新文件描述符,出现两个文件表:
fd1 -> 文件表1 \
> v节点 -> i节点
fd2 -> 文件表2 /
图示:same.bmp
图示:dup.bmp
范例:dup.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#inlcude <unistd.h>
int main (void) {
int fd1 = open ("dup.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd1 == -1) {
perror ("open");
return -1;
}
printf ("fd1 = %d\n", fd1); // 3
int fd2 = dup (fd1);
if (fd2 == -1) {
perror ("dup");
return -1;
}
printf ("fd2 = %d\n", fd2); // 4
int fd3 = dup2 (fd2, 100); // 定义 fd3 的文件描述符为 100
if (fd3 == -1) {
perror ("dup2");
return -1;
}
printf ("fd3 = %d\n", fd3); // 100
// 通过 fd1 写文件
const char* text = "Hello, World !";
if (write (fd1, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
// 通过 fd2 偏移
if (lseek (fd2, -7, SEEK_END) == -1) {
perror ("lseek");
return -1;
}
// 通过 fd3 更改
text = "Linux";
if (write (fd3, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
close (fd3);
close (fd2);
close (fd1);
return 0;
}
输出:Hello, Linux !
dup 一般用于多线程编程里;当两个线程对同一个文件写操作;
如果用 open,那么两个线程写入的东西会产生覆盖;用 dup 用同一个文件表就不会出现这种情况;
范例:same.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main (void) {
int fd1 = open ("same.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd1 == -1) {
perror ("open");
return -1;
}
printf ("fd1 = %d\n", fd1);
int fd2 = open ("same.txt", O_RDWR);
if (fd2 == -1) {
perror ("open");
return -1;
}
printf ("fd2 = %d\n", fd2);
int fd3 = open ("same.txt", O_RDWR);
if (fd3 == -1) {
perror ("open");
return -1;
}
printf ("fd3 = %d\n", fd3);
const char* text = "Hello, World !";
if (write (fd1, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
if (lseek (fd2, -7, SEEK_END) == -1) {
perror ("lseek");
return -1;
}
text = "Linux";
if (write (fd3, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
close (fd3);
close (fd2);
close (fd1);
return 0;
}
输出:Linux, World !
13. 作业:学生管理系统登录模块。
注册 - 增加用户名和密码,
登录 - 验证用户名和密码,
用户信息保存在文件中。
参见项目:mis
应用程序 ------------+
| |
v |
各种库 |
(C/C++标准库、Shell命令和脚本、 |
X11图形程序及库) |
| |
v |
系统调用 <-------------+
(内核提供给外界访问的接口函数,
调用这些函数将使进程进入内核态)
|
v
内核
(驱动程序、系统功能程序)
Unix/Linux 大部分系统功能是通过系统调用实现的,如 open/close。
fopen/fclose 是 C 库的函数;非系统级的;
Unix/Linux 的系统调用已被封装成 C 函数的形式,但它们并不是标准 C 的一部分。
标准库函数大部分时间运行在用户态,但部分函数偶尔也会调用系统调用,进入内核态;如 malloc/free。
程序员自己编写的代码也可以调用系统调用,与操作系统内核交互,进入内核态;如 brk/sbrk/mmap/munmap。
系统调用在内核中实现,其外部接口定义在C库中;该接口的实现借助软中断进入内核。
linux 目录结构:
Home 目录:作用是为每个账户提供账户信息,如果我们的 Linux 中有多个账户,那么在 Home 目录中可以看到和账户名对应的目录
(如果两个账户有不同的桌面,那么这些桌面的配置文件就放在相关的目录中);Home 目录可以理解为 Windows 中的“我的文档”目录;
Var 目录:包含有变动的文件,比如脱机目录(用于有效邮件、新闻、打印机等)、日志文件、格式化的手册页和临时文件,
Usr 目录:可以理解为 Windows 中的“Temp”目录;
Bin 目录:启动期间,它为普通用户提供使用的命令,Bin 目录可以理解为 Windows 中的 “System32” 和 “Program Files” 目录;
Etc 和 Dev 目录:都是与设备有关的目录,里面保存了设备的驱动程序等信息。这两个目录可以理解为 Windows 中的“drivers”目录;
Lib 目录:系统的共享目录,里面保存的文件能供系统中的多个程序调用。该目录可以理解为 Windows 中相关程序的 dll 文件集合;
Boot 目录:包含启动时转载程序所用文件,比如 LILO;
内核镜像通常保存在这里,如果有多个内核镜像,这个目录可能会增长得很快,最好把它单独保存在一个文件系统内。
该目录可以理解为 Windows 中根目录下的相关启动文件的集合。
2. time 命令:测试运行时间
real : 总执行时间
user : 用户空间执行时间(输入输出的等待时间不算)
sys : 内核空间执行时间
范例:
# time a.out
real 0m0.255s // m 分,s 秒
user 0m0.100s
sys 0m0.155s
strace 命令:跟踪系统调用
3. 一切皆文件
Linux 环境中的文件具有特别重要的意义,因为它为操作系统服务和设备,提供了一个简单而统一的接口。
在 Linux 中,(几乎)一切皆文件。
程序完全可以象访问普通磁盘文件一样,访问串行口、网络、打印机或其它设备。
大多数情况下只需要使用五个基本系统调用:
open/close/read/write/ioctl,
即可实现对各种设备的输入和输出。
Linux 中的任何对象都可以被视为某种特定类型的文件,可以访问文件的方式访问之。
广义的文件:
1) 目录文件
# vim day01
2) 设备文件
A. 控制台: /dev/console
B. 声卡: /dev/audio
C. 标准输入输出: /dev/tty
D. 空设备: /dev/null
例如:
标准输入输出:
# cat /dev/tty
输入:Hello, World !
输出:Hello, World !
输出重定向:
# echo Hello, World ! > /dev/tty
输出:Hello, World !
把 hello, world ! 写道 text.txt 文档里
# echo Hello, World ! > test.txt
# cat test.txt // 查看 text.txt 文件
内容:Hello, World !
清空 test.txt
# cat /dev/null > test.txt
# cat test.txt
内容为空;
查找名字含有 per 的文件,且权限不够的不显示:
# find / -name perl 2> /dev/null
4. 文件相关系统调用命令
open - 打开/创建文件
creat - 创建空文件
close - 关闭文件
read - 读取文件
write - 写入文件
lseek - 设置读写位置
fcntl - 修改文件属性
unlink - 删除硬链接
rmdir - 删除空目录
remove - 删除硬链接 (unlink) 或空目录 (rmdir)
unix 下没有文件名的概念,其实硬链接相当于文件名;
磁盘上的一个文件可能对应多个硬链接,硬链接通过 i 节点访问源文件;
文件被真正删除的条件是与之相关的所有硬链接文件都被删除;
软连接相当于硬链接的快捷方式,软链接又叫符号连接;
硬链接只是一个文件名,即目录中的一个条目。
软链接则是一个独立的文件,其内容是另一个文件的路径信息。
不同文件系统之间不可以建立硬链接;
但是可以建立软件连接;
ln:创建硬链接(hard link)
ln -s:创建软连接(symbolic link)
注意:
如果被 unlink/remove 删除的是文件的最后一个硬链接,并且没有进程正打开该文件,
那么该文件在磁盘上的存储区域将被立即标记为自由。
反之,如果有进程正打开该文件,那么该文件在磁盘上的存储区域,
将在所有进程关闭该文件之后被标记为自由。
a -> +-----+
X b -> | ... |
X c -> +-----+
如果被 unlink/remove 删除的是一个软链接文件,
那么仅软链接文件本身被删除,其目标不受影响。
(软链接:本身是个文件,里面存放着硬链接;)
+------+
a -> | ... | // a 是硬链接
+------+
+------+
X b -> | a | // b 和 c 是软链接
+------+
+------+
X c -> | a |
+------+
5. 文件描述符:
非负的整数,表示一个打开的文件。
由系统调用 (open/creat) 返回,被内核空间 (后续系统调用) 引用。
内核缺省为每个进程打开三个文件描述符(类似于句柄):
0 - 标准输入
1 - 标准输出
2 - 标准出错
其他描述符从 3 开始往后定义;
在 unistd.h 中被定义为如下三个宏:
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
范例:redir.c
#include <stdio.h>
int main () {
int data;
fscanf (stdin, "%d", &data);
fprintf (stdout, "%d", data);
fprintf (stderr, "%d", data);
return 0;
}
// stdout:有缓冲区,遇到换行之类的才输出;
// stderr:显示错误,无缓冲区;
下面重定向输出:
# a.out 0<i.txt 1>o.txt 2>e.txt // 一个大于号是覆盖,两个大于号是追加
把标准输入定向到 i.txt 文件里;(注意:输入是小于号,其他是大于号)
把标准输出定向到 o.txt 文件里;
把标准出错定向到 e.txt 文件里;
文件描述符的范围介于 0 到 OPEN_MAX 之间,
传统 Unix 中 OPEN_MAX 宏被定义为 63,现代 Linux 使用更大的上限,一般为 256。
一个进程可以同时打开的文件描述符个数,
受 limits.h 中定义的 OPEN_MAX 宏的限制,
POSIX 要求不低于 16,传统 Unix 是 63,现代 Linux 是 256。
6. open/creat/close
#include <fcntl.h>
int open (
const char* pathname, // 路径
int flags, // 模式
mode_t mode // 权限(仅创建文件有效)
); // 创建/读写文件时都可用此函数
int creat (
const char* pathname, // 路径
mode_t mode // 权限
); // 常用于创建文件
int open (
const char* pathname, // 路径
int flags // 模式
); // 常用于读写文件
成功返回文件描述符,失败返回 -1。
open/creat 所返回的一定是当前未被使用的最小文件描述符。
例如:假如 3 和 4 都被使用了,然后 3 释放了,当再次需要 open/creat 的时候,
是 3 被调用,而不是 5;
creat 函数是通过调用 open 实现的:
int creat (const char* pathname, mode_t mode) {
return open (pathname,
O_WRONLY | O_CREAT | O_TRUNC, mode);
}
flags 为以下值的位或:
常用:
O_RDONLY - 只读 \
|
O_WRONLY - 只写 > 只选一个
|
O_RDWR - 读写 /
O_APPEND - 追加
O_CREAT - 创建,不存在即创建(已存在即直接打开,并保留原内容,除非 . . . ),
有此位 mode 参数才有效。
O_EXCL - 排斥,已存在即失败。 \
> 只选一个,配合 O_CREAT 使用;
O_TRUNC - 清空,已存在即清空 /
(有 O_WRONLY/O_RDWR)。
不常用:
O_NOCTTY - 非控,若 pathname 指向控制终端,则不将该终端作为控制终端。
O_NONBLOCK - 非阻,若 pathname 指向 FIFO/块/字符文件,则该文件的打开及后续操作均为非阻塞模式。
O_SYNC - 同步,write 等待数据和属性,被物理地写入底层硬件后再返回。
O_DSYNC - 数同,write 等待数据,被物理地写入底层硬件后再返回。
O_RSYNC - 读同,read 等待对所访问区域的所有写操作,全部完成后再读取并返回。
O_ASYNC - 异步,当文件描述符可读/写时,向调用进程发送 SIGIO 信号。
#include <unistd.h>
int close (
int fd // 文件描述符
);
成功返回 0,失败返回 -1。
fopen 和 open 之间的等价关系:
r O_RDONLY
r+ O_RDWR
w O_WRONLY | O_CREAT | O_TRUNC, 0666
w+ O_RDWR | O_CREAT | O_TRUNC, 0666
a O_WRONLY | O_CREAT | O_APPEND, 0666
a+ O_RDWR | O_CREAT | O_APPEND, 0666
0666:0 代表八进制;下面三位分别是:属主,同组,其他用户;
6 是由 4 2 1 组成,4 代表可读,2 代表可写,1 代表可执行;
操作系统可通过权限掩码 (当前为 0002,有的是 0022),屏蔽程序所创建文件的某些权限位。如:
$ umask // 查看当前掩码
0002 // 用于屏蔽其他用户的写权限
0666 (-rw-rw-rw-) & ~0002 = 0664 (-rw-rw-r--)
范例:open.c
#include <stdio.h>
#include <fcntl.h>
int main () {
int fd1 = open ("open.txt", O_RDWR | O_CREAT | O_TUNC, 0666); // 可读写,且不存在就创建,存在就清空
if (fd1 == -1) {
perror ("open");
return -1;
}
printf ("%d\n", fd1);
close (fd1);
return 0;
}
这个程序输出后的权限为:-rw-rw-r--
而不是我们期望的:-rw-rw-rw-
原因就是我们上面讲到的系统权限掩码在起作用;自动屏蔽其他用户的写权限;
7. write
#include <unistd.h>
ssize_t write (
int fd, // 文件描述符
const void* buf, // 缓冲区
size_t count // 期望写入的字节数
);
成功返回实际写入的字节数(0 表示未写入,不是失败),失败返回 -1。
size_t: unsigned int,无符号整数
ssize_t: int,有符号整数
范例:write.c
#include <stdio.h>
#include <fcntl.h> // open
#include <string.h> // strlen
#include <unistd.h> // write
int main () {
int fd = open ("write.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fd == -1) {
perror ("open");
return -1;
}
const char* text = "hello word";
printf ("要写入的内容:%s\n", text);
// 要写入的字节数:字符个数乘以字节数,扩大安全性;因为有的机器上一个 char 不一定是一个字节,有可能是 2 字节;
size_t towrite = strlen (text) * sizeof (char);
ssize_t written = write (fd, text, towrite);
if (written == -1) {
perror ("write");
return -1;
}
printf ("期望写入%d字节,实际写入%d字节。\n", towrite, written);
close (fd);
return 0;
}
这个层面上,不分写入的是文本格式还是二进制,都是二进制;
如果遇到实际写入的字节小于期望写入的字节,我们就继续写。
8. read
#include <unistd.h>
ssize_t read (
int fd, // 文件描述符
void* buf, // 缓冲区
size_t count // 期望读取的字节数
);
成功返回实际读取的字节数(返回 0 表示读到文件尾),失败返回 -1。
范例:read.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main () {
int fd = open ("read.txt", O_RDONLY);
if (fd == -1) {
perror ("open");
return -1;
}
char text[256] = {}; // 初始化为 0,让下面的 printf 可以正常输出字符串;
size_t toread = sizeof (text);
ssize_t readed = read (fd, text, toread);
if (-1 == readed) {
perror ("read");
return -1;
}
printf ("期望读取%d字节,实际读取%d字节.\n", toread, readed);
printf ("读取的内容为:%s\n", text);
return 0;
}
练习:二进制读写
范例:binary.c
#include <stdio.h>
#include <fcntl.h>
int main (void) {
int fd = open ("binary.dat", O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fd == -1) {
perror ("open");
return -1;
}
char name[256] = "张飞";
if (write (fd, name, sizeof (name)) == -1) {
perror ("write");
return -1;
}
unsigned int age = 38;
if (write (fd, &age, sizeof (age)) == -1) {
perror ("write");
return -1;
}
double salary = 20000;
if (write (fd, &salary, sizeof (salary)) == -1) {
perror ("write");
return -1;
}
struct Employee {
char name[256];
unsigned int age;
double salary;
} employee = {"赵云", 25, 8000};
// 直接把 employee 写入:
if (write (fd, &employee, sizeof (employee)) == -1) {
perror ("write");
return -1;
}
close (fd);
if ((fd = open ("binary.dat", O_RDONLY)) == -1) {
perror ("open");
return -1;
}
if (read (fd, name, sizeof (name)) == -1) {
perror ("read");
return -1;
}
printf ("姓名:%s\n", name);
if (read (fd, &age, sizeof (age)) == -1) {
perror ("read");
return -1;
}
printf ("年龄:%u\n", age);
if (read (fd, &salary, sizeof (salary)) == -1) {
perror ("read");
return -1;
}
printf ("工资:%.2lf\n", salary);
if (read (fd, &employee, sizeof (employee)) == -1) {
perror ("read");
return -1;
}
printf ("员工:%s %u %.2lf\n", employee.name, employee.age,
employee.salary);
close (fd);
return 0;
}
练习:文本读写
范例:text.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main (void) {
int fd = open ("text.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror ("open");
return -1;
}
char name[256] = "张飞";
unsigned int age = 38;
double salary = 20000;
char buf[1024];
sprintf (buf, "%s %u %.2lf\n", name, age, salary);
if (write (fd, buf, strlen (buf) * sizeof (buf[0])) == -1) {
perror ("write");
return -1;
}
struct Employee {
char name[256];
unsigned int age;
double salary;
} employee = {"赵云", 25, 8000};
// 把 employee 存入 buf 再写入:
sprintf (buf, "%s %u %.2lf", employee.name, employee.age,
employee.salary);
if (write (fd, buf, strlen (buf) * sizeof (buf[0])) == -1) {
perror ("write");
return -1;
}
close (fd);
if ((fd = open ("text.txt", O_RDONLY)) == -1) {
perror ("open");
return -1;
}
memset (buf, 0, sizeof (buf)); // 初始化为 0;
if (read (fd, buf, sizeof (buf)) == -1) {
perror ("read");
return -1;
}
sscanf (buf, "%s%u%lf%s%u%lf", name, &age, &salary,
employee.name, &employee.age, &employee.salary);
printf ("姓名:%s\n", name);
printf ("年龄:%u\n", age);
printf ("工资:%.2lf\n", salary);
printf ("员工:%s %u %.2lf\n", employee.name, employee.age,
employee.salary);
close (fd);
return 0;
}
练习:带覆盖检查的文件复制。
代码:copy.c
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
int main (int argc, char* argv[]) {
if (argc < 3) { // 通过命令行确定源文件和目标文件
fprintf (stderr, "用法:%s <源文件> <目的文件>\n", argv[0]);
return -1;
}
int src = open (argv[1], O_RDONLY); // 打开源文件
if (src == -1) {
perror ("open");
return -1;
}
// 复制的时候,我们不仅需要复制内容,还需要复制状态属性;通过 stat 实现;
struct stat st; // 代表文件的状态;
if (fstat (src, &st) == -1) { // 得到文件状态/属性
perror ("fstat");
return -1;
}
// 以源文件的属性打开目标文件:
int dst = open (argv[2], O_WRONLY | O_CREAT | O_EXCL, st.st_mode);
if (dst == -1) {
if (errno != EEXIST) { // EEXIST 表示文件已存在
perror ("open");
return -1;
}
printf ("文件%s已存在,是否覆盖?(y/n) ", argv[2]);
int ch = getchar ();
if (ch != 'y' && ch != 'Y') // 不覆盖
return 0;
if ((dst = open (argv[2], O_WRONLY | O_CREAT | O_TRUNC, st.st_mode)) == -1) {
perror ("open");
return -1;
}
}
// 读文件
unsigned char buf[1024];
ssize_t bytes;
while ((bytes = read (src, buf, sizeof (buf))) > 0) // 防止文件大于缓冲区,需要多次读取;
if (write (dst, buf, bytes) == -1) { // 如果写入设备特殊,也需要多次写入;
perror ("write");
return -1;
}
if (bytes == -1) {
perror ("read");
return -1;
}
close (dst);
close (src);
return 0;
}
gcc 输入:
$ ./a.out a.out b.out
$ diff a.out b.out // 判断两个文件是否一样
9. 系统 I/O 与标准 I/O
当系统调用函数被执行时,需要切换用户态和内核态,频繁调用会导致性能损失。
标准库做了必要的优化,内部维护一个缓冲区,只在满足特定条件时才将缓冲区与系统内核同步,
借此降低执行系统调用的频率,减少进程在用户态和内核态之间来回切换的次数,提高运行性能。
范例:stdio.c
#include <stdio.h>
int main (void) {
FILE* fp = fopen ("stdio.dat", "wb"); // 二进制方式写入
if (! fp) {
perror ("fopen");
return -1;
}
unsigned int i;
for (i = 0; i < 100000; i++)
fwrite (&i, sizeof (i), 1, fp);
fclose (fp);
return 0;
}
gcc stdio.c -o stdio
范例:sysio.c
#include <stdio.h>
#include <fcntl.h>
int main (void) {
int fd = open ("sysio.dat", O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fd == -1) {
perror ("open");
return -1;
}
unsigned int i;
for (i = 0; i < 100000; i++)
write (fd, &i, sizeof (i));
close (fd);
return 0;
}
gcc sysio.c -o sysio
# time ./sysio
real 0m17.442s
user 0m0.000s
sys 0m0.284s
# time ./stdio
real 0m0.056s
user 0m0.000s
sys 0m0.009s
小结:能用标准库的尽量用标准库,没法用的时候再用系统库;
10. 人为设置文件位置:lseek
每个打开的文件都有一个与其相关的 “文件位置”。
文件位置通常是一个非负整数,用以度量从文件头开始计算的字节数。
读写操作都从当前文件位置开始,并根据所读写的字节数,增加文件位置。
打开一个文件时,除非指定了 O_APPEND,否则文件位置一律被设为 0。
lseek 函数仅将文件位置记录在内核中,并不引发任何 I/O 动作。
如果在文件尾继续把文件指针往后移动,是可以的,那么此时将在文件中形成空洞。
文件空洞不占用磁盘空间,但被算在文件大小内。
lseek (fd, 8, SEEK_END); // 默认移动顺序是从前往后;形成八个字节空洞;
#include <sys/types.h>
#include <unistd.h>
off_t lseek (
int fd, // 文件描述符
off_t offset, // 偏移量
int whence // 起始位置
);
成功返回当前文件位置(相对于文件头 0),失败返回 -1。
whence取值:
SEEK_SET - 从文件头(文件的第一个字节)。
SEEK_CUR - 从当前位置(上一次读写的最后一个字节的下一个位置)。
SEEK_END - 从文件尾(文件的最后一个字节的下一个位置)。
范例:seek.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
int main (void) {
int fd = open ("seek.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror ("open");
return -1;
}
const char* text = "Hello, World !"; // H 位置为 0
if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
// 写完后,当前位置在 ! 后面那个位置
perror ("write");
return -1;
}
// 从当前位置向文件头偏移 7 个位置,也就到了 W 位置;
if (lseek (fd, -7, SEEK_CUR) == -1) { // 返回当前位置,所以不可能返回 -1
perror ("lseek");
return -1;
}
off_t pos = lseek (fd, 0, SEEK_CUR); // off_t 为常整型
if (pos == -1) {
perror ("lseek");
return -1;
}
printf ("当前文件位置:%ld\n", pos); // 输出 7
// 起初文件头为 0,从 H 开始,移动后到 W 位置,所以输出为 7
text = "Linux";
if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
// 再次写入就会覆盖 W 以后 5 个位置,输出:Hello Linux !
perror ("write");
return -1;
}
// 文件动效果:从文件尾往后偏移 8,超出文件长度,形成 8 个字节的洞
if (lseek (fd, 8, SEEK_END) == -1) {
perror ("lseek");
return -1;
}
text = "<-这里有个洞洞!";
if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
// 此时 seek.txt 文件内容为:Hello Linux !xxxxxxxx<-这里有个洞洞!
perror ("write");
return -1;
}
// 求文件大小
off_t size = lseek (fd, 0, SEEK_END);
if (size == -1) {
perror ("lseek");
return -1;
}
printf ("文件大小:%d字节\n", size);
close (fd);
return 0;
}
思考:既然 lseek 系统调用相当于标 C 库函数 fseek,
那么是否存在与标 C 库函数 ftell 相对应的系统调用?
答:不存在,因为通过 lseek(fd, 0, SEEK_CUR) 就可以获得当前文件位置,不需要 ftell 功能。
思考:如何获取文件的大小?
答:通过 lseek(fd, 0, SEEK_END) 可以获得文件的大小。
11. 打开文件的内核数据结构
通过 ls -i 可查看文件的 i 节点号。
i 节点记录了文件的属性和数据在磁盘上的存储位置。
目录也是文件,存放路径和 i 节点号的映射表。
图示:open.bmp
范例:bad.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main (void) {
const char* text = "Hello, World !";
/* 如果未打开就直接写入,而且描述符 3 是无效的,那么下面这段代码会出错;
if (write (3, text, strlen (text)) == -1) {
// 如果描述符为 1,则正常输出,会在屏幕上输出:Hello, World !
perror ("write");
return -1;
}
*/
/* 如果关闭标准输出,那么 printf 这段代码无法输出;
close (1);
printf ("%s\n", text);
*/
if (close (STDOUT_FILENO) == -1) { // STDOUT_FILENO 相当于 1
perror ("close");
return -1;
}
// 输出重定向,先关闭 1,那么调用 creat 的时候,就把标准输出 1 定向到 bad.txt 文件里;
close (1);
if (creat ("bad.txt", 0644) == -1) {
perror ("creat");
return -1;
}
printf ("%s", text); // printf 原来输出到屏幕(1 对应屏幕),现在输出到 bad.txt 文件里;
return 0;
}
相当于:
$ echo hello, word ! 1>bad.txt
$ echo hello, word >bad.txt // 如果不写 1 就不能写感叹号,bashell 不识别;
12. 复制一个已打开的文件描述符 dup/dup2
#include <unistd.h>
int dup (int oldfd);
int dup2 (int oldfd, int newfd);
成功返回文件描述符 oldfd 的副本,失败返回 -1。
返回的一定是当前未被使用的最小文件描述符。
dup2 可由第二个参数指定描述符的值。
若指定描述符已打开,则先关闭之。
所返回的文件描述符副本,与源文件描述符,对应同一个文件表。
注意区分通过 dup 获得的文件描述符副本,
和两次 open 同一个文件的区别:
dup 只复制文件描述符,不复制文件表:
fd1 \
> 文件表 -> v节点 -> i节点
fd2 /
open 创建新文件表,并为其分配新文件描述符,出现两个文件表:
fd1 -> 文件表1 \
> v节点 -> i节点
fd2 -> 文件表2 /
图示:same.bmp
图示:dup.bmp
范例:dup.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#inlcude <unistd.h>
int main (void) {
int fd1 = open ("dup.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd1 == -1) {
perror ("open");
return -1;
}
printf ("fd1 = %d\n", fd1); // 3
int fd2 = dup (fd1);
if (fd2 == -1) {
perror ("dup");
return -1;
}
printf ("fd2 = %d\n", fd2); // 4
int fd3 = dup2 (fd2, 100); // 定义 fd3 的文件描述符为 100
if (fd3 == -1) {
perror ("dup2");
return -1;
}
printf ("fd3 = %d\n", fd3); // 100
// 通过 fd1 写文件
const char* text = "Hello, World !";
if (write (fd1, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
// 通过 fd2 偏移
if (lseek (fd2, -7, SEEK_END) == -1) {
perror ("lseek");
return -1;
}
// 通过 fd3 更改
text = "Linux";
if (write (fd3, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
close (fd3);
close (fd2);
close (fd1);
return 0;
}
输出:Hello, Linux !
dup 一般用于多线程编程里;当两个线程对同一个文件写操作;
如果用 open,那么两个线程写入的东西会产生覆盖;用 dup 用同一个文件表就不会出现这种情况;
范例:same.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main (void) {
int fd1 = open ("same.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd1 == -1) {
perror ("open");
return -1;
}
printf ("fd1 = %d\n", fd1);
int fd2 = open ("same.txt", O_RDWR);
if (fd2 == -1) {
perror ("open");
return -1;
}
printf ("fd2 = %d\n", fd2);
int fd3 = open ("same.txt", O_RDWR);
if (fd3 == -1) {
perror ("open");
return -1;
}
printf ("fd3 = %d\n", fd3);
const char* text = "Hello, World !";
if (write (fd1, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
if (lseek (fd2, -7, SEEK_END) == -1) {
perror ("lseek");
return -1;
}
text = "Linux";
if (write (fd3, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
close (fd3);
close (fd2);
close (fd1);
return 0;
}
输出:Linux, World !
13. 作业:学生管理系统登录模块。
注册 - 增加用户名和密码,
登录 - 验证用户名和密码,
用户信息保存在文件中。
参见项目:mis