本文主要介绍了如下内容:
- C标准库函数与系统函数的关系
- 进程控制块
- 文件描述符
- 系统调用:open、close、read、write、lseek、fcntl和ioctl
先导概念
C标准库函数与系统函数的关系
API层次如图所示:
API调用顺序
由上往下(用户态 -> 内核态)的顺序依次是:
- C标准库函数:调用系统库函数(即 系统调用);
- 系统调用:即操作系统的应用层API,调用内核层API;
- 内核层API: 调用具体的驱动层API(在Linux中一般以
sys_
开头); - 驱动层函数:直接控制硬件设备。
以调用fwrite()函数将文件内容显示在终端为例,fwrite()函数将调用write()系统调用,而write()系统调用的实现则是调用内核态的sys_write()函数,由sys_write()来判断具体调用哪个驱动函数来访问硬件设备。
当然,对于Linux操作系统而言,还多了一层VFS(virtual File System,虚拟文件系统)层:
write()系统调用将来自用户空间的数据流,首先通过VFS的通用系统调用,然后通过文件系统的特殊写法,最后写入物理介质中。
各API在缓冲区上的不同之处
- fopen():每打开一个文件,都会对应一个单独的缓冲区;
- open():无缓冲区;
- sys_open:有缓冲区,但是由所有打开的文件共用。
关于缓冲区的刷新方式:
- 刷新C标准缓冲区
- 缓冲区满,自动刷新;
- 手动调用fflush()函数刷新;
- 使用fclose()函数关闭文件时刷新;
- 程序正常结束后缓冲区自动刷新。
- 刷新内核缓冲区
由一个守护进程定时刷新。
PCB和文件描述符fd
PCB(process control block,进程控制块)在Linux源码中的实现即task_struct
结构体,位于/include/linux/sched.h
文件中。该结构体在Linux中被称为进程描述符(process descriptor)。 其部分结构如下(linux kernel 版本为4.4.36):
1380 struct task_struct {
1381 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
1382 void *stack;
1383 atomic_t usage;
1384 unsigned int flags; /* per process flags, defined below */
1385 unsigned int ptrace;
1386
1387 #ifdef CONFIG_SMP
1388 struct llist_node wake_entry;
1389 int on_cpu;
1390 unsigned int wakee_flips;
1391 unsigned long wakee_flip_decay_ts;
1392 struct task_struct *last_wakee;
1393
1394 int wake_cpu;
1395 #endif
1396 int on_rq;
1483 pid_t pid;
1484 pid_t tgid;
Linux内核把进程的列表存放在任务队列(task list)中,该队列是一个双向循环链表,链表中的每一项都是一个task_struct
结构体。
在Linux内核中,每一个进程都有一个PCB来管理,每一个PCB中都有一个指向files_struct
结构体的指针:
1564 /* open file information */
1565 struct files_struct *files;
可以看到,task_struct
结构体中的files
是个指针(充当目录项的角色),指向files_struct
结构体。而files_struct
结构体是一张文件描述符表(实际上就是一个整形数组,里面存放的是诸如0
、 1
、 2
这样的文件描述符,文件描述符即一些非负整数),这些文件描述符指向真正的设备文件,包括磁盘文件、显示屏文件等所有文件。
文件描述符struct files_struct
源码:
(位于linux-4.4.36/include/linux/fdtable.h
中)
43 /*
44 * Open file table structure
45 */
46 struct files_struct {
47 /*
48 * read mostly part
49 */
50 atomic_t count; /* 该结构体的引用计数 */
51 bool resize_in_progress;
52 wait_queue_head_t resize_wait;
53
54 struct fdtable __rcu *fdt;
55 struct fdtable fdtab;
56 /*
57 * written part on a separate cache line in SMP
58 */
59 spinlock_t file_lock ____cacheline_aligned_in_smp;
60 int next_fd;
61 unsigned long close_on_exec_init[1];
62 unsigned long open_fds_init[1];
63 unsigned long full_fds_bits_init[1];
64 struct file __rcu * fd_array[NR_OPEN_DEFAULT]; /* 缺省的文件对象数组 */
65 };
关系图:
文件‘开’ ‘关’ ‘读’ ‘写’的系统接口
open()
功能:打开或者创建(如果文件不存在)一个文件。
每打开一个文件,操作系统内核(kernel)就会在内存中新建一个
files_struct
结构体。在同一个进程中 多次打开同一个文件,内核也会在内存中分别新建不同的
files_struct
结构体(由不同的文件描述符映射)。因此,每次打开的文件在使用完之后一定要及时关闭,否则可能会引起内存泄漏。
声明:
NAME
open - open and possibly create a file
SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
返回值:
- 成功:返回新分配的文件描述符;
- 出错:则返回-1,并设置errno;
close()
功能:关闭一个打开的文件,一般与open()成对使用。
每调用一次
close(fd)
,实际上是将该文件描述符fd所指向的files_struct
结构体中的引用计数count
值减一。当引用计数值减为0时,操作系统内核(kernel)才真正关闭该文件。通过调用dup/dup2系统调用可使
files_struct
结构体中的引用计数count
值加一。具体是dup/dup2新生成一个文件描述符newfd
,并使其指向旧文件描述符oldfd
所指向的files_struct
结构体,即这两个文件描述符共用一个files_struct
结构体。
声明:
NAME
close - close a file descriptor
SYNOPSIS
#include <unistd.h>
int close(int fd);
返回值:
- 成功:返回0;
- 出错:则返回-1,并设置errno;
read()
功能: 从打开的设备或文件中读取数据。
声明:
NAME
read - read from a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:
- 成功:返回读取的字节数;
- 出错:则返回-1,并设置errno;
- 如果在调read之前已到达文件末尾,则这次read返回0。
write()
功能:从内存地址buf
开始,向打开的文件写入count
字节(byte)的数据。
声明:
NAME
write - write to a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回值:
- 成功:返回写入的字节数;
- 出错:返回-1,并设置errno。
注意:
在向常规文件进行写操作时,write函数的返回值通常等于请求写的字节数count
,而向终端设备或网络设备进行写操作时则不一定。
Demo:mycp.c
程序功能描述:模仿cp命令,将一个文件中的内容复制到一个新的文件之中。
code:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define SIZE 8192
int main(int argc, char *argv[])
{
int fd_src, fd_des, len;
char buf[SIZE];
/* 参数输入太少,不符合要求,打印命令使用提示信息并退出 */
if (argc < 3) {
printf("Usage: ./mycp src_file des_file\n");
exit(1);
}
/* 打开源文件 */
fd_src = open(argv[1], O_RDONLY);
if (fd_src == -1) {
printf("Openning file %s failed...\n", argv[1]);
exit(-1);
}
/* 新建目标文件 */
fd_des = open(argv[2], O_CREAT | O_WRONLY | O_TRUNC, 0664);
if (fd_des == -1) {
printf("Creating file %s failed...\n", argv[2]);
exit(-1);
}
/* 读取源文件中的内容,然后写入目标文件之中 */
while ( (len = read(fd_src, buf, sizeof(buf))) > 0 ) {
write(fd_des, buf, len);
}
/* 关闭文件 */
close(fd_src);
close(fd_des);
return 0;
}
test
slot@slot-ubt:~/test$ gcc mycp.c -o mycp
slot@slot-ubt:~/test$ cat aa
Hello, this is my cp cmd.
Welcome to use...
slot@slot-ubt:~/test$ cat bb
cat: bb: No such file or directory
slot@slot-ubt:~/test$ ./mycp aa bb
slot@slot-ubt:~/test$ cat bb
Hello, this is my cp cmd.
Welcome to use...
slot@slot-ubt:~/test$
lseek()
功能:移动打开的文件的读写指针的位置。
每个打开的文件都记录着当前读写指针的位置,打开文件时读写位置是0,表示文件开头。通常,读写多少个字节,就会将读写位置往后移动多少个字节。但有一个例外,如果以
O_APPEND(追加)
方式打开,则每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。
声明:
NAME
lseek - reposition read/write file offset
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
lseek的两个"副作用"示例
demo1. 扩展一个文件
注意:
拓展一个文件,一定要有一个写操作。
code:extend_file.c
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
int fd;
/* 新建一个名为abc的文件 */
fd = open("abc", O_CREAT | O_RDWR);
if (fd < 0) {
perror("Opening file failed: ");
exit(-1);
}
/* 将读写指针移到文件末尾 */
lseek(fd, 0x1000, SEEK_SET);
/* 追加写一个字节到文件中去
* string "a" will be translated to an addr
* od -tcx see file abc
*/
write(fd, "a", 1);
close(fd);
return 0;
}
errno
是个用户态的全局变量,声明在头文件/usr/include/errno.h
中 :
45 #ifndef errno
46 extern int errno;
47 #endif
Linux下的错误码可以查阅文件:
/usr/include/asm-generic/errno-base.h
(部分展示如下):
1 #ifndef _ASM_GENERIC_ERRNO_BASE_H
2 #define _ASM_GENERIC_ERRNO_BASE_H
3
4 #define EPERM 1 /* Operation not permitted */
5 #define ENOENT 2 /* No such file or directory */
6 #define ESRCH 3 /* No such process */
7 #define EINTR 4 /* Interrupted system call */
8 #define EIO 5 /* I/O error */
9 #define ENXIO 6 /* No such device or address */
10 #define E2BIG 7 /* Argument list too long */
11 #define ENOEXEC 8 /* Exec format error */
12 #define EBADF 9 /* Bad file number */
13 #define ECHILD 10 /* No child processes */
14 #define EAGAIN 11 /* Try again */
15 #define ENOMEM 12 /* Out of memory */
16 #define EACCES 13 /* Permission denied */
perror()函数将打印用户自定义信息和
errno
后面对应的注释信息,其声明为:
NAME
perror - print a system error message
>
SYNOPSIS
#include <stdio.h>
>
void perror(const char *s);
>
#include <errno.h>
>
const char * const sys_errlist[];
int sys_nerr;
int errno;
test:
slot@slot-ubt:~/test$ gcc extend_file.c -o exf
slot@slot-ubt:~/test$ ./exf
slot@slot-ubt:~/test$ od -txc abc
0000000 00000000 00000000 00000000 00000000
\0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
*
0010000 00000061
a
0010001
demo2. 获取文件的大小
方法:将指针移到文件末尾,然后输出返回值,该值即文件大小。
code: see_file_size.c
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
int fd;
fd = open("abc", O_RDWR);
if (fd < 0) {
perror("Opening file failed: ");
exit(-1);
}
/* print file size */
printf("abc size is: %lld\n", lseek(fd, 0, SEEK_END));
close(fd);
return 0;
}
test:
slot@slot-ubt:~/test$ gcc see_file_size.c -o fsize
slot@slot-ubt:~/test$ ./fsize
abc size is: 4097
slot@slot-ubt:~/test$ ls -l abc
-rwxrwxrwx 1 slot staff 4097 12 14 19:40 abc
fcntl()
功能: 获取或者设置已打开文件的访问属性。
声明:
NAME
fcntl - manipulate file descriptor
SYNOPSIS
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
demo:
改变文件的状态标志位为非阻塞状态
code: test_fcntl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int main()
{
char buf[10];
int n;
int flags;
/* get file flag */
flags = fcntl(STDIN_FILENO, F_GETFL);
/* change file flags to nonblock */
flags |= O_NONBLOCK;
if (fcntl(STDIN_FILENO, F_SETTL, flags) == -1) {
perror("change file flag failed: ");
exit(1);
}
try_again:
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
if (errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto try_again;
}
perror("read stdin failed: ");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
test:
ioctl()
功能:向设备发送控制和配置命令。
有些命令也需要读写一些数据,但这些数据是不能用read/write读写的,称为
Out-of-band
数据。也就是说,read/write读写的数据是in-band
数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数据。
例如,在串口线上收发数据通过read/write操作,而串口的波特率、校验位、停止位则通过ioctl来设置;A/D转换(模数转换)的结果通过read读取,而A/D转换的精度和工作频率则通过ioctl设置。
声明:
NAME
ioctl - control device
SYNOPSIS
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
fd
是某个设备的文件描述符,request
是ioctl的命令,可变参数取决于request
,通常是一个指向变量或结构体的指针。
若出错,则返回-1;若成功,则返回其他值。返回值也取决于
request
。
demo: 获取终端窗口的大小
code: get_tty_size.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(void)
{
struct winsize size;
/* 不是终端设备文件则退出 */
if (isatty(STDOUT_FILENO) == 0)
exit(1);
/* 通过 ioctl() 获取终端窗口的大小 */
if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0) {
perror("ioctl TIOCGWINSZ error");
exit(1);
}
/* 打印终端窗口的长和宽 */
printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
return 0;
}
test:
(测试结果依赖于当前终端的窗口大小)
slot@slot-ubt:~/test$ gcc get_tty_size.c -o ttysize
slot@slot-ubt:~/test$
slot@slot-ubt:~/test$ ./ttysize
24 rows, 65 columns
slot@slot-ubt:~/test$