[C Linux内核] 文件与I/O

[C Linux内核] 文件与I/O

系统调用 Hello world

先看一个例子,利用系统调用sys_write用汇编实现的向stdout输出hello world

.data
msg:
    .ascii "Hello, world!\n"
    len = . - msg
.text
.global _start

_start:
    movl $len, %edx # third argument: message length
    movl $msg, %ecx # second argument: pointer to message to write
    movl $1, %ebx   # frist argument: file handle(here stdout)
    movl $4, %eax
    int $0x80
    movl $0, %ebx
    movl $1, %eax
    int $0x80

将_start作为 ELF linker or loader的 外部可以使用的符号(通过 .global) 并且在.data段内定义一个标号msg,代表字符串Hello world 的首地址,  定义len的时候用到的 . 是一个用来代替当前段内地址(在每一个段开头, “.” 的值都会初始化回0)因而, len的值就是当前地址减去msg的首地址, 换句话说,就是 字符串常量”Hello world\n”长度, 上面的代码进行了两次系统调用, 第一次, 是调用sys_write 第二次是调用 sys_exit, 第一次传递的参数有三个, 代码注释已经写明了

将上面的代码 汇编,链接, 运行

as =o hello.o hello.s
ld -o hello hello.o
./hello

即可得到我们想要的结果并且正常退出

上述的汇编代码, 在C语言中的实现是这样的

#include<unistd.h>

char msg[13] = "Hello world!\n";
#define len 13

int main(void)
{
    write(1, msg, len);
    _exit(0);
}

调用系统调用 write之后,再调用_exit退出 ,下面对C语言内提供的内核 I/O操作函数进行总结

libstdc I/O 与 Unbuffered I/O

libstdc: fopen fgetc fputc fclose

unbuffered: open read write close

Unbuffered I/O 与 libstdc I/O对比:

1.每一次调用都必须进入内核, 调用系统调用的效率要比调用在用户空间的函数慢很多(libstdc的函数在用户空间)

2.无缓冲的I/O对于每一次写入都会直接更新相应文件, 就算没有直接写入文件而是写入了内核的I/O缓存中的话, 对于其他同时访问这个文件的进程, 是能够同时读取到相应的改变的(也就是可以认为没有缓冲)

3.对于要求读写非常及时的情况(如对网络设备的读写) 一般情况会使用无缓冲的I/O保证及时接收或者写入信息

4.Unbuffered I/O不是C标准的一部分,而是UNIX标准的一部分, 而libstdc I/O是C标准的一部分(stdio库) 在Windows下,同样有printf, fputc这样的libstdc的库函数,但是没有UNIX标准中的(unistd.h)read write 在Windows下内核文件I/O的函数是 ReadFile WriteFile(Win32API)

5. write接收的第一个参数就是文件句柄 即file handle, 是一个 int型的数据, fprintf, fputc 等接收的第一个参数是一个文件指针FILE* 是一个FILE结构体指针, FILE结构体内包含file handle(fd)的值

在Linux下, 每一个进程是享用独立的file handle的, 举例而言,  参看下面的代码

#include<unistd.h>
#include<stdio.h>
#include<fcntl.h>

char msg[13] = "Test!!";
int len = 10;

int main(void)
{
    int fd = open("./meow", O_APPEND);
    printf("%d\n", fd);
    while(1);
}

open打开一个文件,返回值就是文件句柄的值(fd), 同时运行多个这个程序, 会发现输出的fd值都为3 , 如果fd不是共享的话,每一次执行open之后, fd的值都应该变化, 即三个进程的fd值是各不相同的

上面的例子里还能猜测到一个结果,通过file handle每一次都为3 这个事实, 再根据我们给write传的参数就是fd, file handle, 而stdin, stdout, stderr这些都可以作为write的第一个参数, 说明, stdin, stdout, stderr这三个默认就是被打开的文件,占据了file handle 0, 1, 2(实际上,在进程运行的时候, 默认就执行了三次open 分别打开 /dev/stdin /dev/stdout /dev/stderr)

为了证明上面的猜想(即stdin, stdout, stderr 在进程创建的时候就被打开了), 编写了下面的代码进行验证

#include<stdio.h>
#include<unistd.h>
#include<stdio.h>

char msg[20] = "I love C";
#define len 20

int main(void)
{
    //close(0);
    close(1);
    //close(2);
    write(1, msg, len);
    perror("Write Error");
    printf("Hello world\n");
    perror("Printf Error");
}

因为我们关闭了标准输出设备, 之后的所有输出到标准输出的内容都不能正确输出了, 再上面的代码内 write,和printf都没有成功

open/close

open函数的参数 flags比较复杂,参考 manual 的 2, 3 section 进行查看

其中比较重要的一个参数是 O_CREAT,指定了这个参数之后, 就需要提供第三个参数 mode了,而 这个mode要与当前Shell环境的umask做一个掩码运算,才得到的是创建文件时真正的file mode

