嵌入式Linux C应用编程

什么是Linux应用程序

(1)运行在Linux操作系统用户空间的程序。

(2)内核程序运行在内核空间、应用程序运行在用户空间。

(3)内核空间和用户空间。

为了保护内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。

为什么要区分内核空间与用户空间?

在CPU的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃。如果允许所以程序都可以使用这些指令,那么系统崩溃的概率将大大增加。

所以,CPU将指令分为特权指令和非特权指令,对于危险指令,只允许操作系统及其相关模块使用,普通程序只能使用那些不会造成灾难的指令。

应用程序和驱动程序、单片机程序有何不同?

(1)与驱动程序的不同

所处空间不同:应用程序运行在用户空间、驱动程序运行在内核空间。

功能、作用不同:应用程序完成的是业务逻辑、驱动程序完成的是底层硬件操作逻辑

(业务逻辑:一个实体单元为了向另一个实体单元提高服务,应该具备的规则与流程)

编译方式:驱动程序可编译成模块或者内置到内核,但都需要依赖于内核源码进行编译;应用程序可单独编译

(2)与单片机程序的不同

单片机程序是裸机程序,没有操作系统的概念

单片机是硬件驱动+业务逻辑的集合,程序是整体编译

如何编写Linux应用程序?

系统调用(C语言接口)

Linux操作系统向用户层提供的接口,system call,是Linux应用层进入到内核空间的入口

库函数:标准C库函数

对系统调用的封装,在效率和使用便利性方面有提升

不局限编程语言

可通过C/C++、Python、shell、Qt来写应用程序

文件I/O

什么是文件I/O?

文件I/O指的是对文件的输入、输出操作,也就是对文件的读写操作。

操作步骤:

(1)打开文件;

(2)读写文件;

(3)关闭文件。

文件描述符

什么是文件描述符?

文件句柄、一个非负整数、与对应的文件相绑定。

文件描述符如何分配?

分配一个没有被使用的最小非负整数作为文件描述符。

标准输入、标准输出和标准错误

0、1、2(父进程)

打开文件:open()函数

(1)打开文件;

(2)创建文件。

函数原型

int open(const char*pathname,int flags); //pathname 文件路径、flags标志
int open(const char*pathname,int flags,mode_t mode); //文件路径、标志、权限

pathname:字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径)信息;如果pathname是一个符号链接,会对其进行解引用。

flags:调用open函数时需要提供的标志,包括文件访问模式标志以及其他文件相关标志,这些标志使用宏定义进行描述,都是常量。

部分flags参数介绍:

O_RDONLY 以只读方式打开文件

O_WRONLY 以只写方式打开文件

O-RDWR 以可读可写方式打开文件

O_CREAT 如果pathname参数指向的文件不存在则创建此文件

O_DIRECTORY 如果pathname参数指向的不是一个目录,则调用open失败

O_EXCL 此标志一般结合O_CREAT 标志一起使用,用于专门创建文件。在flags参数同时使用到了O_CREAT和O_EXCL标志的情况下,如果pathname参数指向的文件已经存在,则open函数返回错误。

flags参数既可以单独使用某一个标志,也可以通过位或运算(|)将多个标志进行组合,例如:

open("./src_file",O_RDONLY)    //单独使用某一个标志
open("./src_file",O_RDONLY|O_NOFOLLOW)//多个标志位一起使用

mode:此参数用以指定新建文件的访问啊权限,只有当flags参数中包含了O_CREAT或O_TMPFILE标志是才有效(O_TMPFILE标志用于创建一个临时文件)。

三个用户基本权限:可执行、可读、可写。

mode参数的类型是mode_t,这是一个u32无符号整形数据,

从低位到高位,每3个bit位分一组,分别表示:

O...这3个bit位用于表示其他用户权限;

G...这3个bit位表示同组用户(group)的权限,即与文件所有者有相同组ID的所有放用户;

U...这3个bit位表示文件所属用户的权限,即文件或目录的所属者;

S...这3个bit位表示文件的特殊权限,文件特殊权限一般用的比较少。

3个bit位从高到低分别为rwx(读、写、可执行),当具有某个权限时,该位为1,否则为0。

例如:

111000000(二进制):表示文件所属者具有读、写、执行权限,面向同组用户和其他用户不具有任何权限;

100100100(二进制):表示文件所属者、文件同组用户以及其他用户都具有读权限,但都没有写、执行权限。

关于文件权限表示方法,在实际编程中,可以直接使用Linux中已经定义好的宏,不同的宏定义表示不同的权限。

例如:

S_IRUSR:允许文件所属者读文件

S_IWUSR:允许文件所属者写文件

........

这些宏既可以单独使用,也可以通过位或运算将多个宏组合在一起,如:

S_IRUSR|S_IWUSR|S_IROTH

返回值:成功将返回文件描述符,文件描述符是一个非负整数;失败将返回-1

写文件:write()函数

函数原型

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

参数描述

fd:文件描述符

buf 指定写入数据对应的缓冲区

count 指定写入的字节数

返回值:如果成功将返回写入的字节数(0表示未写入任何字节),如果此数字小于count参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1

读写位置问题:

读写操作都是从文件位置偏移量处开始,默认情况下当前位置偏移量一般是0,也就是指向了文件的起始位置,当调用read、write函数读写操作完成之后,当前位置偏移量也会向后移动对应的字节数,譬如当前位置偏移量为1000个字节处,调用write()写入或read()读取500个字节之后,当前位置偏移量将会移动到1500个字节处;发生错误返回值为-1

读文件:read()函数

函数原型

ssize_t read(int fd,void *buf,size_count);

参数描述

fd:文件描述符

buf : 指定用于存储读取数据的缓冲区

count:指定需要读取的字节数

返回值:如果读取成功将返回读取到的字节数,实际读取到的字节数可能会小于count参数指定的字节数,也有可能会为0,譬如进行读操作时,当前文件位置偏移量已经到了文件末尾。实际读取得到的字节数少于要求读取的字节数,譬如在到达文件末尾有30个字节数据,而要求读取100个字节,则read读取成功只能返回30;而下一次在调用read读,它将返回0(文件末尾);发生错误返回值为-1

关闭文件:close()函数

函数原型 int close(int fd)

参数描述

使用实例

成功返回:0

失败返回:-1

文件I/O编程练习

编程练习

1.新建一个文件test.txt写入“Hello World”。对新建文件的要求O、G用户对文件只有可读权限,U用户对文件具有

可读可写权限.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
​
int main(void)
{
    int fd;
    int ret;
    fd=open("./text.txt",O_WRONLY|O_CREAT|O_EXCL,0644);//创建一个文件,并打开,若文件已经存在就会报错,0644是权限说明,"0"表示八进制数
        if(-1==fd)
        {
            printf("open error\n");//打开失败
            return 1;
        }
        printf("open ok!\n");
        write(fd,"Hello World",11);
        if(-1==ret)
        {printf("write error\n");
        close(fd);
        return 1;
        }
        printf("write %d bytes ok!/n",ret);
        close(fd);
        return 0;
    
}

2.读取文件text.txt的内容,并打印出来。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
    int fd;
    char buf[125]={0};
    int ret;
    fd=open(".text.txt,O_RDONLY");
    if (-1==fd)
        {
        printf("open error\n");
        return 1;
        }
        printf("open ok!\n");
        read(fd,buf,11);
        if(-1==ret
        {
        printf("read error\n");
        close(fd);
        return 1;
        }
        printf("read %d bytes: %s\n",ret,buf);
        close(fd);
        return 1;
}

调整读写位置偏移:lseek()函数

函数原型

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

参数描述

fd:目标文件

offset:偏移量,以字节为单位,需要一个参考值

whence:用于定于参数offset偏移量对应的参考值,有三种(宏定义):文件头部,文件尾部,当前位置

SEEK_SET:读写偏移量将指向offset字节位置处(从文件头部开始算);

SEEK_CUR:读写偏移量将指向当前位置+offset字节位置处,offset可以为正,也可以为负,如果是正数表示向后偏移,如果是负数则表示往前偏移;

SEEK_END:读写偏移量将指向文件末尾+offset字节位置处,同样offset可以为正,也可以为负,如果是正表示向后偏移,如果是负数则表示往前偏移。

返回值:成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置,也可以用该函数计算文件大小;发生错误将返回-1。

(1)将读写位置移动到文件开头处:

off_t off=lseek(fd,0,SEEK_SET);
if(-1==off)
    return -1;

(2)将读写位置移动到文件末尾:

off_t off=lseek(fd,0,SEEK_END);
if(-1==off)
    return -1;

(3)将读写位置移动到偏移文件开头100个字节处:

off_t off=lseek(fd,100,SEEK_SET);
if(-1==off)
    return -1;

(4)获取当前读写位置偏移量:

off_t off=(fd,0,SEEK_CUR);//没有改变指针位置,但是可以通过这种方式得到读写位置相对于文件头部的偏移量
if(-1==off)
    return -1;

函数执行成功将返回当前读写位置

int main(int argc,char **argv[])//第一个变量是参数的个数,第二个是字符串数组

深入文件I/O:文件管理

Linux系统下的文件管理

静态文件(文件没有被打开)

扇区(sector):磁盘存储的单位,512字节。

块(block):一个块=多个扇区,操作系统在读取写入时,最小单位是块。

inode

PCB(process control block):进程控制块,内核会为每一个进程设置一个PCB数据块。

目的:方便内核管理进程。

错误编号errno

errno变量:是一个全局变量,只需要调用头文件<errno.h>就可以使用,是一个int型编号,用来记录错误标号。

#include <errno.h>
#include <stdio.h>
int main (void)
{
    printf("errno=%d\n",errno);
    return 0;
}

输出结果为:

errno=0   //0表示没有错误,错误编号为大于0的整数

测试

#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
include <sys/stat.h>
#include <fcntl.h>
​
int main (void)
{
    int fd;
    printf("errno=%d\n",errno);
    fd=open("./test.txt",O_RDONLY); //打开一个不存在的文件
    if(-1==fd)
    {
        printf("errno=%d\n",errno);
        return 1;
    }
    return 0;
}
errno=0
errno=2

但并不是所有系统调用、库函数在发生错误时都会改变errno值。

strerror()函数

可获取系统错误信息或打印用户程序错误信息

