自学内容网 自学内容网

【Linux】基础IO


本节重点
1.复习C文件IO相关操作
2.认识文件相关系统调用接口
3.认识文件描述符,理解重定向
4.对比fd和FILE,理解系统调用和库函数的关系
5.理解文件系统中inode的概念
6.认识软硬链接,对比区别
7.认识动态静态库,学会结合gcc选项,制作动静态库

1.回顾C文件接口

1.1 hello.c写文件

#include <stdio.h>
#include <string.h>

int main()
{
 FILE *fp = fopen("myfile", "w");
 if(!fp){
 printf("fopen error!\n");
 }
 const char *msg = "hello bit!\n";
 int count = 5;
 while(count--){
 fwrite(msg, strlen(msg), 1, fp);
 }
 fclose(fp);
 return 0;
}

1.2 hello.c读文件

#include <stdio.h>
#include <string.h>

int main()
{
 FILE *fp = fopen("myfile", "r");
 if(!fp){
 printf("fopen error!\n");
 }
 
 char buf[1024];
 const char *msg = "hello bit!\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;
}

1.3 输出信息到显示器,你有哪些方法

#include <stdio.h>
#include <string.h>
int main()
{
 const char *msg = "hello fwrite\n";
 fwrite(msg, strlen(msg), 1, stdout);
 printf("hello printf\n");
 fprintf(stdout, "hello fprintf\n");
 return 0;
}

1.4 stdin & stdout & stderr

1.C默认会打开三个输入输出流,分别是stdin,stdout,stderr
2.仔细观察发现,这三个流的类型都是FIFE*,fopen的返回值类型,文件指针

1.5 总结

1.打开文件的方式

r  Open test file for reading.
   The stream is postioned at the beginning of the file.

r+ Open for reading and writing.
   The stream is positioned at the beginning of the file.
   
w  Truncate(缩短) file to zero length or create text file for writing.
   The stream is positioned at the beginning of the file.
 
w+ Open for reading and writing.
   The file is created if it does not exist, otherwise it is truncated.
   The stream is positioned at the beginning of the file.
   
a  Open for appending (writing at end of file). 
   The file is created if it does not exist. 
   The stream is positioned at the end of the file.
   
a+ Open for reading and appending (writing at end of file).
   The file is created if it does not exist. The initial file position
   for reading is at the beginning of the file, 
   but output is always appended to the end of the file.

如上,是学习C语言文件操作时学到的相关操作。还有fseek、ftell、rewind等函数,在C部分也有所涉猎就,要及时拿出来复习。

1.6 系统文件I/O

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码:
hello.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 bit!\n";
 int len = strlen(msg);
 while(count--){
 write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
 }
 close(fd);
 return 0;
}

hello.c读文件

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
 int fd = open("myfile", O_RDONLY);
 if(fd < 0){
 perror("open");
 return 1;
 }
 const char *msg = "hello bit!\n";
 char buf[1024];
 while(1){
 ssize_t s = read(fd, buf, strlen(msg));//类比write
 if(s > 0){
 printf("%s", buf);
 }else{
 break;
 }
 }
 close(fd);
 return 0;
}

接口介绍
open man 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: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
 O_RDONLY: 只读打开
 O_WRONLY: 只写打开
 O_RDWR : 读,写打开
 这三个常量,必须指定一个且只能指定一个
 
 O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
 O_APPEND: 追加写
返回值:
 成功:新打开的文件描述符
 失败:-1

mode_t理解:直接 man 手册,比什么都清楚。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

write read close lseek ,类比C文件相关接口。

2. open函数返回值

在认识返回值之前,先来认识两个概念:系统调用和库函数。
1.上面的fopen、fclose、fread、fwrite都是C标准库当中的函数,我们称之为库函数(libc);
2.而,openclosereadwritelseek都属于系统提供的接口,称之为系统调用接口;
3.回忆一下操作系统概念图:
在这里插入图片描述
系统调用接口和库函数的关系,一目了然。
所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

2.1 文件描述符fd

通过对open函数的学习,我们知道了文件描述符就是一个小整数。

2.2 0&1&2

1.linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2.
2.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;
}

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

2.3 文件描述符的分配规则

直接看代码:

#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;
}
//输出发现是 fd:3

关闭0或者2,再看:

#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;
}
//发现结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

2.4 重定向

