Linux系统编程——基础IO
1. C语言IO接口
我们可以通过C语言的库函数执行I/O操作,可以将内容输出到屏幕中,也可以将内容输出到文件中,可以从键盘获取数据,也可以从文件中获取数据等等。下面是C语言接口演示:
#include <stdio.h>
#include <string.h>
// 写文件
int main()
{
FILE *fp = fopen("myfile","w"); // 以只读的方式打开文件
if(!fp)
printf("fopen error!\n");
const char* msg = "hello world!\n";
int count = 5;
while(count--)
{
fwrite(msg,strlen(msg),1,fp);
}
fclose(fp);
}
// 读文件
int main()
{
FILE *fp = fopen("myfile", "r");
if(!fp)
printf("fopen error!\n");
char buf[1024];
const char* msg = "hello world!\n";
while(1)
{
//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
ssize_t s = fread(buf, 1, strlen(msg), fp);
if(s > 0){
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp))
break;
}
fclose(fp);
return 0;
}
// 输出到屏幕
int main()
{
printf("hello printf!\n");
fprintf(stdout,"hello fprintf!\n");
}
在C语言中,存在三个标准流,分别是标准输入流stdin
,标准输出流stdout
,标准错误流stderr
,这三个流都是FILE*
,也就是文件指针。
2. 系统文件IO接口
操作文件,除了上述的C接口(也会有C++接口,其他语言也同样拥有),我们也可以使用系统接口直接进行文件操作,先上一段代码案例,与C接口的写文件操作一样,只不过用系统接口替换了C语言的接口。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0); // 设置文件掩码
int fd = open("myfile", O_WRONLY | O_CREAT, 0644); // 分别是文件名,文件打开方式,文件的权限
if(fd < 0)
{
perror("open");
return 1;
}
int count = 5;
const char *msg = "hello world!\n";
int len = strlen(msg);
while(count--)
{
write(fd,msg,len); // 分别是文件句柄,缓冲区首地址,期待读取的字节数
}
close(fd);
return 0;
}
其他操作就不一样介绍了,下面具体介绍系统接口。
2.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);
- 函数说明:用于打开或者创建一个文件。
- pathname:要打开或创建的目标文件
flags:打开文件时,可以传入多个参数选项,使用多个选项时,使用|
。
参数:
- O_RDONLY:只读打开
- O_WRONLY:只写打开
- O_RDWR :读,写打开
- O_CREAT :文件不存在,则创建它。如果使用了mode选项,来指明新文件的访问权限。
- O_APPEND : 追加写
- 返回值:
- 成功:新打开的文件描述符
- 失败:-1
2.2 read
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t nbytes);
- 函数说明:从打开文件中读取数据。
- fd:打开的文件描述符
- buf:缓冲区的指针
- nbytes:想要读取的字节数
- 返回值:
- 成功:返回实际读到的字节数,如果已经到达文件尾,返回0
- 失败:返回-1
2.3 write
#include <unistd.h>
ssize_t write(int fd,const void *buf, size_t nbytes);
- 函数说明:从打开文件中写入数据
- fd:打开的文件描述符
- buf:缓冲区的指针
- nbytes:想要写入的字节数
- 返回值:
- 成功:返回实际写入的字节数
- 失败:返回-1
2.4 close
#include <unistd.h>
int close(int fd);
- 函数说明:关闭一个打开的文件。关闭一个文件的时候还会释放该进程加在该文件上的所有记录锁。
- fd:打开的文件描述符
- 返回值:
- 成功:返回0
- 失败:返回-1
2.5 lseek
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
- 函数说明:每个打开的文件都有一个与其相关联的
当前文件的偏移量
。通常是一个非负整数,你可以理解为是一个指针,指向文件的某处地方,一开始指向的是文件的开头部分,当文件到达末尾的时候,这个指针也会移动到尾部。- fd:打开的文件描述符
- offset:文件偏移量
- whence:文件的偏移量相对于什么位置开始
- SEEK_SET:文件的偏移量相比于文件开始处
offset
个字节。- SEEK_CUR:文件的偏移量相比于当前值加上
offset
个字节。- SEEK_END:文件的偏移量相比于文件末尾处
offset
个字节。- 返回值:
- 成功:返回新的文件偏移量
- 失败:返回-1
2.6 文件描述符fd
上面一系列系统接口都是围绕一个叫做文件描述符
的东西展开的,它是一个整数,你可以尝试打印一下返回的结果,通常都是一个小整数。文件描述符一个通俗的解释就是对于打开文件的一个唯一标识号。
-
Linux进程中,默认会打开三个文件描述符:分别是标准输入0,标准输出1,标准错误输出2。
-
这三个分别对应的物理设备一般是:键盘,显示器,显示器。
-
当我们使用系统接口想对标准输入或者标准错误进行写入的时候,采用如下方式:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> int main() { char buf[1024]; ssize_t s = read(0, buf, sizeof(buf)); if(s > 0) { buf[s] = 0; write(1, buf, strlen(buf)); write(2, buf, strlen(buf)); } return 0; }
用一张图理解文件描述符:
当我们打开文件的时候,操作系统会在内存创建相应的数据结构去存储文件的信息,也就是file结构体。当我们执行open系统调用的时候,会让进程与文件相关联起来,就会在进程中保存一个文件描述符表,每个表就指向一个file
结构体,本质上,文件描述符表就是一个指针数组,而标准输入、标准输出、标准错误是默认打开的,也就是这个表默认拥有的,分别是下标0,1,2。
分配规则:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
当我们直接打开一个文件的时候,对应的文件描述符是3。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
当我们关闭0的时候,给我们分配的文件描述符是0,当我们关闭2的时候,给我们分配的是2。因此,当我们打开一个文件时,优先分配数组下标最小并且没有被使用过的,作为新的文件描述符。
2.7 重定向
重定向就是把本该输出到屏幕的内容输出到其他地方,本该从键盘输入内容变成从其他地方输入,也就是改变了默认的IO方式。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
这段代码是将标准输入对应的文件描述符关闭,然后打开的文件获取的文件描述符fd
就是1。然后我们使用C语言的printf
函数,将本该输出到电脑屏幕的内容输出到文件中。
那么,为何会出现这种情况呢?我们重新理解一下printf
函数。
printf函数底层也是调用了write
系统接口进行写操作,默认是对stdout
也就是标准输出进行输出内容,那么我们看一下stdout
的源代码,
#define STDOUT_FILENO 1
#define stdout (&_iob[STDOUT_FILENO])
stdout底层就是文件描述符为1的位置,所以我们使用printf
函数进行打印,本质上就是对文件描述符1的文件进行输出。
2.8 dup2
#include <unistd.h>
int dup2(int oldfd, int newfd);
- 函数说明:我们知道,默认打开的文件的文件描述符是当前可用的文件描述符中的最小值,dup2可以指定新描述符的值。
- oldfd:旧的文件描述符
- newfd:新的文件描述符
- 返回值:
- 成功:返回新的文件描述符
- 失败:返回-1
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("./log", O_CREAT | O_RDWR);
if (fd < 0)
{
perror("open");
return 1;
}
close(1);
// 如果newfd已经打开,先将其关闭
// 调用后,fd为3和fd为1的指针都指向log文件
dup2(fd, 1); // dup2(3,1);
for (;;)
{
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0)
{
perror("read");
break;
}
printf("%s", buf); // 向文件描述符1写入buf数据
fflush(stdout);
}
return 0;
}
3. FILE
- C语言有一个
FILE
结构体,所有相关的IO操作其实都是对这个结构体相关联。 - 我们知道底层的系统接口都是对文件描述符
fd
进行访问,所以FILE结构体对象如果想要访问文件,就必须在结构体中封装文件描述符。
// FILE 源码
typedef struct _IO_FILE FILE;
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; // 文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
4. 动态库和静态库
4.1 静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
测试代码:
/add.h/
#ifndef __ADD_H__
#define __ADD_H__
int add(int a, int b);
#endif // __ADD_H__
/add.c/
#include "add.h"
int add(int a, int b)
{
return a + b;
}
/sub.h/
#ifndef __SUB_H__
#define __SUB_H__
int sub(int a, int b);
#endif // __SUB_H__
/sub.c/
#include "sub.h"
int sub(int a, int b)
{
return a - b;
}
///main.c
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main( void )
{
int a = 10;
int b = 20;
printf("add(10, 20)=%d\n", a, b, add(a, b));
a = 100;
b = 20;
printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
}
生成和使用:
[root@localhost linux]$ gcc -c add.c -o add.o
[root@localhost linux]$ gcc -c sub.c -o sub.o
# 生成静态库
# ar是gnu归档工具 rc表示替换和创建
[root@localhost linux]$ ar -rc libmymath.a add.o sub.o
# 查看静态库中的目录列表
# -t 表示列出静态库的文件 -v 详细信息
[root@localhost linux]$ ar -tv libmymath.a
rw-r--r-- 0/0 1240 Jan 19 13:28 2023 add.o
rw-r--r-- 0/0 1240 Jan 19 13:28 2023 sub.o
# 使用
# -L 指定库路径 -l 指定库名(去掉lib和后缀)
[root@localhost linux]$ gcc main.c -L. -lmymath
4.2 动态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序可以共享使用动态库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘是的该动态库复制到内存中,整个过程称为动态链接。
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
测试代码:
/add.h/
#ifndef __ADD_H__
#define __ADD_H__
int add(int a, int b);
#endif // __ADD_H__
/add.c/
#include "add.h"
int add(int a, int b)
{
return a + b;
}
/sub.h/
#ifndef __SUB_H__
#define __SUB_H__
int sub(int a, int b);
#endif // __SUB_H__
/sub.c/
#include "sub.h"
int sub(int a, int b)
{
return a - b;
}
///main.c
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main( void )
{
int a = 10;
int b = 20;
printf("add(10, 20)=%d\n", a, b, add(a, b));
a = 100;
b = 20;
printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
}
生成和使用:
# 生成动态库
# fPIC 产生位置无关的.o文件
[root@localhost linux]$ gcc -fPIC -c sub.c add.c
[root@localhost linux]$ gcc -shared -o libmymath.so *.o
#使用动态库
[root@localhost linux]$ gcc main.c -o main -L. -lmymath
库的搜索路径:
- 从左到右搜索-L指定的目录
- 由环境变量指定的目录(LIBRARY_PATH)
- 由系统指定的目录
- /usr/lib
- /usr/local/lib