#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
include <sys/stat.h>
#include <fcntl.h>
#include <string.h>  //strerror()头文件
​
int main (void)
{
    int fd;
    printf("errno=%d\n",errno);
    fd=open("./test.txt",O_RDONLY); //打开一个不存在的文件
    if(-1==fd)
    {
        printf("errno=%d\n",errno);
        printf("%s\n",strerror(errno)); //打印错误信息
        return 1;
    }
    return 0;
}

测试结果:

errno=0
errno=2
No such file or directory

perror()函数

不需要调用 printf 函数并且不需要读取errno编号 就可以直接打印

#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
include <sys/stat.h>
#include <fcntl.h>
#include <string.h>  //strerror()头文件
​
int main (void)
{
    int fd;
    printf("errno=%d\n",errno);
    fd=open("./test.txt",O_RDONLY); //打开一个不存在的文件
    if(-1==fd)
    {
        printf("errno=%d\n",errno);
        perror("");
        perror("open error");
        return 1;
    }
    return 0;
}
errno=0
errno=2
No such file or directory  //perror("");
open error:No such file or directory  //perror("open error");

空洞文件

概念:没有数据的区域

对文件的写操作:

单线程的写操作:从头到尾写入数据

多线程的写操作:将文件分为几个部分,每个线程各自负责写其中一个部分,同时写入,互不干扰。

空洞文件的应用场景:

(1)在使用迅雷下载文件时,还未下载完成,就发现该文件已经占据全部文件大小的空间,这就是空洞文件;下载如果没有空洞文件,多线程下载时文件就只能从一个地方写入,这就不能发挥多线程的作用了;如果有了空洞文件,就可以从不同的地址同时写入,就能达到多线程的优势;

(2)在创建虚拟机时,给虚拟机分配了100G磁盘空间,但其实系统安装完成之后,开始也不过只用了3、4G磁盘空间,如果一开始就把100G分配出去,资源是很大的浪费。

创建空洞文件:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
​
static unsigned char buf[4096];
int main(void)
{
    int fd;
    int ret;
    fd=open("./test.txt",O_WRONLY|O_CREAT|O_EXCL,0644); //新建文件,并设置权限
     if(-1==fd)
     {
        perror("open error");
        return 1;
     }
    ret=lseek(fd,4096,SEEK_SET); //利用lseek函数移动指针到4k的位置
    if(-1==ret)
    {
        perror("lseek error");
        close(fd);
        return 1;
    }
    ret=write(fd,buf,4096);  //调用write()函数写入数据
    if(-1==ret)
    {
        perror("write error");
        close(fd);
        return 1 ;
    }
    printf("write %d bytes\n",ret);
    close (fd);
    return 0;
}

使用ls命令查看到的空洞文件的大小是8K,使用ls命令查看到的大小是文件的逻辑大小,包括了空洞部分大小和真实数据部分的大小;

当使用du命令查看空洞文件时,其大小显示为4K,du命令查看到的是文件实际站存储块的大小(空洞文件大小未被计算)。

O_TRUNC和O_APPEND标志

O_TRUNC标志

截断文件、丢弃文件中的内容,文件大小变为0

测试

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main (void)
{
	int fd;
	fd=open("./test.txt".O_RDONLY|O_TRUNC);
	if(-1==fd)
	{
		perror("open error");
		return 1;
	}
	close(fd);
	return 0;
}

运行之后文件大小变为了0,类似于新建文件的操作。

O_APPEND标志

保证每次调用write()时都是从文件末尾开始写入。

注:O_APPEND标志对read操作不会有影响。

lseek操作不会影响write操作。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
	int fd;
	int ret;
	fd=open("./test.txt",O_WRONLY);
	if(-1==fd)
	{
		perror("open error");
		return 1;
	}
	ret=write(fd,"Hello world",11);
	if(-1==ret)
	{
		perror("write error");
		close(fd);
		return 1;
	}
	close (fd);
	return 0;
}
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
	int fd;
	int ret;
	fd=open("./test.txt",O_WRONLY|O_APPEND);
	if(-1==fd)
	{
		perror("open error");
		return 1;
	}
	ret=write(fd,"Hello world",11);
	if(-1==ret)
	{
		perror("write error");
		close(fd);
		return 1;
	}
	close (fd);
	return 0;
}

未使用O_APPEND时,在写入时会先删除掉内部内容,再进行写入,即从文件头部开始写入,这样就会覆盖点原本的内容。

使用O_APPEND时,在写入时不会删掉内部内容,并写入新的内容,即写入内容时是从文件末尾写入

同一个文件被多次打开

文件描述符与文件的对应关系:

可以是1对1,也可以是n对1;

使用任何一个fd都可对同一文件进行读、写操作(文件共享中常用)。

读写位置偏移量是相互独立的

每个文件描述符都有各自的文件表,所以位置偏移量是各自维护自己的。

使用多个文件描述符对同一文件进行写操作应注意:

(1)在写入fd2时,不能将fd1中的数据覆盖。(使用O_APPEND就可以避免,因为O_APPEND标志会使写操作从文件尾部开始写入)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
	int fd1;
	int fd2;
	int ret1;
	int ret2;
	fd1=open("./test.txt",O_WRONLY|O_TRUNC|O_APPEND); //O_TRUNC将文件原有内容丢弃
	if(-1==fd1)
	{
		perror("open error");
		return 1;
	}
	fd1=open("./test.txt",O_WRONLY|O_APPEND);
	if(-1==fd2)
	{
		perror("open error");
		close (fd1); //每一个fd都要使用close关闭
		return 1;
	}
	write(fd1,"Hello World",11);
	write(fd2,"ABCD EFAG",9);
		close(fd1);
		close(fd2);
		return 0;
	
}

测试结果输出:

Hello WorldABCD EFAG

文件描述符的复制

文件描述符的复制原理:对fd进行复制,得到它的副本。

fd1是原文件,fd2是经过复制操作得到的fd1的副本。

也就是fd1和fd2指向的事同一个文件表,使用同一个读写指针。

dup()函数

函数原型:int dup(int oldfd);

返回值:成功时会返回一个新的文件描述符newfd;失败则返回-1,并设置errno。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
​
int main (void)
{
    int fd1,fd2;
    int ret;
    char buf[128]={0};
    fd1=open("./test.txt",O_RDWR|O_TRUNC);
    if(-1==fd1)
    {
        perror("open error");
        return 1;
    }
    fd2=dup(fd1); //fd2和fd1拥有同样的权限,即在本次程序中拥有可读可写权限
    if(-1==fd2)
    {
        perror("dup error");
        close(fd1);
        return 1;
    }
    ret=write(fd2,"Hello World",11);  //读写指针已经移动到了文件末尾
    if(-1==ret)
    {
        perror("write error");
        close(fd1);
        close(fd2);
        return 1;
    }
    lseek(fd2,0,SEEK_SET); //改变读写指针位置,改变fd1和fd2都可以
    ret=read(fd1,buf,11);//读写指针在读写数据的时候指针已经移动到了文件末尾,读取到的数据为空,添加上一行代码即可完成读写
    if(-1==ret)
    {
        perror("read error");
        close(fd1);
        close(fd2);
        return 1;
    }
    printf("read: %s\n",buf)
    close(fd1);
    close(fd2);
    return 0;
}

测试结果:

read:Hello World

dup2()函数:int dup2(int oldfd,int newfd)

dup2()允许用户指定新的文件描述符

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main (void)
{
	int fd1,fd2;
	int ret;
	char buf[128]={0};
	fd1=open("./test.txt",O_RDWR|O_TRUNC);
	if(-1==fd1)
	{
		perror("open error");
		return 1;
	}
	fd2=dup2(fd1,1000); //与上一段dup程序相比,只将dup函数变成了dup2函数,并增加了一个参数
	if(-1==fd2)
	{
		perror("dup error");
		close(fd1);
		return 1;
	}
	printf("fd2:%d\n",fd2); //打印文件大小
	ret=write(fd2,"Hello World",11);  //读写指针已经移动到了文件末尾
	if(-1==ret)
	{
		perror("write error");
		close(fd1);
		close(fd2);
		return 1;
	}
	lseek(fd2,0,SEEK_SET); //改变读写指针位置,改变fd1和fd2都可以
	ret=read(fd1,buf,11);//读写指针在读写数据的时候指针已经移动到了文件末尾,读取到的数据为空,添加上一行代码即可完成读写
	if(-1==ret)
	{
		perror("read error");
		close(fd1);
		close(fd2);
		return 1;
	}
	printf("read: %s\n",buf)
	close(fd1);
	close(fd2);
	return 0;
}

文件共享

什么是文件共享?

所谓文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个inode)被多个独立的读写体同时进行IO操作。多个独立的读写体可以理解为对应于同一个文件的多个不同的文件描述符,譬如多次打开同一个文件所得到的多个不同的fd,或使用dup()(或dup2)函数复制得到多个不同的fd等。

同时进行IO操作指的是一个读写体操作文件尚未调用close关闭的情况下,另一个读写体去操作文件。

文件共享的常见方式

(1)同一进程的多个线程间共享

(2)多个不同的进程间实现共享

文件共享存在竞争冒险

原子操作与竞争冒险

竞争(数据覆盖)与冒险

竞争冒险不但存在于Linux应用层、也存在于Linux内核驱动层。

原子操作(不可被打断):

1.O_APPEND标志:能够保证每次调用write()函数时,都是从文件的末尾开始写入的。

2.pread()和pwrite()函数:和read()、write()函数一样,但是pread()和pwrite()可以实现原子操作(移动指针+写入数据)。调用pread和pwrite函数可传入一个位置偏移量offset参数,用于指定文件当前读或写的位置偏移量,所以调用pread相当于调用lseek后再调用read;同理,调用pwrite相当于调用lseek后再调用write。即:使用pread或pwrite函数不需要使用lseek来调整当前位置偏移量,并会将“移动当前位置偏移量、读或写”这两步操作组成一个原子操作。

#include <unistd.h>
ssize_t pread(int fd,void *buf,size_t count,off_t offset);
ssize_t pwrite(int fd,const void *buf,size_t count,off_t offset);

函数参数和返回值:

fd、buf、count参数与read或write函数意义相同。

offset:表示当前需要进行读或写的位置偏移量。

返回值:返回值与read、write函数返回值意义一样。

