前言
linux系统下一切皆文件,包括对硬件设备的操作本质上也是对文件的操作,要学习linux开发,那么理解文件IO的操作是最基本的,也是格外重要的。此篇文章记录平时开发中的一些有用没用的细节知识点,规范平时的软件开发。
1. 系统调用(system call)
linux内核(运行在内核态)提供了一系列的服务、功能以及硬件资源等,为了维护内核的稳定和安全,不允许linux 应用程序(运行在用户态)直接访问、操作linux内核资源,也就是说用户态无法直接访问内核态,应用程序如果需要访问内核要怎么办呢?这时候“中间商”系统调用便横空出世,linux内核中有一组用于实现系统功能的子程序,称为系统调用。
除异常和中断外,系统调用是访问内核的唯一合法入口,通常系统调用都是通过软中断实现的,它为用户空间提供了统一的硬件的抽象接口,也维护了内核的稳定安全。
2 .标准C库函数
linux系统中,使用的标准C语言函数库叫GNU C函数库(也叫glibc),它和应用程序一样运行在用户空间。库函数大部分是由系统调用封装而来的(例如fopen调用open),也有部分函数不需要经过系统调用(例如strlen()字符串处理函数)。标准库的路径一般在/lib 和 /usr/lib目录下。
库函数相比系统调用API有更好的移植性;库函数通常是有缓存的,而系统调用是无缓存的,所以在性能、效率上,库函数通常要优于系统调用。
linux中 库函数在应用程序中存在的形式有:
动态库(lib前缀;.so后缀):程序在运行的时候才会把库函数链接进来,可执行文件较小。
静态库(lib前缀;.a后缀) :程序编译的时候库函数直接链接在可执行文件中,可执行文件会较大。
2.1 静态库的制作与使用
2.1.1 静态库制作
① 先将库函数源文件(即 .c文件),编译成 .o文件,可以同时编译多个源文件。
命令格式:gcc -c 源文件名
② 使用①生成的 .o文件生成 .a库文件
命令格式:ar rsc lib库名.a 源文件名.o
/******** mylib.h *********/
ifndef _MYLIB_H_
#define _MYLIB_H_
void print();
void print2();
#endif
/******** print.c *********/
#include <stdio.h>
#include "mylib.h"
void print()
{
printf("hello qurry.\n");
}
/******** print2.c *********/
#include <stdio.h>
#include "mylib.h"
void print2()
{
printf("hello qurry2.\n");
}
/******** app.c *********/
#include <stdio.h>
#include "mylib.h"
void main()
{
print();
print2();
}
2.1.2 静态库使用
① 使用命令:gcc -o 可执行程序名 应用程序名.c -L 库文件所在路径 -l 库名
-L : 指定库文件所在路径 (相对或绝对路径)
-l :指定库名(库名是libxxx.a中的xxx,去掉前缀lib和后缀 .a)
② ./ 运行可执行文件。
2.2 动态库制作和使用
2.2.1 动态库制作
命令格式:gcc -shared -fpic 源文件名.c -o lib库名.so
2.2.2 动态库使用
有如下两种情况:
① 将生成的动态库文件拷贝到/usr/lib/目录下,因为系统默认从这里寻找动态库文件的。
使用命令:gcc -o 可执行程序名 应用程序名.c -l 库名
② 配置系统环境变量,命令为:export LD_LIBRARY_PATH=. ( . 表示当前目录)。
系统就会优先到当前目录下查找动态库文件,找不到才去默认路径找。
配置好后,使用命令:gcc -o 可执行程序名 应用程序名.c -L 库文件所在路径 -l 库名
3. main函数传参
通常main函数有两种写法:
① 无参main:void main(void)
② 传参main:void main(int argc, char *argv[ ])
-argc :表示传递的参数个数;
-argv:表示参数具体值。
举例说明:如下可以看出执行目录和可执行文件名算做第一个参数,每个参数用空格字符间隔开。传参法可以使我们的程序灵活度更高。
/*********** main.c *************
#include <stdio.h>
void main(int argc,char *argv[])
{
printf("argc = %d\n",argc);
for(int i = 0; i < argc; i++)
{
printf("argv[%d] = %s\n",i,argv[i]);
}
}
4. #和##运算符应用
经常在define宏定义的应用中会用到#和##,有些时候看着会感觉满脸懵,这是啥意思呢?
#:是一种运算符,用于带参宏的文本替换,将跟在后面的参数转成一个字符串常量;简单来说,就是在对它所引用的宏变量通过替换后,在其左右各加上一个双引号。
##:一种运算符,将两个运算对象连接在一起,只能出现在带参宏定义的文本替换中;(可以作为宏定义中的变量替换)。
举例说明:这里定义了两个宏,分别为#define func1(a) printf(#a"\n")和#define func2(a,b) func##a(b) 。
func1是#运算符的应用,func1(hello)会被替换成printf("hello\n"),因为printf(#hello"\n")中#hello先被转换为“hello”字符串常量,另外相邻且未被任何字符隔开的两个字符串会被合并在一起,即“hello\n”。
func1是##运算符的应用,func2(1,qurry)先转换为func1(qurry),再转换为printf("qurry\n"),因为func##1结合变成func1,func1与(qurry)结合为func1(qurry),最终体不就相当于func1(a)的宏定义运算嘛。
/*********** main.c ****************/
#include <stdio.h>
#define func1(a) printf(#a"\n")
#define func2(a,b) func##a(b)
void main(int argc,char *argv[])
{
func1(hello);
func2(1,qurry);
}
5. linux man命令
linux系统中,man命令就相当于一个帮助命令,但凡你有哪个命令或者API接口不懂的,都可以使用它来查找帮助,按 q 键退出帮助界面。
命令格式:man (选项) (参数) 括号表示可选项。
选项:
-a:在所有的man帮助手册中搜索;
-f:等价于whatis指令,显示给定关键字的简短描述信息;
-P:指定内容时使用分页程序;
-M:指定man手册搜索的路径。
数字:指定从哪本man手册中搜索帮助;
1:用户在shell环境可操作的命令或执行文件;
2:系统内核可调用的函数与工具等
3:一些常用的函数(function)与函数库(library),大部分为C的函数库(libc)
4:设备文件说明,通常在/dev下的文件
5:配置文件或某些文件格式
6:游戏(games)
7:惯例与协议等,如Linux文件系统,网络协议,ASCII code等说明
8:系统管理员可用的管理命令
9:跟kernel有关的文件。
关键字:指定要搜索帮助的关键字,例如 ls open等
例如:输入命令:man 1 ls 显示如下:
输入命令查找系统调用API接口:man 2 open 显示如下:
6. linux系统中文件管理
文件没有被调用运行的时候是被存储在磁盘或其它外部存储设备中的,此时的文件叫做静态文件。
磁盘的最小存储单位叫做扇区(Sector),512个字节为一个扇区,通常连续8个扇区(占4k个字节)组成一个块(block)。
为了提高效率,系统在磁盘中读取文件是以块为单位读取的,所以块是文件存取的最小单位。
静态文件存储在磁盘中,系统是如何读取到对应文件的呢?磁盘分为两个区:一个为data区(存储文件数据),一个为inode区(存放 inode table, inode table下有众多inode,每个文件都有唯一的inode,每个inode中记录着文件的各种信息,如文件大小、类型、权限等等)。整个查找过程是系统找到这个文件名所对应的inode编号,通过inode编号从inode table中找到对应的inode结构体,最后根据inode结构体中记录的信息,确定文件数据所在的块,并读出数据。linux系统下可以使用命令:ls -i 查看文件inode编号。应用开发可以不去深究inode结构体。
调用open函数去打开文件,内核会申请一段内存,将磁盘中的文件读取到内存中进行管理,文件在内存中存在的形式叫做动态文件;磁盘、U盘等块设备有读写限制,就算只有一个字节的改动也要把对应的块读取出来,修改完再存入到磁盘中;而内存就没有这样的限制,可以以字节为单位的修改任意地址的数据(比如,我们平时修改文档等,就是把文件从磁盘中读取到内存,然后进行修改,修改完再保存到磁盘中,如果没有保存就退出,意味着你文档在内存中的修改没有写回到磁盘中,导致修改无效)。
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;
loff_t i_size;
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
unsigned int i_blkbits;
enum rw_hint i_write_hint;
blkcnt_t i_blocks;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
unsigned long i_state;
struct mutex i_mutex;
unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned long dirtied_time_when;
struct hlist_node i_hash;
struct list_head i_io_list; /* backing dev IO list */
#ifdef CONFIG_CGROUP_WRITEBACK
struct bdi_writeback *i_wb; /* the associated cgroup wb */
/* foreign inode detection, see wbc_detach_inode() */
int i_wb_frn_winner;
u16 i_wb_frn_avg_time;
u16 i_wb_frn_history;
#endif
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
u64 i_version;
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
#ifdef CONFIG_IMA
atomic_t i_readcount; /* struct files open RO */
#endif
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
char *i_link;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct hlist_head i_fsnotify_marks;
#endif
void *i_private; /* fs or device private pointer */
};
7. 文件IO
7.1 常用函数汇总
7.1.1 open函数
头文件:
#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);
返回值:
成功:返回非负值整数(>=0)
错误:返回 -1。
都直到C语言是不支持重载的,为什么open函数还有两个呢?因为那是可变参,mode参数可以省略。调用open函数成功会返回一个非负值的文件描述符,所有打开的文件都会通过文件描述符进行索引,它用于指代被打开的文件,后面所有执行文件IO操作的函数(read()、write()等)都需要用到这个文件描述符。
可以多次打开同一个文件,获得不同的文件描述符,它是分别写的,不是接续写(接续写意思就是上个文件描述符写操作完后,下个文件描述符接着在写完的末尾接着进行写操作)。
linux中一个进程可以打开的最大文件数默认为1024个(查看命令:ulimit -n),超过这个数就会报错退出进程。
7.1.2 close函数
close函数用于将打开的文件关闭。
头文件
#include <unistd.h>
函数原型:
int close(int fd); //fd为文件描述符
返回值:
成功:返回0;
错误:返回-1。
7.1.3 write函数
write函数用于向打开的文件中写入数据。
头文件:
#include <unistd.h>
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
返回值:
成功:返回写入的字节数;
错误:返回-1。
7.1.4 read函数
read函数用于向打开的文件读取数据。
头文件:
#include <unistd.h>
函数原型:
ssize_t read(int fd, void *buf, size_t count);
返回值:
大于0, 表示读取到的字节数;
等于0, 表示在阻塞模式下,到达文件末尾或没有数据可读(EOF),并进入堵塞;
等于-1, 表示出错,在非阻塞模式下表示没有数据可读。
7.1.5 lseek函数
打开的文件都有一个当前文件偏移量(cfo),read、write操作就从这个偏移量开始,使用lseek函数可以改变cfo的大小。
头文件:
#include <sys/types.h>
#include <unistd.h>
函数原型:
off_t lseek(int fd, off_t offset, int whence);
参数:
-offset : 以字节为单位的偏移量,负值表示向前移动,正值表示向后移动。
-whence:作为offset偏移量移动的参考基点,值可以为:
-SEEK_SET :相对于文件开头;
-SEEK_CUR:相对于当前的文件读写指针位置;
-SEEK_END:相对于文件末尾;返回值:
成功:返回从文件头部开始算起的位置偏移量大小;
错误:返回 -1。
示例如下:
lseek(fd,50,SEEK_SET) :把文件当前偏移量设置为文件开头50字节的位置;
lseek(fd,0,SEEK_CUR) :把文件当前偏移量设置为当前位置;
lseek(fd,0,SEEK_END) :把文件当前偏移量设置为文件末尾。
7.1.6 dup函数
dup函数用于对文件描述符的复制,成功返回新的文件描述符,新的文件描述符和旧的文件描述符拥有相同的权限,都可以对文件进行IO操作。
这个函数用的比较少。
头文件:
#include <unistd.h>
函数原型:
int dup(int oldfd);
返回值:
成功:返回一个新的文件描述符;
错误:返回 -1。
7.1.7 ioctl函数
ioctl函数一般用于对硬件外设的操作。
头文件:
#include <sys/ioctl.h>
函数原型:
int ioctl(int fd, unsigned long request, ...); //可变参,根据第二个参数来确定。
返回值:
成功:返回0;
错误:返回 -1。
7.1.8 截断函数
使用系统调用API接口 truncate()或 ftruncate()可将普通文件截断为指定字节长度的文件。
如果参数 length的值小于文件总长度,则将多余的数据丢掉;
如果参数 length的值大于文件总长度,则在扩展部分填充若干 ‘\0’ 字符。
头文件:
#include <unistd.h>
#include <sys/types.h>
函数原型:
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
返回值
成功:返回0;
错误:返回 -1。
7.2 空洞文件
上面介绍了lseek函数,它可以改变文件位置偏移量,并且还允许设置文件偏移量超出文件长度。
例如文件总长度为2048个字节,调用lseek函数将当前偏移量设置为文件开头起4096字节的位置,那么2048到4096中间的这些空间就是空的,什么数据都没有,那么这部分区域就被称为文件空洞,该文件也被称为空洞文件。
文件空洞部分实际上并不会占用任何物理内存,但是逻辑上该文件的大小是包含了空洞部分的大小的。对于空洞文件有个很形象的例子:Windows下载文件时,文件还没有下载完成就可以看到该文件占据了整个文件大小的空间,这个还未下载完成的文件就是空洞文件,里面有些数据还未被下载。
8. 函数返回错误值处理
Linux系统中,对常见的错误做了一个编号,每一个编号都代表着不同的错误类型,当函数执行发生错误的时候,操作系统会将这个错误所对应的编号赋值给errno变量(进程维护的一个全局变量),并不是执行所有的系统调用或C库函数出错时,操作系统都会设置errno,程序当中包含<errno.h>头文件即可。
/************ /usr/include/asm-generic/errno-base.h **************/
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */
/************ /usr/include/asm-generic/errno.h **************/
#define EDEADLK 35 /* Resource deadlock would occur */
#define ENAMETOOLONG 36 /* File name too long */
#define ENOLCK 37 /* No record locks available */
#define ENOSYS 38 /* Invalid system call number */
#define ENOTEMPTY 39 /* Directory not empty */
#define ELOOP 40 /* Too many symbolic links encountered */
#define EWOULDBLOCK EAGAIN /* Operation would block */
#define ENOMSG 42 /* No message of desired type */
#define EIDRM 43 /* Identifier removed */
#define ECHRNG 44 /* Channel number out of range */
#define EL2NSYNC 45 /* Level 2 not synchronized */
#define EL3HLT 46 /* Level 3 halted */
#define EL3RST 47 /* Level 3 reset */
#define ELNRNG 48 /* Link number out of range */
#define EUNATCH 49 /* Protocol driver not attached */
#define ENOCSI 50 /* No CSI structure available */
#define EL2HLT 51 /* Level 2 halted */
#define EBADE 52 /* Invalid exchange */
#define EBADR 53 /* Invalid request descriptor */
#define EXFULL 54 /* Exchange full */
#define ENOANO 55 /* No anode */
#define EBADRQC 56 /* Invalid request code */
#define EBADSLT 57 /* Invalid slot */
.........
#define EOWNERDEAD 130 /* Owner died */
#define ENOTRECOVERABLE 131 /* State not recoverable */
#define ERFKILL 132 /* Operation not possible due to RF-kill */
#define EHWPOISON 133 /* Memory page has hardware error */
8.1 strerror函数
C库函数strerror函数可以将对应的errno编号转换成便于查看的字符串信息。
头文件:
#include <string.h>
函数原型:
char *strerror(int errnum);
示例如下:
在当前路径下是没有qurry.c文件的,调用open函数时没创建该文件打开它就会报错,返回的错误字符串如下图所示。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
void main(int argc,char *argv[])
{
int fd = -1;
fd = open("./qurry.c",O_RDONLY);
if(fd < 0)
{
printf("open error: %s\n", strerror(errno));
return;
}
close(fd);
}
8.2 perror函数
perror函数比较常见,它不需要传入errno,函数内部会自己去获取errno变量的值,调用此函数会直接将错误提示字符串打印出来,还可以在输出的错误提示字符串之前加入自己的打印信息。
头文件:
#include <stdio.h>
函数原型:
void perror(const char *s);
示例如下:
在当前路径下是没有qurry.c文件的,调用open函数时没创建该文件打开它就会报错。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
void main(int argc,char *argv[])
{
int fd = -1;
fd = open("./qurry.c",O_RDONLY);
if(fd < 0)
{
perror("open perror");
return;
}
close(fd);
}
9. 退出进程
当程序执行过程中,遇到了会影响后面语句执行的错误时(例如申请内存失败等),就应该提前退出进程,那么有哪些方优雅的退出方式呢?最容易想到的就是return了,如果在main函数中调用return可以退出进程,但在除main函数外的函数调用只能退出当前函数了,无法达到退出进程的效果,这时候就可以使用exit()、_exit()以及_Exit()函数了。
9.1 _exit()、_Exit()函数
_exit()和_Exit()函数是系统调用API接口,它们的作用是一样的,清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程。
头文件:
#include <unistd.h>
函数原型:
void _exit(int status);
status:0 :表示正常结束;其它值表示发生错误才结束。
头文件:
#include <stdlib.h>
函数原型:
void _Exit(int status);
9.2 exit()函数
exit()函数是一个标准C库函数,最终通过系统调用_exit()函数来清理内存、终止进程。
头文件:
#include <stdlib.h>
函数原型:
void exit(int status);
10 . proc文件系统
proc文件系统是一个虚拟文件系统,它是动态创建的,只存在于内存中,存储当前内核运行状态的信息文件,用户可以通过这些文件查看系统硬件及运行的进程信息,还可以更改可写文件的数据来改变内核的运行状态。proc文件系统挂载在系统的/proc目录下,给开发者提供调试内核的方法。
10.1 proc/目录
可以看到proc/目录下有一些以数字命名的子目录,这些数字表示系统运行进程的进程号(PID),子目录下包含对应进程的相关信息文件。
buddyinfo:用于诊断内存碎片问题;使用查看命令查看信息如下:
cmdline:启动内核时,传递给内核的相关参数信息;
device-tree:设备树配置的相关信息文件,cd进入该目录会跳转到sys/firmware/devicetree/base/目录下,这个信息对驱动开发调试非常有用;
mounts:查看当前挂载的文件系统列表;
devices:对应系统已加载的设备信息;
iomem:系统内存中的映射信息;
ioports:正在使用且已经注册过的与物理设备进行通讯的端口范围信息列表;
versions:系统版本信息;
modules:内核的所有模块名称列表;
meminfo:读取内存的使用状况等的信息;
11 .system函数
调用system函数可以在应用程序中使用shell命令,例如:system("ls -al");
头文件:
#include <stdlib.h>
函数原型:
int system(const char *command)