linux下编写打印文件的函数,Linux系统编程笔记-文件IO

本文主要介绍了如下内容:

C标准库函数与系统函数的关系

进程控制块

文件描述符

系统调用:open、close、read、write、lseek、fcntl和ioctl

先导概念

C标准库函数与系统函数的关系

API层次如图所示:

2ecb6eb98778

API层次

API调用顺序

由上往下(用户态 -> 内核态)的顺序依次是:

C标准库函数:调用系统库函数(即 系统调用);

系统调用:即操作系统的应用层API,调用内核层API;

内核层API: 调用具体的驱动层API(在Linux中一般以sys_开头);

驱动层函数:直接控制硬件设备。

以调用fwrite()函数将文件内容显示在终端为例,fwrite()函数将调用write()系统调用,而write()系统调用的实现则是调用内核态的sys_write()函数,由sys_write()来判断具体调用哪个驱动函数来访问硬件设备。

当然,对于Linux操作系统而言,还多了一层VFS(virtual File System,虚拟文件系统)层:

2ecb6eb98778

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 };

关系图:

2ecb6eb98778

文件句柄关系

文件‘开’ ‘关’ ‘读’ ‘写’的系统接口

open()

功能:打开或者创建(如果文件不存在)一个文件。

每打开一个文件,操作系统内核(kernel)就会在内存中新建一个files_struct结构体。

在同一个进程中 多次打开同一个文件,内核也会在内存中分别新建不同的files_struct结构体(由不同的文件描述符映射)。因此,每次打开的文件在使用完之后一定要及时关闭,否则可能会引起内存泄漏。

声明:

NAME

open - open and possibly create a file

SYNOPSIS

#include

#include

#include

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

int close(int fd);

返回值:

成功:返回0;

出错:则返回-1,并设置errno;

read()

功能: 从打开的设备或文件中读取数据。

声明:

NAME

read - read from a file descriptor

SYNOPSIS

#include

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

ssize_t write(int fd, const void *buf, size_t count);

返回值:

成功:返回写入的字节数;

出错:返回-1,并设置errno。

注意:

在向常规文件进行写操作时,write函数的返回值通常等于请求写的字节数count,而向终端设备或网络设备进行写操作时则不一定。

Demo:mycp.c

程序功能描述:模仿cp命令,将一个文件中的内容复制到一个新的文件之中。

code:

#include

#include

#include

#include

#include

#include

#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

#include

off_t lseek(int fd, off_t offset, int whence);

lseek的两个"副作用"示例

demo1. 扩展一个文件

注意:

拓展一个文件,一定要有一个写操作。

code:extend_file.c

#include

#include

#include

#include

#include

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

>

void perror(const char *s);

>

#include

>

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

#include

#include

#include

#include

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

#include

int fcntl(int fd, int cmd, ... /* arg */ );

demo:

改变文件的状态标志位为非阻塞状态

code: test_fcntl.c

#include

#include

#include

#include

#include

#include

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

int ioctl(int fd, unsigned long request, ...);

fd是某个设备的文件描述符,request是ioctl的命令,可变参数取决于request,通常是一个指向变量或结构体的指针。

若出错,则返回-1;若成功,则返回其他值。返回值也取决于request。

demo: 获取终端窗口的大小

code: get_tty_size.c

#include

#include

#include

#include

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$

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值