虽然pread(或pwrite)函数相当于lseek与pread(或pwrite)函数的集合,但还是有以下区别:

(1)调用pread函数时,无法中断其定位和读操作(也就是原子操作);

(2)不更新文件表中的当前文件位置偏移量。

3.O_EXCL标志:可以用来创建文件,如果被打开文件不存在,可以添加O_EXCL标志去创建

判断文件是否存在+创建文件的原子操作

截断文件

truncate()函数

int truncate(const char *path,off_t length); //path文件路径,length截断长度,使用场景为未被打开的文件

返回值:成功返回0;失败返回-1,并且会设置errno

ftruncate()函数

int ftruncate(int fd,off_t length); //fd为文件描述符,用来指定目标文件,使用场景为已经被open函数打开的文件

这两个函数都可以对文件进行截断操作,将文件截断为参数length指定的字节长度。

什么是截断?

如果文件的大小大于参数length所指定的大小,则多余的数据将被丢失,类似于多余的部分被“砍”掉了;如果文件目前的大小小于参数length所指定的大小,则将其进行扩展,对扩展部分进行读取将得到空字节"\0"。

使用ftruncate()函数进行文件截断操作之前,必须调用open()函数打开该文件得到文件描述符,并且必须具有可写权限,也就是调用open()打开文件时需要指定O_WRONLY或O_RDWR。

调用这两个函数并不会导致文件读写位置偏移量发生改变,所以截断之后一般需要重新设置文件当前的读写位置偏移量,以免由于之前所指向的位置已经不存在而发生错误(譬如文件长度变短了,文件当前所指向的读写位置已不存在了)。

测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
​
int main(void)
{
    int ret;
    ret=truncate("./test.txt",1024);
    if(-1==ret)
    {
        perror("truncate error");
        return 1;
    }
    return 0;
}

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
	int ret;
	int fd;
	fd=open("./test.txt",O_WRONLY);
	if(-1==fd)
	{
		perror("open error");
		return 1;
	}
	ret=ftruncate(fd,4096);
	if(-1==ret)
	{
		perror("truncate error");
		return 1;
	}
	close(fd);
	return 0;
}

fcntl()函数和ioctl()函数(系统调用)

fcntl()函数:可以对一个已经打开的文件描述符执行一系列控制操作,譬如复制一个文件描述符(与dup、dup2作用相同)、获取/设置文件描述符标志、获取/设置文件状态标志等,类似于一个多功能文件描述符管理工具箱

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

函数参数和返回值:

fd:文件描述符

cmd:操作命令。此参数表示对fd进行什么操作,cmd参数支持很多操作命令,这些命令都是以F_XXX开头,不同的cmd具有不同的作用,cmd操作命令大致可以分为以下5种功能:

(1)复制文件描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC);

(2)获取/设置文件描述符标志(cmd=F_GETFD或cmd=F_SETFD);

(3)获取/设置文件状态标志(cmd=GETFL或cmd=F_SETFL);

(4)获取/设置异步IO所有权(cmd=F_GETOWN或cmd=F_SETOWN);

(5)获取/设置记录锁(cmd=F_GETLK或cmd=F_SETLK);

fcntl函数是一个可变惨函数,第三个参数需要根据不同的cmd来传入对应的实参,配合cmd来使用。

返回值:执行失败,返回-1,并会设置errno;执行成功,其返回值与cmd(操作命令)有关,譬如cmd=F_DUPFD(复制文件描述符)将返回一个新的文件描述符、cmd=F_GETFD(获取文件描述符标志)将返回文件描述符标志、cmd=F_GETFL(获取文件状态标志)将返回文件状态标志等。

测试:

1.复制文件描述符

当cmd=F_DUPFD时,它的作用会根据fd复制出一个新的文件描述符,此时需要传入第三个参数,第三个参数用于指出新复制出的文件描述符是一个大于或等于该参数的可用文件描述符(没有使用的文件描述符);如果第三个参数等于一个已经存在的文件描述符,则取一个大于该参数的可用文件描述符。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
​
int main (void)
{
    int fd1,fd2;
    fd1=open("./test.txt",O_RDONLY);
    if(-1==fd1)
    {
        perror("open error");
        return 1;
    }
    fd2=fcntl(fd1,F_DUPFD,100);
    if(-1==fd2)
    {
        perror("fcntl error");
        close(fd1);
        return 1;
    }
    printf("fd1: %d,fd2: %d\n",fd1,fd2);
    close (fd1);
    close (fd2);
    return 0;
}

2.获取/设置文件状态标志

当cmd=F_GETFL可用于获取文件状态标志,cmd=F_SETFL可用于设置文件状态标志。cmd=F_GETFL时,不需要传入第三个参数,返回值成功表示获取到的文件状态标志;cmd=F_SETFL时,需要传入第三个参数,此参数表示需要设置的文件状态标志。

这些标志指的就是在调用open函数时传入的flags标志,可以指定一个或多个(通过位或 | 运算符组合),但是文件权限标志(O_RDONLY、O_WRONLY、O_RDONLY)以及文件创建标志(O_CREAT、O_EXCL、O_NOCTTY、O_TRUNC)不能被设置、会被忽略;在Linux系统中,只有O_APPEND、O_ASYNC、O_DIRECT、O_NOATIME以及O_NONBLOCK这些标志可以被修改。所以,对于一个硬件被打开的文件描述符,可以通过这种方式添加或移除标志。

测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
​
int main(void)
{
    int fd;
    int ret;
    
    fd=open("./test.txt",O_RDONLY);
    if(-1==fd)
    {
        perror("open error");
        return 1;
    }
    ret=fcntl(fd,F_GETFL);
    if(-1==ret)
    {
        perror("fcntl error");
        close(fd);
        return 1;
    }
    printf("flags:0x%x\n",ret);  //使用十六进制打印添加之前的标志
    
    ret=fcntl(fd,F_SETFL,ret|O_APPEND);
    if(-1==ret)
    {
        perror("fcntl error");
        close (fd);
        return 1;
    }
    printf("flags: 0x%x\n",ret); //使用十六进制打印添加之后的标志
    close(fd);
    return 0;
}

ioctl()函数:一般用于操作特殊文件或硬件外设,譬如可以通过ioctl获取LCD相关信息等。

函数原型:

#include <sys/ioctl.h>

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

函数参数和返回值:

fd:文件描述符

request:此参数与具体要操作的对象有关,没有统一值,表示向文件描述符请求相应的操作;

...:此函数是一个可变参函数,第三个参数需要根据request参数来决定,配合request来使用。

返回值:成功返回0;失败返回-1.

标准I/O库

什么是标准I/O库?

标准C库当中用于文件I/O操作相关的一套库函数,使用标准I/O需要包含头文件

标准I/O和文件I/O之间的区别?

(1)标准I/O是库函数,而文件I/O是系统调用;

(2)标准I/O是对文件I/O的封装;

(3)可移植性:标准I/O相对于文件I/O具有更好的可移植性;

(4)效率:标准I/O在效率上要优于文件I/O。

FILE指针和fopen函数

FILE指针:标准I/O使用FILE指针作为文件句柄,与文件I/O中的文件描述符相似

打开文件——fopen()函数:标准I/O中使用fopen()函数打开文件

FILE *fopen(const char *path,const char *mode);

参数说明:

path:参数path指向文件路径,可以是绝对路径,也可以是相对路径;

mode:参数mode指定了对该文件的读写权限,是一个字符串。

返回值:调用成功返回一个指向FILE类型对象的指针(FILE*),该指针与打开或创建的文件相关联,后续的标准I/O操作将围绕FILE指针进行。如果失败则返回NULL,并设置errno以指示错误原因。

fopen()函数的mode参数说明:

mode说明对应于open()函数的flags参数取值
r以只读方式打开文件O_RDONLY
r+以可读、可写方式打开文件O_RDONLY
w以只写方式打开文件,如果参数path指定文件存在,将文件长度截断为0;如果指定文件不存在则创建该文件O_WRONLY|O_CREAT|O_TRUNC
w+以可读、可写方式打开文件,如果参数path指定的文件存在,将文件长度截断为0;如果指定文件不存在则创建该文件。O_RDWR|O_CREAT|O_TRUNC
a以只写方式打开文件,打开以进行追加内容(在文件末尾写入),如果文件不存在则创建该文件。O_WRONLY|O_CREAT|O_APPEND
a+以可读可写方式打开文件,以追加方式写入(在文件末尾写入),如果文件不存在则创建该文件O_RDWR|O_CREAT|O_APPEND

新建文件的权限

由fopen()函数原型可知,fopen()只有两个参数path和mode,不同于open()系统调用,它并没有任何一个参数来指定新建文件的权限。当参数mode取值为“w”、“w+”、“a”、“a+”之一时,如果参数path指定的文件不存在。则会创建该文件,那么新的文件权限是如何确定的呢?

虽然调用fopen()函数新建文件时无法手动指定文件的权限,但却有一个默认值;

S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH  (0666)六进制// 110 110 110

使用实例:

fopen(path,"r"); //使用只读方式打开文件
fopen(path,"r+"); //使用可读、可写方式打开文件
fopen(path,"w"); //使用只写方式打开文件,并将文件截断为0,如果文件不存在则创建该文件

关闭文件——fclose()函数:调用fclose()库函数可以关闭一个由fopen()打开的文件

int fclose(FILE *stream);

读文件和写文件

读文件——fread()函数

size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);

库函数fread()用于读取文件数据,其参数和返回值含义如下:

ptr:fread()将读取到的数据存放在参数ptr指向的缓冲区中;

size:fread()从文件读取nmemb个数据项,每一个数据项的大小未size个字节,所以总共读取的数据大小为nmemb*size个字节;

nmemb:参数nmemb指定了读取数据项的个数;

stream:FILE指针。

返回值:调用成功时返回读取到的数据项的数目(数据项数目并不等于实际读取的字节数,除非参数size等于1);如果发生错误或到达文件末尾,则fread()返回的值将小于参数nmemb,那么到底发生了错误还是到达了文件末尾,fread()不能区分文件结尾和错误,酒精时哪一种情况,此时可以使用ferror()或feof()函数来判断。

写文件——fwrite()函数

size_t fwrite(const void *ptr,size_t size,size_t nmemb,FILE *stream);

库函数fwrirte()用来将数据写入到文件中

ptr:将参数ptr指向缓冲区中的数据写入到文件中;