那如果关闭1呢?看代码:

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

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <

那重定向的本质是什么呢?

这里需要画图,之后会补上。

2.5 使用dup2系统调用

函数原型如下:

#include <unistd.h>
int dup2(int oldfd, int newfd);

实例代码:

#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);
 dup2(fd, 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);
 fflush(stdout);
 }
 return 0;
}

例子1.在minishell中添加重定向功能:

# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <fcntl.h>
# define MAX_CMD 1024
char command[MAX_CMD];
int do_face()
{
 memset(command, 0x00, MAX_CMD);
 printf("minishell$ ");
 fflush(stdout);
 if (scanf("%[^\n]%*c", command) == 0) 
 {
 getchar();
 return -1; 
 } 
 return 0;
}

char **do_parse(char *buff)
{
 int argc = 0;
 static char *argv[32];
 char *ptr = buff;

 while(*ptr != '\0') 
 {
 if (!isspace(*ptr)) 
 {
 argv[argc++] = ptr;
 while((!isspace(*ptr)) && (*ptr) != '\0') 
 {
 ptr++;
 }
 }else 
 {
 while(isspace(*ptr)) 
 {
 *ptr = '\0';
 ptr++;
 }
 }
 }
 argv[argc] = NULL;
 return argv;
}

int do_redirect(char *buff)
{
 char *ptr = buff, *file = NULL;
 int type = 0, fd, redirect_type = -1;
 while(*ptr != '\0') 
 {
 if (*ptr == '>') 
 {
 *ptr++ = '\0';
 redirect_type++;
 if (*ptr == '>') 
 {
 *ptr++ = '\0';
 redirect_type++;
 }
 while(isspace(*ptr)) 
 {
 ptr++;
 }
 file = ptr;
 while((!isspace(*ptr)) && *ptr != '\0') 
 {
 ptr++;
 }
 *ptr = '\0';
 if (redirect_type == 0) 
 {
 fd = open(file, O_CREAT|O_TRUNC|O_WRONLY, 0664);
 }else 
 {
 fd = open(file, O_CREAT|O_APPEND|O_WRONLY, 0664);
 }
 dup2(fd, 1);
 }
 ptr++;
 }
 return 0;
}

int do_exec(char *buff)
{
 char **argv = {NULL};
 
 int pid = fork();
 if (pid == 0) 
 {
 do_redirect(buff);
 argv = do_parse(buff);
 if (argv[0] == NULL) {
 exit(-1);
 }
 execvp(argv[0], argv);
 }else {
 waitpid(pid, NULL, 0);
 }
 return 0;
}

int main(int argc, char *argv[])
{
 while(1) {
 if (do_face() < 0)
 continue;
 do_exec(command);
 }
 return 0;
}

printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。那追加和输入重定向如何完成呢?这个之后需要再研究一下。

3. FILE

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。
来段代码研究一下:

#include <stdio.h>
#include <string.h>
int main()
{
 const char *msg0="hello printf\n";
 const char *msg1="hello fwrite\n";
 const char *msg2="hello write\n";
 printf("%s", msg0);
 fwrite(msg1, strlen(msg0), 1, stdout);
 write(1, msg2, strlen(msg2));
 fork();
 return 0;
 }
 //运行出结果:
hello printf
hello fwrite
hello write

但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!

1.一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
2.printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
3.而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
4.但是进程退出之后,会统一刷新,写入文件当中。
5.但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
6.write 没有变化,说明没有所谓的缓冲。

综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。

那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

如果有兴趣,可以看看FILE结构体:
typedef struct _IO_FILE FILE; 在user/include/stdio.h

/usr/include/libio.h
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. 理解文件系统

我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。

[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9月 13 14:56" test.c

每行包含7列:
1.模式
2.硬链接数
3.文件所有者
4.组
5.大小
6.最后修改时间
7.文件名
ls -ll读取存储在磁盘上的文件信息,然后显示出来。
在这里插入图片描述

其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息。

[root@localhost linux]# stat test.c
 File: "test.c"
 Size: 654 Blocks: 8 IO Block: 4096 普通文件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800

上面的执行结果有几个信息需要解释清楚
inode
为了能解释清楚inode,我们先简单了解一下文件系统。
在这里插入图片描述

Linux ext2文件系统,


原文地址:https://blog.csdn.net/m0_46676283/article/details/142908965

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!