O_TRUNC 表明文件如果存在,并且以W模式打开,则文件长度缩短到0Byte

O_NONBLOCK 非阻塞I/O(对设备文件有效)

close就没啥好说的了= =

 

read/write

read从打开的文件(设备)中读取数据, 并且写入到buf中,并将内核中文件的当前读写位置向后移动[注意,内核读写位置和用户空间的缓冲区中读写位置不是一回事]

对于上述说的内核读写位置 & 用户空间缓冲区中读写位置不是一回事,进行一个举例解释:

当用户使用fgetchar 或者 getchar scanf 的时候,有可能从内核中预读 1024字节内容到用户空间I/O缓冲区中,这时候,内核的缓冲区读写位置后移了1024字节, 而用户空间, 以getchar为例, 只移动了一个字节

read还分为阻塞 & 非阻塞read两种, 一般读常规文件不会阻塞, 而读网络设备, 以及终端设备的时候,没有输入就会阻塞

所谓阻塞, 就是指, 如果进行到read, 该进程自动进入等待态,等待信号将其唤醒, 这样可以减少CPU资源的消耗,如果非阻塞式则会不论是否接收到终端的输入(信号)都自动进行下去

下面给出一段通过非阻塞方式读取终端输入的例子

/*************************************************************************
    > File Name: nonblock_read.c
    > Author: VOID_133
    > ################### 
    > Mail: ################### 
    > Created Time: Thu 05 May 2016 08:12:42 AM CST
 ************************************************************************/
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>

#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"

int main(void)
{
    char buf[10];
    int fd, n, i;
    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
    if(fd < 0)
        perror("open /dev/tty");
    for(i = 0; i < 5; i++)
    {
        n = read(fd, buf, 10);
        if(n >= 0)
            break;
        if(errno != EAGAIN)
        {
            perror("read /dev/tty");
            exit(1);
        }
        sleep(1);
        write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
    }
    if(i == 5)
        write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
    else
        write(STDOUT_FILENO, buf, n);

    return 0;
}

write操作, 会在向终端设备/网络设备写入的时候,返回的字节数可能小于请求的字节数, 这个在socket编程部分继续研究

lseek

与 fseek 类似, 用于移动当前读写位置, 使用lseek将读写位置(offset)移动到文件末尾之后并写入内容, 就可以创建空洞文件,具体代码参考APUE lseek部分

fcntl/ioctl

fcntl 可以直接改变已经打开的文件的各种属性,而不需要重新打开文件,在打开的时候指定这些属性, 同时也可以获得文件的属性,还可以给文件加锁, 这种锁和 flock加的锁不一样 ,另外 fcntl设置的文件的属性与ioctl不同, ioctl 是控制设备文件各种参数的设置, 如串口波特率等.ioctl用于进行控制信息的修改,而fcntl用于对文件本身属性(而不是设备的属性)进行修改,

read/write 与 ioctl

read/write 和 ioctl 都可以对设备文件的数据进行读写, 不过他们读写的内容是有区别的,在串口线上收/发数据使用的是read write操作,而 对串口的波特率, 校验位,等通过ioctl进行设置,传递控制信息

mmap

mmap可以把一个文件的内容映射到内存中, 下面这段代码可以通过修改内存地址内的文件来更改真实文件的内容

首先在/tmp/ 下创建一个文件 hello 然后运行下面的代码

/*************************************************************************
    > File Name: mmap_test.c
    > Author: VOID_133
    > ################### 
    > Mail: ################### 
    > Created Time: Thu 05 May 2016 02:17:01 PM CST
 ************************************************************************/
#include<stdio.h>
#include<unistd.h>
#include<sys/mman.h>
#include<stdlib.h>
#include<sys/file.h>
#include<errno.h>

int main(void)
{
    /* mmap test */
    int fd = open("/tmp/hello", O_RDWR);
    int *p;
    if(fd < 0)
    {
        perror("open hello error");
        exit(1);
    }
    p = mmap(NULL, 10, PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    p[0] = 0x30313233;
    fprintf(stderr, "Map OK ,Change p[0]\n");
    sleep(5);
    munmap(p, 10);
    fprintf(stderr, "UnMap OK\n");
    sleep(5);
    return 0;
}

这段代码, 然后 等到输出 “Map OK Change p[0]”之后, 去查看 /tmp/hello这个文件,你会发现已经更改了(3210*****)

注意这里使用的是MAP_SHARED模式 ,如果改为MAP_PRIVATE则不会更改文件, 这个查看man 2 mmap即可得知

mmap的实现也是一个系统调用,这个系统调用经常用于在执行程序的时候将共享库映射到该进程的地址空间 通过strace 执行一个程序可以看出来, 这里不做说明

Leave a Reply

Your email address will not be published. Required fields are marked *

14 − 7 =

This site uses Akismet to reduce spam. Learn how your comment data is processed.