size:参数size指定了每个数据项的字节大小,与fread()函数的size参数意义相同;

nmemb:参数nmemb指定了写入的数据项个数,与fread()函数的nmemb参数意义相同;

stream:FILE指针。

返回值:调用成功时返回写入的数据的数目(数据项数目并不等于实际写入的字节数,除非参数size等于1);如果发生错误,则fwrite()返回的值将小于参数nmemb(或者等于0)。

由此可知,库函数fread()、fwrite()中指定读取或写入数据大小的方式和系统调用read()、write()不同,前者通过nmemb(数据项个数)*size(每个数据项的大小)的方式来指定数据大小,而后者则直接通过一个size参数指定数据大小。

譬如将一个struct mystr结构体数据写入到文件中,可按照以下方式写入:

fwrite(buf,sizeof(struct mystr),1,file);

也可以按照下列方式写入:

fwrite(buf,1,sizeof(struct mystr),file);

设置读写位置——fseek()函数

int fseek(FILE *stream,long offset,int whence);

函数参数和返回值如下:

stream:FILE指针;

offset:与lseek()函数的offset参数意义相同;

whence:与lseek()函数的whence参数意义相同。

返回值;成功返回0;发生错误将返回-1,并且会设置errno以指示错误原因;与lseek()函数的返回值意义不同。

调用库函数fread()、fwrite()读写文件时,文件的读写位置偏移量会自动递增,使用fseek()可手动设置文件当前的读写位置偏移量。

譬如将文件的读写位置移动到文件开头处:

fseek(file,0,SEEK_SET);

将文件的读写位置移动到文件末尾:

fseek(file,0,SEEK_END);

将文件的读写位置移动到100个字节偏移量处:

fseek(file,100,SEEK_SET);

测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
​
int main(void)
{
    FILE *f=NULL;
    int ret;
    char buf[128]={0};
    f=fopen("./test.txt","w+");  //w+表示以可读可写方式打开文件,如果参数path指定的文件存在,将文件截断为0;如果文件不存在则创建该文件
    if(NULL==f)
    {
        perror("fopen error");
        fclose(f);
        return 1;
    }
    ret=fwrite("Hello World",1,11,f);
    if(11>ret)
    {
        printf("fwrite error");
        fclose(f);
        return 1;
    }
    ret=fseek(f,0,SEEK_SET);
    if(-1==ret)
    {
    perror("fseek error");
    fclose (f);
    return 1;
    }
    ret=fread(buf,1,11,f);
    if(11>ret)
    {
        printf("fread error or -of-file\n");
        fclose(f);
        return 1;
    }
    printf("fread: %s\n",buf);
    fclose(f);
    return 0;
}

##

efeof()函数和ferror()函数

判断是否到达文件末尾——feof()函数

int feof(FILE *stream);

判断是否发生错误——ferror()函数

int ferror(FILE*stream)

清除标志——clearerr()函数

void clearerr(FILE *stream);

测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
	FILE *f=NULL;
	int ret;
	char buf[128]={0};
	f=fopen("./test.txt","w+");  //w+表示以可读可写方式打开文件,如果参数path指定的文件存在,将文件截断为0;如果文件不存在则创建该文件
	if(NULL==f)
	{
		perror("fopen error");
		fclose(f);
		return 1;
	}
	ret=fwrite("Hello World",1,11,f);
	if(11>ret)
	{
		printf("fwrite error");
		fclose(f);
		return 1;
	}
	ret=fseek(f,0,SEEK_SET);
	if(-1==ret)
	{
	perror("fseek error");
    fclose (f);
    return 1;
	}
	ret=fread(buf,1,11,f);
	if(11>ret)
	{
		if(ferrro(f))
		{
		printf("fread error");
		fclose(f);
		return 1;
		}
		else{
				if(feof(f))
				{
				printf("fread end-of-file\n");
				fclose(f);
				return 1;
				}
			}
			clearerr(f);
	}
	printf("fread: %s\n",buf);
	fclose(f);
	return 0;
}

格式化I/O

格式化输出(写操作)

include <stdio.h>

printf():用于将程序中的字符串信息输出到显示终端(也就是标准输出),函数调用成功返回打印输出的字符数;失败将返回一个负值。

int printf(const char *format,...); //将信息输出(写入)到标准输出中
printf("Hello World!\n"); //打印Hello World!
printf("%d\n",5); //打印数字5

fprintf():可将格式化数据写入到由FILE指针指定的文件中。

int fprintf(FILE *stream,const char *format,...); //将格式转换后的字符串写入到指定文件中
fprintf(stderr,"Hello World!\n"); //譬如将字符串“Hello World”写入到标准错误:
fprintf(stderr,"%d\n",5); //向标准错误写入数字5

函数调用成功返回写入到文件中的字符数;失败将返回一个负值。

dprintf():可将格式化数据写入到由文件描述符fd指定的文件中

int dprintf(int fd,const char *format,...); //将格式转换后的字符串写入到指定文件中,只是用的文件描述符
dprintf(STDERR_FILENO,"Hello World!\n");  //将字符串Hello World写入到标准错误
dprintf(STDERR_FILENO,"%d\n",5);  //向标准错误写入数字5

sprintf():将格式化数据存储在由参数buf指定的缓冲区中

int sprintf(char *buf,const char *format,...);//将格式转换后的字符串保存到buf中
char buf[100];
sprintf(buf,"Hello World!\n");

事实上,一般会使用这个函数进行格式转换,并将转换后的字符串存放在缓冲区中,譬如将数字100转换为字符串“100”,将转换后得到的字符串存放在buf中:

char buf[20]={0};
sprintf(buf,"%d",100);

sprintf()函数会在字符串尾端自动加上一个字符串终止字符'\0'。

注意:sprintf()函数可能会造成由参数buf指定的缓冲区溢出,调用者需要确保该缓冲区足够大,因为缓冲区溢出会造成程序不稳定甚至安全隐患。

函数调用成功返回写入到buf中的字节数;失败将返回一个负值。

snprintf():为了解决sprintf()函数可能发生的缓冲区溢出问题,引入了snprintf()函数;在该函数中,使用参数size显式的指定缓冲区大小,如果写入到缓冲区字节数大于参数size指定的大小,超出部分将会被丢弃,如果缓冲区空间足够大,sprintf()函数就会返回写入到缓冲区的字符数,与sprintf()函数相同,也会在字符串末尾自动添加终止字符'\0'。

若发生错误,snprintf()将返回一个负值。

int snprintf(char *buf,size_t size,const char *format,...); //将格式转换后的字符串写入到指定文件中,并指定buf的大小

格式控制字符串format:能控制后续变参的格式转换

格式控制字符串由两部分组成:普通字符(非%字符)和转换说明。普通字符会进行原样输出,每个转换说明都会对应后续一个参数,通常有几个转换说明就提供几个参数(除固定参数之外的参数),使之一一对应,用于控制对应的参数如何进行转换,如下:

printf("转换说明1 转换说明2 转换说明3",arg1,arg2,arg3);

注:这里只是举例子,实际不是这样使用。三个转换说明与参数进行一一对应,按照顺序方式一一对应。

每个转换说明都是以%字符开头,其格式如下(使用[ ]括起来的部分是可选的):

%[flags][width][.precision][length]type

flags:标志,可包含0个或多个标志;

width:输出最小宽度,表示转换后的额字符串的最小宽度;

precision:精度,前面有一个“." ;

length: 长度修饰符;

type:转换类型,指定待转换数据的类型。

可以看到:只有%和type字段是必须得,其余都是可选的

测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main (void)
{
	FILE *f=NULL;
	f = fopen("./test.txt","w+");
	if(NULL == f )
	{
		perror("fopen error");
		return 1;
	}
	fprintf(f,"Hello World %d\n",100);
	fprintf(stdout,"Hello World %d\n",100);// 写入到标准输出中,stdin stdout stderr(标准输入、标准输出、标准错误)
	
	dprintf(1,"Hello World %d\n",100);  //写入到标准输出
	dprintf(2,"Hello World %d\n",100);  //写入到标准错误
	
	return 0;
}

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main (void)
{
	char buf[128]={0};
	sprintf(buf,"Hello World %d",200);
	printf("%s\n",buf);
	return 0;
}
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main (void)
{
	char buf[128]={0};
	snprintf(buf,128,"Hello World %d",200);
	printf("%s\n",buf);
	return 0;
}

格式化输入(读操作)

scanf():可将用户输入(标准输入)的数据进行格式化转换

int scanf(const char *format,...);

scanf()函数将用户输入(标准输入)的数据进行格式化转换并进行存储,它从格式化控制字符串format参数的最左端开始,每遇到一个转换说明便将其与下一个输入数据进行“匹配”,如果二者匹配则继续,否则结束对后面输入的处理。而每遇到一个转换说明,便按该说明所描述的格式对其后的输入数据进行转换,然后将转换得到的数据存储于与其对应的输入地址中,以此类推,直到对整个输入数据的处理结果结束为止。

从函数原型可以看出,scanf()函数也是一个可变参函数,除第一个参数format之外,scanf()函数还可以有若干个输入地址(指针),这些指针指向对应的缓冲区,用于存储格式化转换后的数据;且对于每一个输入地址,在格式控制字符串format参数中必须有一个转换说明与之一一对应,即从format字符串的左端第一个转换说明对应第一个输入地址,以此类推。譬如:

int a,b,c;
scanf("%d %d %d",&a,&b,&c);

函数调用成功后,将返回成功匹配和分配的输入项的数量;如果较早匹配失败,则该数目可能小于所提供的数目,甚至为0;发生错误则返回负值。

fscanf():从FILE指针指定文件中读取数据,并将数据进行格式化转换

int fscanf(FILE *stream,const char *format,...);

fscanf()函数从指定文件中读取数据,作为格式化转换的输入数据,文件通过FILE指针指定,所以他有两个固定参数,FILE指针和格式控制字符串format。譬如从标准输入文件中读取数据进行格式化转换:

int a,b,c;
fscanf(stdin,"%d %d %d",&a,&b,&c)

函数调用成功后,将返回成功匹配和分配的输入项的数量;如果较早匹配失败,则该数目可能小于所提供的数目,甚至为0;发生错误则返回负值。

sscanf():从参数str所指向的字符串中读取数据,并进行格式化转换

int sscanf(const char *str,const char *format,...);

sscanf()将从参数str所指向的字符串缓冲区读取数据,作为格式转换的输入数据,所以他有两个固定参数,字符串str和格式控制字符串format,譬如:

char *str="5454 hello";
char buf[10];
int a;
​
sscanf(str,"%d %s",&a,buf);

函数调用成功后,将返回成功匹配和分配的输入项的数量;如果较早匹配失败,则该数目可能小于所提供的数目,甚至为0;发生错误则返回负值。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
​
int main(void)
{
    char buf[128]={0};
    
    scanf("%s",buf);
    printf("%s\n",buf);
    
    return 0;
}

测试发现:运行堵塞,标准输入没有数据,需要用户输入数据,此时在终端输入“helloworld”,显示结果如下:

helloworld
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
	char buf[128]={0};
	
	fscanf(stdin,"%s",buf);
	printf("%s\n",buf);
	
	return 0;
}

测试发现:运行堵塞,标准输入没有数据,需要用户输入数据,此时在终端输入“123456”,显示结果如下:

123456

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
	int data;
	char buf1[25];
	char buf2[25];
	
	sscanf("100 hello world","%d %s %s",&data,buf1,buf2);
	printf("%d\n",data);
	printf("%s\n",buf1);
	printf("%s\n",buf2);
	return 0;
}

测试结果:

100
hello
world

I/O缓冲

处于速度和效率的考虑,系统I/O调用(即文件I/O,open、read、write等)和标准C语言库I/O函数(即标准I/O函数)在操作磁盘文件时会对数据进行缓冲。

文件I/O的内核缓冲

read()和write()系统调用在进行文件读写操作的时候并不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区之间进行复制数据。譬如调用write()函数将5个字节数据从用户空间内存拷贝到内核空间的缓冲区中

write(fd,"Hello",5);  //写入5个字节数据

调用write()后仅仅是将这5个字节数据拷贝到内核空间的缓冲区中,拷贝完成之后函数就返回了,在后面的某个时刻,内核会将其缓冲区中的数据写入(刷新)到磁盘设备中,所以由此可知:系统调用write()与磁盘操作并不同步,write()函数并不会等待数据真正写入到磁盘之后再返回。如果再次期间,其他进程调用read()函数读取该文件的这几个字节数据,那么内核将自动从缓冲区读取这几个字节数据返回给应用程序。

读文件也是如此:内核会从磁盘设备中读取文件的数据并存储到内核的缓冲区中,当调用read()函数读取数据时,read()调用将从内核缓冲区中读取数据,直至把缓冲区数据读完,这时,内核会将文件的下一段内容读入到内核缓冲区中进行缓存。

刷新文件I/O的内核缓冲

Linux中提供了一些系统调用可用于控制文件I/O内核缓冲,包括系统调用sync()、syncfs()、fsync()以及fdatasync()。

fsync()函数

系统调用fsync()将参数fd所指文件的内容数据和元数据写入磁盘,只有在对磁盘设备的写入操作完成之后,fsync()函数才会返回,函数原型:

#include <unistd.h>

int fsync(int fd);

参数fd表示文件描述符,函数调用成功将返回0,失败返回-1并设置errno以指示错误原因。

元数据并不是文件内容本身的数据,而是一些用于记录文件属性相关的数据信息,譬如文件大小、时间戳、权限等等信息,统称为元数据,这些数据也是存储在磁盘设备中的。

测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static char buf[4096];
int main(void)
{
	int fd;
	fd=open("./test.txt",O_WRONLY|O_CREAT|O_EXCL,0666);
	if(-1==fd)
	{
		perror("open error");
		return 1;
	}
	for (int i;i<4096;i++)
		write(fd,buf,sizeof(buf));  //4096*4k=16M
		fsync(fd); //使用该函数会增加等待时间
		close(fd);
		return 0;
}

fdatasync()函数:系统调用fdatasync()与fsync()类似,不同之处在于fdatasync()仅将参数fd所指文件的内容数据写入磁盘,并不包含文件的元数据;同样,只有在对磁盘设备的写入操作完成之后,fdatasync()函数才会返回,其函数原型如下:

#include <unistd.h>

int fdatasync(int fd);

测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static char buf[4096];
int main(void)
{
	int fd;
	fd=open("./test.txt",O_WRONLY|O_CREAT|O_EXCL,0666);
	if(-1==fd)
	{
		perror("open error");
		return 1;
	}
	for (int i;i<4096;i++)
		write(fd,buf,sizeof(buf));  //4096*4k=16M
		fdatasync(fd); 
		close(fd);
		return 0;
}

sync()函数:系统调用sync()会将所有文件I/O内核缓冲区中的文件内容数据和元数据全部更新到磁盘设备中,该函数没有参数、也无返回值,意味着它不是对某一个文件进行数据更新,而是刷新所有文件I/O内核缓冲区。函数原型:

#include <unistd.h>

void sync(void);

在Linux实现中,调用sync()函数仅在所有数据已经写入到磁盘设备之后才会返回;然后再其他系统中,sync()实现只是简单调度一下I/O传递,在动作未完成之后即可返回。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static char buf[4096];
int main(void)
{
	int fd;
	fd=open("./test.txt",O_WRONLY|O_CREAT|O_EXCL,0666);
	if(-1==fd)
	{
		perror("open error");
		return 1;
	}
	for (int i;i<4096;i++)
		write(fd,buf,sizeof(buf));  //4096*4k=16M
		sync(); 
		close(fd);
		return 0;
}

控制文件I/O内核缓冲的标志

调用open()函数时,指定一些标志也可以影响到文件I/O内核缓冲,譬如:O_DSYNC标志和O_SYNC标志。

O_DSYNC标志:在调用open()函数时,指定O_DSYNC标志,其效果类似于在每个write()调用之后调用fdatasync()函数进行数据同步。

如:

fd=open(filepath,O_WRONLY|O_DSYNC);
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
​
static char buf[4096];
int main(void)
{
    int fd;
    fd=open("./test.txt",O_WRONLY|O_CREAT|O_EXCL|O_DSYNC,0666);
    if(-1==fd)
    {
        perror("open error");
        return 1;
    }
    for (int i;i<4096;i++)
        write(fd,buf,sizeof(buf));  //4096*4k=16M
    
        close(fd);
        return 0;
}

会发现运行时间特别长,就是因为O_DSYNC标志

O_SYNC标志:在调用open()函数时,指定O_SYNC标志,使得每个write()调用都会自动将文件内容和元数据刷新到磁盘设备中,其效果类似于在每个write()调用之后调用fsync()函数进行数据同步,譬如:

fd=open(filepath,O_WRONLY|O_DSYNC);
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
​
static char buf[4096];
int main(void)
{
    int fd;
    fd=open("./test.txt",O_WRONLY|O_CREAT|O_EXCL|O_SYNC,0666);
    if(-1==fd)
    {
        perror("open error");
        return 1;
    }
    for (int i;i<4096;i++)
        write(fd,buf,sizeof(buf));  //4096*4k=16M
    
        close(fd);
        return 0;
}

测试会发现时间也非常的长。

直接I/O

从Linux内核2.4版本开始,Linux允许应用程序在执行I/O操作时绕过内核缓冲区,从用户空间直接将数据传递到文件或磁盘设备,把这种操作也称为直接I/O。

文件属性与目录

7种文件类型

(1)普通文件(regular file)

(2)目录(directory)

(3)字符设备(character)和块设备(block)

(4)符号链接(link)

(5)管道(pipe)

(6)套接字(socket)

普通文件

普通文件在Linux系统下是最常见的。普通文件可以分为两类:文本文件和二进制文件。

文本文件:文件中的内容是由文本构成的,所谓的文本指的是ASCII码字符。文本中的内容其本质都是数字(因为计算机本身只有0和1,存储在磁盘上的文件内容也都是有0和1所构成),而文本文件中的数字应该被理解为这个数字所对应的ASCII字符码。文本文件的好处就是方便人阅读、浏览以及编写。

二进制文件:二进制文件中存储的本质上也是数字,只不过对于二进制文件来说,这些数字并不是文本字符编码,而是真正的数字。譬如Linux系统下的可执行文件、C代码编译之后得到的.o文件、.bin文件等都是二进制文件。

在Linux系统下,可以通过stat命令或者ls命令来查看文件类型。stat命令会直观的把文件类型显示出来;对于ls命令来说,并没有直观地直观显示文件的类型,而是通过符号表示出来。

'-' :普通文件

'd' :文件目录

'c' :字符设备文件

'b' :块设备文件

'l' :符号链接文件

's' : 套接字文件

'p' : 管道文件

目录

目录就是文件夹,文件夹在Linux系统中也是一种文件,是一种特殊文件。文件夹中记录了该文件本身的路径以及该文件夹下所存放的文件。文件夹作为一种特殊文件,本身并不适合使用文件I/O的方式来读写,在Linux系统下,会有专门的系统调用用于读写文件夹。

字符设备文件和块设备文件

Linux系统中,可以将硬件设备分为字符设备和块设备,所以就有了字符设备和块设备文件两种文件类型,虽然有设备文件,但是设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护,当系统关机时,设备文件都会消失;字符设备文件一般存放在Linux系统/dev/目录下,所以/dev也称为虚拟文件系统devfs。

文件属性与目录 -stat 函数

获取文件属性:stat函数

原型:

int stat(const char *pathname,struct  stat*buf); //用struct stat来接收获取到的文件属性信息

函数参数及返回值:

pathname:用于指定一个需要查看属性的文件路径。

buf:struct stat类型指针,用于指向一个struct stat结构体变量。调用stat函数的时候需要传入一个struct stat变量的指针,获取到的文件属性信息就记录在struct stat结构体中。

返回值:成功返回0;失败返回-1,并设置error

通过st_mode变量判断文件类型,如下(假设st是struct stat类型变量):

/*判断是不是普通文件*/
if((st.st_mode&S_IFREG)==S_IFREG)
{
/*是*/
}

/*判断是不是链接文件*/
if((st.st_mode&S_IFMT)==S_IFLNK)
{
/*是*/
}

除了上面的方式,还可以使用Linux系统封装好的宏来进行判断,如下所示(m是st_mode变量):

S_ISREG(m)   #判断是不是普通文件,如果是返回true,否则返回false
S_ISDIR(m)    #判断是不是目录,如果是返回true,否则返回false
S_ISCHR(m)     #判断是不是字符设备文件,如果是返回true,否则返回false
S_ISBLK(m)     #判断是不是块设备文件,如果是返回true,否则返回false
S_ISFIFO(m)    #判断是不是管道文件,如果是返回true,否则返回false
S_ISLNK(m)    #判断是不是链接文件,如果是返回true,否则返回false
S_ISSOCK(m)       #判断是不是套接字文件,如果是返回true,否则返回false

有了这些宏之后,就可以通过如下方式判断文件类型了:

/*判断是不是普通文件*/
if(S_ISREG(st.st_mode))
{
/* 是 */
}

/*判断是不是目录*/
if(S_ISDIR(st.st_mode))
{
/* 是 */
}

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
	struct stat st;
	int ret;
	
	ret =stat("./test.txt",&st);
	if(-1==ret)
	{
	perror("stat error");
	return 1;
	}
	
	printf("inode:%d\n",st.st_ino);
	printf("size: %d\n",st,st_size);
	printf("type: ");
	if(S_ISREG(st.st_mode))
		printf("普通文件");
		else if(S_ISDIR(st.st_mode))
			printf("目录");
		else if(S_ISCHR(st.st_mode))
			printf("字符设备");
		else if(S_ISBLK(st.st_mode))
			printf("块设备");
		else if(S_ISFIFO(st.st_mode))
			printf("管道");
		else if(S_ISLNK(st.st_mode))
			printf("链接");
		else if(S_ISSOCK(st.st_mode))
			printf("套接字");
		printf("\n");
		return 0;
}

struct stat结构体

struct是内核定义的一个结构体,这个结构体对中的所有元素加起来构成了文件的属性信息,结构体内容如下所示:

struct stat
{
    dev_t st_dev; /* 文件所在设备的 ID */
    ino_t st_ino; /* 文件对应 inode 节点编号 */
    mode_t st_mode; /* 文件对应的模式 */
    nlink_t st_nlink; /* 文件的链接数 */
    uid_t st_uid; /* 文件所有者的用户 ID */
    gid_t st_gid; /* 文件所有者的组 ID */
    dev_t st_rdev; /* 设备号(指针对设备文件) */
    off_t st_size; /* 文件大小(以字节为单位) */
    blksize_t st_blksize; /* 文件内容存储的块大小 */
    blkcnt_t st_blocks; /* 文件内容所占块数 */
    struct timespec st_atim; /* 文件最后被访问的时间 */
    struct timespec st_mtim; /* 文件内容最后被修改的时间 */
    struct timespec st_ctim; /* 文件状态最后被改变的时间 */
};

st_dev;该字段用于描述此文件所在的设备。不常用
st_ino:文件的inode编号。
st_mode:该字段用于描述文件的模式,譬如文件类型、文件权限都记录在该变量中
st_nlink:该字段用于记录文件的硬链接数,也就是为该文件创建了多少个硬链接文件。链接文件可以
分为软链接(符号链接)文件和硬链接文件
st_uid、st_gid:此两个字段分别用于描述文件所有者的用户 ID 以及文件所有者的组 ID
st_rdev:该字段记录了设备号,设备号只针对于设备文件,包括字符设备文件和块设备文件,不用理会。
st_size:该字段记录了文件的大小(逻辑大小),以字节为单位。
st_atim、st_mtim、st_ctim:此三个字段分别用于记录文件最后被访问的时间、文件内容最后被修改的时
间以及文件状态最后被改变的时间,都是 struct timespec 类型变量

st_mode变量

st_mode是struct stat结构体中的一个成员变量,是一个32位无符号整形数据,该变量记录了文件的类型、文件权限的信息,表示方法如下:

open函数的第三个参数mode也有类似的图。唯一不同的在与open函数的mode参数只涉及到S、U、G、O这12个bit位,并不包含用于描述文件类型的4个bit位。

O 对应的 3 个 bit 位用于描述其它用户的权限; G 对应的 3 个 bit 位用于描述同组用户的权限; U 对应的 3 个 bit 位用于描述文件所有者的权限; S 对应的 3 个 bit 位用于描述文件的特殊权限。

这些bit位表达内容与open函数的mode参数相对应。

struct timespec结构体

该结构体定义在<time.h>头文件中,是Linux系统中时间相关的结构体。应用程序中包含了<time.h>头文件,就可以在应用程序中使用该结构体了。结构体内容如下所示:

struct timespec
{
    time_t tv_sec;
    syscall_slong_t tv_nsec;
}

struct timespec结构体中只有两个成员变量 ,一个秒(tv_sec)、一个纳秒(tv_nsec),time_t其实指的就是long int类型,所以由此可知,该结构体所表示的时间可以精确到纳秒,对于文件的时间属性来说,并不需要那么高的精度,往往只需要精确到秒级别即可。

编程练习:

#include <sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
​
int main(void)
{
    struct stat file_stat;
    int ret;
    
    ret=stat("./test_file",&file_stat);
    if(-1==ret)
{
        perror("stat error");
        exit(-1);
}
    printf("file size:%ld bytes\n"
                "inode number:%ld\n",file_stat,st_size,
                file_stat,st_ino);
    exit(0);
}

fstat和lstat函数

fstat 与 stat 区别在于,stat 是从文件名出发得到文件属性信息,不需要先打开文件;而 fstat 函数则是从 文件描述符出发得到文件属性信息,所以使用 fstat 函数之前需要先打开文件得到文件描述符。具体该用 stat 还是 fstat,看具体的情况;譬如,并不想通过打开文件来得到文件属性信息,那么就使用 stat,如果文件已 经打开了,那么就使用 fstat。

fstat 函数原型如下(可通过"man 2 fstat"命令查看):

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int fstat(int fd, struct stat *buf);

示例:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 struct stat file_stat;
 int fd;
 int ret;
 /* 打开文件 */
 fd = open("./test_file", O_RDONLY);
 if (-1 == fd) {
 perror("open error");
 exit(-1);
 }
 /* 获取文件属性 */
 ret = fstat(fd, &file_stat);
 if (-1 == ret)
 perror("fstat error");
 close(fd);
 exit(ret);
}

lstat函数

lstat()与 stat、fstat 的区别在于,对于符号链接文件,stat、fstat 查阅的是符号链接文件所指向的文件对 应的文件属性信息,而 lstat 查阅的是符号链接文件本身的属性信息。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int lstat(const char *pathname, struct stat *buf);

目录存储形式

目录快当中有多个目录项(或叫目录条),每一个目录项都会对应目录下的某一个文件,目录项当中记录了该文件的文件名以及它的inode节点编号,所以通过目录的目录快便可以遍历找到该目录下的所有文件以及对应的inode节点。

对比:

普通文件由inode节点和数据块构成

目录由inode节点和目录块构成

创建和删除目录

使用open函数可以创建一个普通文件,但不能用于创建目录文件,在Linux系统下,提供了专门用于创建目录mkdir()以及删除目录rmdir相关的系统调用。

mkdir函数

#include <sys/stat.h>
#include <sys/types.h>

int mkdir(const char *pathname,mode_t mode);

函数参数和返回值含义;

pathname:需要创建的目录路径

mode:新建目录的权限设置,设置方式与open函数的mode参数一样,最终权限为(mode&~umask)。

返回值:成功返回0;失败返回-1,并会设置errno。

pathname:参数指定的新建目录的路径,该路劲名可以是相对路劲,也可以是绝对路径,若指定的路径名已经存在,则调用mkdir()将会失败。

mode参数指定了新目录的权限,目录拥有与普通文件相同的权限位,但是其表示的含义与普通文件却有不同。

mkdir()函数测试

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(void)
{
 int ret;
 ret = mkdir("./new_dir", S_IRWXU |
 S_IRGRP | S_IXGRP |
 S_IROTH | S_IXOTH);
 if (-1 == ret) {
 perror("mkdir error");
 exit(-1);
 }
 exit(0);
}

上述代码中,通过mkdir函数在当前目录下创建了一个目录new_dir,并将其权限设置为0755(八进制),编译运行;

rmdir函数

rmdir()用以删除一个目录

#include <unistd.h>
int rmdir(const char *pathname);

函数参数和返回值含义:

pathname:需要删除的目录对应的路径名,并且该目录必须是一个空目录,也就是该目录下只有.和..这两个目录项;pathname指定的路径名不能是软链接文件,即使该链接文件指向了一个空目录。返回值:成功返回0;失败将返回-1,并设置errno。

rmdir函数测试

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
 int ret;
 ret = rmdir("./new_dir");
 if (-1 == ret) {
 perror("rmdir error");
 exit(-1);
 }
 exit(0);
}
​

打开、读取以及关闭目录

打开、读取、关闭一个普通文件可以使用open()、read()、close(),而对于目录来说,可以使用opendir()、readdir()和closedir()来打开、读取以及关闭目录。

打开文件opendir

opendir()函数用于打开一个目录,并返回指向该目录的句柄,供后续操作使用。Opendir是一个C库函数,opendir()函数原型如下所示:

#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);

函数参数和返回值含义如下:

name:指定需要打开的目录路径名,可以是绝对路径,也可以是相对路径。

返回值:成功返回指向该目录的句柄,一个DIR指针(实质是一个结构体指针),其作用类似于open函数返回的文件描述符发fd,后续对该目录的操作需要使用DIR指针变量;若调用失败,则返回NULL。

读取目录readdir

readdir()用于读取目录,获取目录下所有文件的名称以及对应的inode号。

#include <dirent.h>
struct dirent *readdir(DIR *dirp);

函数参数和返回值含义如下:

dirp:目录句柄DIR指针。

返回值:返回一个指向struct dirent结构体表示dirp指向的目录流中的下一个目录条目。在到达目录流的末尾或发生错误时,他将返回NULL。

Tips:“流”是从自然界中抽象出来的一种概念,有点类似于自然界当中的水流,在文件操作中,文件 内容数据类似池塘中存储的水,N 个字节数据被读取出来或将 N 个字节数据写入到文件中,这些数据就构 成了字节流。 “流”这个概念是动态的,而不是静态的。编程当中提到这个概念,一般都是与 I/O 相关,所以也经 常叫做 I/O 流;但对于目录这种特殊文件来说,这里将目录块中存储的数据称为目录流,存储了一个一个 的目录项(目录条目)。

struct dirent结构体内容如下所示:

struct dirent {
 ino_t d_ino; /* inode 编号 */
 off_t d_off; /* not an offset; see NOTES */
 unsigned short d_reclen; /* length of this record */
 unsigned char d_type; /* type of file; not supported by all filesystem types */
 char d_name[256]; /* 文件名 */
};

对于struct dirent结构体,只需关注d_ino和d_name两个字段即可,分别记录了文件的inode编号和文件名。

字符串处理

字符串输出

常用字符串输出函数有putchar()、puts()、fputc()、fputs(),前面使用较多的printf()函数,而并没有用到上述函数,原因在于printf()可以按照自己规定的格式输出字符串信息,一般称为格式化输出;而putchar()、puts()、fputs()这些函数只能输出字符串,不能进行格式转换。

与 printf()一样,putchar()、puts()、fputc()、fputs()这些函数也是标准 I/O 函数,属于标准 C 库函数,所 以需要包含头文件,并且它们也使用 stdio 缓冲。

puts()函数

该函数用来向标准输出设备(屏幕、显示器)输出字符串并自行换行。把字符串输出到标准输出设备,将'\0'转换为换行符'\n'。puts函数原型如下(可通过"man 3 puts"命令查看):

#include <stdio.h>
int puts(const char *s);

函数参数和返回值含义:

s:需要输出的字符串。

返回值:成功返回一个非负数;失败将返回EOF,EOF其实就是-1。

使用puts()函数,函数内部会自动在其后添加一个换行符。所以,如果只是单纯输出字符串到标准输出设备,而不包含数字格式化转换操作,那么使用puts()会更加方便。

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 char str[50] = "Linux app puts test";
 puts("Hello World!");
 puts(str);
 exit(0);
}

putchar函数

putchar()函数可以把参数c指定的字符(一个无符号字符)输出到标准输出设备,其输出可以是一个字符,可以是介于0~127之间的一个十进制整型数(包含0和127,输出其对应的ASCII码字符),也可以是用char类型定义好的一个字符变量。putchar 函数原型如下所示(可通过"man 3 putchar"命令查看):

#include <stdio.h>
int putchar(int c);

函数参数和返回值含义:

c:需要进行输出的字符。

返回值:出错将返回EOF.

putchar函数测试

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 putchar('A');
 putchar('B');
 putchar('C');
 putchar('D');
 putchar('\n');
 exit(0);
}

fputc函数

fputc()与putchar()类似,也用于输出参数c指定的字符(一个无符号字符),与putchar()区别在于,putchar()只能输出到标准输出设备,而fputc()可把字符输出到指定文件中,既可以是标准输出、标准错误设备,也可以是一个普通文件。

fputc 函数原型如下所示:

#include <stdio.h>
int fputc(int c, FILE *stream);

函数参数和返回值含义如下:

c:需要进行输出的字符。

stream:文件指针。

返回值:成功时返回输出的字符;出错将返回EOF。

(1)使用 fputc 函数将字符输出到标准输出设备。

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 fputc('A', stdout);
 fputc('B', stdout);
 fputc('C', stdout);
 fputc('D', stdout);
 fputc('\n', stdout);
 exit(0);
}

(2)使用 fputc 函数将字符输出到一个普通文件。

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 FILE *fp = NULL;
 /* 创建一个文件 */
 fp = fopen("./new_file", "a");
 if (NULL == fp) {
 perror("fopen error");
 exit(-1);
 }
 /* 输入字符到文件 */
 fputc('A', fp);
 fputc('B', fp);
 fputc('C', fp);
 fputc('D', fp);
 fputc('\n', fp);
 /* 关闭文件 */
 fclose(fp);
 exit(0);
}

fputs函数

同理,fputs()与 puts()类似,也用于输出一条字符串,与 puts()区别在于,puts()只能输出到标准输出设 备,而 fputs()可把字符串输出到指定的文件中,既可以是标准输出、标准错误设备,也可以是一个普通文件。

#include <stdio.h>
int fputs(const char *s, FILE *stream);

函数参数和返回值含义:

s:需要输出的字符串。

stream:文件指针。

返回值:成功返回非负数;失败将返回EOF。

(1)使用 fputs 输出字符串到标注输出设备。

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 fputs("Hello World! 1\n", stdout);
 fputs("Hello World! 2\n", stdout);
 exit(0);
}

(2)使用 fputs 输出字符串到一个普通文件。

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 FILE *fp = NULL;
 /* 创建一个文件 */
 fp = fopen("./new_file", "a");
 if (NULL == fp) {
 perror("fopen error");
 exit(-1);
 }
 fputs("Hello World! 1\n", fp);
 fputs("Hello World! 2\n", fp);
 /* 关闭文件 */
 fclose(fp);
 exit(0);
}

字符串输入

常用的字符串输入函数有gets()、getchar()、fgetc()、fgets()。与printf()对应,在C库函数中同样也提供了格式化输入函数scanf()。scanf()与gets()、getchar()、fgetc()、fgets()这些函数相比在功能上有优势,但是在使用上不如他们方便。

与 scanf()一样,gets()、getchar()、fgetc()、fgets()这些函数也是标准 I/O 函数,属于标准 C 库函数,所 以需要包含头文件,并且它们也使用 stdio 缓冲。

gets函数

该函数用于从标准输入设备(譬如键盘)中获取用户输入的字符串,ges()函数原型如下所示:

#include <stdio.h>
char*gets(char*s);

函数参数和返回值:

s:指向字符数组的指针,用于存储字符串。

返回值:如果成功,该函数返回指向s的指针;如果发生错误或者到达末尾时还未读取任何字符,则返回NULL。

用户从键盘输入的字符串数据首先会存放在一个输入缓冲区中,gets()函数会从输入缓冲区中读取字符串到字符指针变量s所指向的内存空间,当从输入缓冲区读走字符后,相应的字符边不存在于缓冲区了。

输入的字符串中就算有空格也可以直接输入,字符串输入完成之后按回车即可,gets()函数不检查缓冲区溢出。

示例:使用gets()函数获取用户输入字符串:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	char str[100]={0};
	char *ptr=NULL;
	ptr=gets(str);
	if(NULL==ptr)
		exit(-1);
	puts(str);
	exit(0);
}

但是,在程序中使用gets()函数是非常不安全的,可能会出现bug、出现不可靠性,gets()在某些意外情况下会导致程序陷入不可控状态。所以,一般不使用这个函数。

gets()与 scanf()的区别:

gets()函数不仅比 scanf 简洁,而且,就算输入的字符串中有空格也可以,因为 gets()函数允许输入 的字符串带有空格、制表符,输入的空格和制表符也是字符串的一部分,仅以回车换行符作为字符 串的分割符;而对于 scanf 以%s 格式输入的时候,空格、换行符、TAB 制表符等都是作为字符串 分割符存在,即分隔符前后是两个字符串,读取字符串时并不会将分隔符读取出来作为字符串的组 成部分,一个%s 只能读取一个字符串,若要多去多个字符串,则需要使用多个%s、并且需要使用 多个字符数组存储。

gets()会将回车换行符从输入缓冲区中取出来,然后将其丢弃,所以使用 gets()读走缓冲区中的字符 串数据之后,缓冲区中将不会遗留下回车换行符;而对于 scanf()来说,使用 scanf()读走缓冲区中 的字符串数据时,并不会将分隔符(空格、TAB 制表符、回车换行符等)读走将其丢弃,所以使 用 scanf()读走缓冲区中的字符串数据之后,缓冲区中依然还存在用户输入的分隔符。

getchar函数

该函数用于从标准输入设备中读取一个字符(一个无符号字符),函数原型如下所示:

#include <stdio.h>
int getchar(void);

函数参数和返回值:

无需传参。或

返回值:该函数以无符号char强制转换为int的形式返回读取的字符,如果文件到达末尾或发生读错误,则返回EOF。

同样,getchar()函数也是从输入缓冲区读取字符数据,但只读取一个字符,包括空格、TAB制表符、换行回车符等。

getchar()函数使用示例:

#include <stdio.h>
#include <stdlib.h>

int main (void)
{
	int ch;
	ch=getchar();
	printf("ch;%c",ch);
	exit(0);
}

getchar()只从输入缓冲区中读取一个字符,与scanf以%c格式读取一样,空格、TAB制表符、回车符都将是正常字符,即使输入了多个字符,但getchar()仅读取一个字符。

fgets函数

fgets()与gets()一样用于获取输入的字符串,fgets()函数原型如下:

#include <stdio.h>
char *fgets(char*s,int size,FILE*stream);

函数参数和返回值:

s:指向字符数组的指针,用于存储字符串。

size:这是要读取的最大字符数。

stream:文件指针。

fgets()与 gets()的区别主要是三点:

gets()只能从标准输入设备中获取输入字符串,而 fgets()既可以从标准输入设备获取字符串、也可 以从一个普通文件中获取输入字符串。

fgets()可以设置获取字符串的最大字符数。

gets()会将缓冲区中的换行符'\n'读取出来、将其丢弃、将'\n'替换为字符串结束符'\0';fgets()也会将 缓冲区中的换行符读取出来,但并不丢弃,而是作为字符串组成字符存在,读取完成之后自动在最 后添加字符串结束字符'\0'。

示例:

fgets从标准输入设备获取字符串

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char str[100]={0};
    printf("请输入字符串:");
    fgets(str,sizeof(str),stdin);
    printf("%s",str);
    exit(0);
}

fgets从普通文件中输入字符串:

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char str[100]={0};
    FILE*fp=NULL;
    
    /*打开文件*/
    fp=fopen("./test_file","r");
    if(NULL==fp)
    {
    perror("fopen error");
    exit(-1);
    }
/*从文件中输入字符串*/
fgets(str,sizeof(str),fp);
printf("%s",str);
​
/*关闭文件*/
fclose(fp);
exit(0);
}

使用fgets()读取文件中输入的字符串,文件指针会随着读取字符串字节数向前移动。

fgetc函数

fgetc()和getchar()一样,用于读取一个输入字符串,函数原型如下所示:

#include <stdio.h>
int fgetc(FILE*stream);

函数参数和返回值含义:

stream:文件指针。

返回值:该函数以无符号char强制转换为int的形式返回读取的字符,如果达到文件末尾或

发生读错误,则返回EOF。

fgetc()与 getchar()的区别在于,fgetc 可以指定输入字符的文件,既可以从标准输入设备输入字符,也可 以从一个普通文件中输入字符,其它方面与 getchar 函数相同。

测试:

fgetc 从标准输入设备中输入字符

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 int ch;
 ch = fgetc(stdin);
 printf("%c\n", ch);
 exit(0);
}

fgetc 从普通文件中输入字符

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 int ch;
 FILE *fp = NULL;
 /* 打开文件 */
 fp = fopen("./test_file", "r");
 if (NULL == fp) 
 {
 perror("fopen error");
 exit(-1);
 }
 /* 从文件中输入一个字符 */
 ch = fgetc(fp);
 printf("%c\n", ch);
/* 关闭文件 */
 fclose(fp);
 exit(0);
}

字符串长度

C语言函数库中有一个用于计算字符串长度的函数strlen(),函数原型如下:

#include <string.h>
size_t strlen(const char*s);

函数参数和返回值:

s:需要进行长度计算的字符串,字符串必须包含结束字符'\0'。

返回值:返回字符串长度(以字节为单位),字符串结束字符'\0'不在计算之内。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
 char str[] = "Linux app strlen test!";
 printf("String: \"%s\"\n", str);
 printf("Length: %ld\n", strlen(str));
 exit(0);
}

sizeof和strlen的区别

sizeof 是 C 语言内置的操作符关键字,而 strlen 是 C 语言库函数;

sizeof 仅用于计算数据类型的大小或者变量的大小,而 strlen 只能以结尾为' \0 '的字符串作为参数;

编译器在编译时就计算出了 sizeof 的结果,而 strlen 必须在运行时才能计算出来;

sizeof 计算数据类型或变量会占用内存的大小,strlen 计算字符串实际长度。

字符串拼接

C语言函数库提供了strcat()函数或strncat()函数用于将两个字符串连接(拼接起来),strcat函数原型如下:

#include <string.h>
char*strcat(char*dest,const char *src);

函数参数和返回值:

dest:目标字符串。

src:源字符串。

返回值:返回指向目标字符串dest的指针。

strcat()函数会把src所指向的字符串追加到dest所指向的字符串末尾,所以必须保证dest有足够的存储空间来容纳两个字符串,否则会导致溢出错误;dest末尾的'\0'结束字符会被覆盖,src末尾的结束字符'\0'会一起被复制过去,最终的字符串只有一个'\0'。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
 char str1[100] = "Linux app strcat test, ";
 char str2[] = "Hello World!";
 strcat(str1, str2);
 puts(str1);
 exit(0);
}

strncat函数

strncat()与 strcat()的区别在于,strncat 可以指定源字符串追加到目标字符串的字符数量,strncat 函数原 型如下所示:

#include <string.h>
char *strncat(char *dest, const char *src, size_t n);

函数参数和返回值:

dest:目标字符串。

src:源字符串。

n:要追加的最大字符数。

返回值:返回指向目标字符串dest的指针。

如果源字符串src包含n个或更多个字符,则strncat()将n+1个字节追加到dest目标字符串(src中的n个字符加上结束字符'\0')。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
 char str1[100] = "Linux app strcat test, ";
 char str2[] = "Hello World!";
 strncat(str1, str2, 5);
 puts(str1);
 exit(0);
}

字符串拷贝

C语言函数库中提供了strcpy()函数和strncpy()函数用于实现字符串的拷贝,strcpy函数原型如下:

#include <string.h>
char *strcpy(char*dest,const char*src);

函数参数和返回值:

dest:目标字符串。

src:源字符串。

返回值:返回指向目标字符串dest的指针。

strcpy()会把 src(必须包含结束字符' \0 ')指向的字符串复制(包括字符串结束字符' \0 ')到 dest,所以 必须保证 dest 指向的内存空间足够大,能够容纳下 src 字符串,否则会导致溢出错误。

strcpy测试:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
 char str1[100] = {0};
 char str2[] = "Hello World!";
 strcpy(str1, str2);
 puts(str1);
 exit(0);
}

strncpy函数

strncpy()与 strcpy()的区别在于,strncpy()可以指定从源字符串 src 复制到目标字符串 dest 的字符数量, strncpy 函数原型如下所示:

#include <string.h>
char *strncpy(char *dest, const char *src, size_t n);

函数参数和返回值:

dest:目标字符串。

src:源字符串。

n:从src中复制的最大字符数。

返回值:返回指向目标字符串dest的指针。

把 src 所指向的字符串复制到 dest,最多复制 n 个字符。当 n 小于或等于 src 字符串长度(不包括结束 字符的长度)时,则复制过去的字符串中没有包含结束字符' \0 ';当 n 大于 src 字符串长度时,则会将 src 字符串的结束字符' \0 '也一并拷贝过去,必须保证 dest 指向的内存空间足够大,能够容纳下拷贝过来的字符 串,否则会导致溢出错误。

strncpy测试:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
 char str1[100] = "AAAAAAAAAAAAAAAAAAAAAAAA";
 char str2[] = "Hello World!";
 strncpy(str1, str2, 5);
 puts(str1);
 puts("~~~~~~~~~~~~~~~");
 strncpy(str1, str2, 20);
 puts(str1);
 exit(0);
}

内存填充

在编程中,经常需要将某一块内存中的数据全部设置为指定的值,譬如在定义数组、结构体这种类型变 量时,通常需要对其进行初始化操作,而初始化操作一般都是将其占用的内存空间全部填充为 0。

memset函数

menset()函数用于将某一块内存的数据全部设置为指定的值,函数原型:

#include <string.h>
void*memset(void*s,int c,size_t n);

函数参数和返回值:

s:需要进行数据填充的内存空间起始地址。

c:要被设置的值,该值以int类型传递。

n:填充的字节数。

返回值:返回值 向内存空间s的指针。

参数c虽然是以int类型传递,但menset()函数在填充内存块时是使用该值的无符号字符形式,也就是函数内部会将该值转换为unsigned char类型的数据,以字节为单位进行填充。

menset测试:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
 char str[100];
 memset(str, 0x0, sizeof(str));
 exit(0);
}

bzero函数

bzero()函数用于将一段内存空间中的数据全部设置为0,函数原型如下:

#include <string.h>
void bzero(void*s,size_t n);

函数参数和返回值:

s:内存空间的起始地址。

n:填充的字节数。

返回值:无返回值。

bzero测试:

对数组str进行初始化操作,将其存储的数据全部设置为0:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
 char str[100];
 bzero(str, sizeof(str));
 exit(0);
}

字符串比较

C语言函数库提供了用于字符串比较的函数strcmp和strncmp,strcmp函数原型:

#include <string.h>
int strcmp(const char*s1,const char*s2);

函数参数和返回值含义:

s1:进行比较的字符串1。

s2:进行比较的字符串2。

返回值:

如果返回值小于 0,则表示 str1 小于 str2

如果返回值大于 0,则表示 str1 大于 str2

如果返回值等于 0,则表示字符串 str1 等于字符串 str2

strcmp 进行字符串比较,主要是通过比较字符串中的字符对应的 ASCII 码值,strcmp 会根据 ASCII 编 码依次比较 str1 和 str2 的每一个字符,直到出现了不同的字符,或者某一字符串已经到达末尾(遇见了字 符串结束字符' \0 ')。

strcmp测试:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
 printf("%d\n", strcmp("ABC", "ABC"));
 printf("%d\n", strcmp("ABC", "a"));
 printf("%d\n", strcmp("a", "ABC"));
 exit(0);
}

strncmp函数

strncmp()与 strcmp()函数一样,也用于对字符串进行比较操作,但最多比较前 n 个字符,strncmp()函数 原型如下所示:

#include <string.h>
int strncmp(const char *s1, const char *s2, size_t n);

函数参数和返回值:

s1:参与比较的第一个字符串。

s2:参与比较的第二个字符串。

n:最多比较前 n 个字符。 返回值:返回值含义与 strcmp()函数相同。

strncmp测试:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
 printf("%d\n", strncmp("ABC", "ABC", 3));
 printf("%d\n", strncmp("ABC", "ABCD", 3));
 printf("%d\n", strncmp("ABC", "ABCD", 4));
 exit(0);
}

字符串查找

C 语言函数库中也提供了一些用于字符串查找的函数,包括 strchr()、 strrchr()、strstr()、strpbrk()、index()以及 rindex()等。

strchr函数

使用strchr函数可以查找到给定字符串当中的某一个字符,函数原型:

#include <string.h>
char*strchr(const char*s,int c);

函数参数和返回值:

s:给定目标字符串。

c:需要查找的字符串。

返回值:返回字符c第一次在字符串s中出现的位置,如果未找到字符c,则返回NULL。字符串结束字符'\0'也将作为字符串的一部分,因此如果参数c指定为'\0',则函数将返回指向结束 字符的指针。

字符串与数字互转

给应用程序传参

如果在执行应用程序时,需要向应用程序传递参数,写法如下:

int main(int argc,char**argv)
{
    /*代码*/
}

或者写成如下形式:

int main(int argc,char*argv[])
{
    /*代码*/
}

传递进来的参数以字符串的形式存在,字符串的起始地址存储在argv数组中,参数argc表示传递进来的参数个数,包括应用程序自身路径名,多个不同的参数之间使用空格分隔开来,如果参数本身带有空格、则可以使用双引号" "或者单引号' '形式来表示。

测试:获取执行应用程序时,向应用程序传递的参数:

打印传递给应用程序的参数

#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
	int i=0;
	printf("Number of parameter:%d\n",argc);
	for (i=0;i<argc;i++)
	printf("%s\n",argv[i]);
	exit(0);
}

正则表达式

初识正则表达式

又叫做规则表达式,正则表达式通常被用来检索、替换那些符合某个模式(规则)的字符串,正则表达式描述了一种字符串的匹配模式,可以用来检测一个给定字符串中是否含有某种子字符串、将匹配到的字符串替换或者从某个字符串中提取出符合某个条件的子字符串。

正则表达式也是一个字符串,该字符串由普通字符(譬如,数字0~9、大小写字母以及其他字符)和特殊字符(称为元字符)所组成,由这些字符组成的一个规则字符串,这个规则字符串用来表达对给定字符串的一种查找、匹配逻辑。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值