标准IO&文件IO

代码规范(以内核代码为例)

缩进和空白

  1. 由于UNIX系统标准的字符终端是24行80列的,接近或大于80个字符的较长语句要折行写,折行后用空格和上面的表达式或参数对齐

  2. 有的人喜欢在变量定义语句中用Tab字符,使变量名对齐,这样看起来很美观。

int a,b;
char c;

  1. 代码中每个逻辑段落之间应该用一个空行分隔开。例如每个函数定义之间应该插入一个空行,头文件、全局变量定义和函数定义之间也应该插入空行,例如
#include <stdio.h>
#include <stdlib.h>

int g;
double h;

int foo(void)
{
       →语句列表
}

int bar(int a)
{
       →语句列表
}

int main(void)
{
       →语句列表
}
  1. 一个函数的语句列表如果很长,也可以根据相关性分成若干组,用空行分隔。这条规定不是严格要求,通常把变量定义组成一组,后面加空行,return语句之前加空行,例如:
int main(void)
{
       →int    →a, b;
       →double →c;

       →语句组1

       →语句组2

       →return 0;
}

注释

  1. 整个源文件的顶部注释。说明此模块的相关信息,例如文件名、作者和版本历史等,顶头写不缩进。例如内核源代码目录下的kernel/sched.c文件的开头:
/*
 *  kernel/sched.c
 *
 *  Kernel scheduler and related syscalls
 *
 *  Copyright (C) 1991-2002  Linus Torvalds
 *
 *  1996-12-23  Modified by Dave Grothe to fix bugs in semaphores and
 *              make semaphores SMP safe
 *  1998-11-19  Implemented schedule_timeout() and related stuff
 *              by Andrea Arcangeli
 *  2002-01-04  New ultra-scalable O(1) scheduler by Ingo Molnar:
 *              hybrid priority-list and round-robin design with
 *              an array-switch method of distributing timeslices
 *              and per-CPU runqueues.  Cleanups and useful suggestions
 *              by Davide Libenzi, preemptible kernel bits by Robert Love.
 *  2003-09-03  Interactivity tuning by Con Kolivas.
 *  2004-04-02  Scheduler domains code by Nick Piggin
 */
  1. 函数注释。说明此函数的功能、参数、返回值、错误码等,写在函数定义上侧,和此函数定义之间不留空行,顶头写不缩进。

  2. 相对独立的语句组注释。对这一组语句做特别说明,写在语句组上侧,和此语句组之间不留空行,与当前语句组的缩进一致。

  3. 代码行右侧的简短注释。对当前代码行做特别说明,一般为单行注释,和代码之间至少用一个空格隔开,一个源文件中所有的右侧注释最好能上下对齐。内核源代码目录下的lib/radix-tree.c文件中的一个函数包含了上述三种注释:

/**
 *      radix_tree_insert    -    insert into a radix tree
 *      @root:          radix tree root
 *      @index:         index key
 *      @item:          item to insert
 *
 *      Insert an item into the radix tree at position @index.
 */
int radix_tree_insert(struct radix_tree_root *root,
                        unsigned long index, void *item)
{
        struct radix_tree_node *node = NULL, *slot;
        unsigned int height, shift;
        int offset;
        int error;

        /* Make sure the tree is high enough.  */
        if ((!index && !root->rnode) ||
                        index > radix_tree_maxindex(root->height)) {
                error = radix_tree_extend(root, index);
                if (error)
                        return error;
        }

        slot = root->rnode;
        height = root->height;
        shift = (height-1) * RADIX_TREE_MAP_SHIFT;

        offset = 0;                     /* uninitialised var warning */
        do {
                if (slot == NULL) {
                        /* Have to add a child node.  */
                        if (!(slot = radix_tree_node_alloc(root)))
                                return -ENOMEM;
                        if (node) {
                                node->slots[offset] = slot;
                                node->count++;
                        } else
                                root->rnode = slot;
                }

                /* Go a level down */
                offset = (index >> shift) & RADIX_TREE_MAP_MASK;
                node = slot;
                slot = node->slots[offset];
                shift -= RADIX_TREE_MAP_SHIFT;
                height--;
        } while (height > 0);

        if (slot != NULL)
                return -EEXIST;

        BUG_ON(!node);
        node->count++;
        node->slots[offset] = item;
        BUG_ON(tag_get(node, 0, offset));
        BUG_ON(tag_get(node, 1, offset));

        return 0;
}

函数内的注释要尽可能少用。写注释主要是为了说明你的代码“能做什么”(比如函数接口定义),而不是为了说明“怎样做”,只要代码写得足够清晰,“怎样做”是一目了然的,如果你需要用注释才能解释清楚,那就表示你的代码可读性很差,除非是特别需要提醒注意的地方才使用函数内注释。

  1. 复杂的结构体定义比函数更需要注释。例如内核源代码目录下的kernel/sched.c文件中定义了这样一个结构体:
/*
 * This is the main, per-CPU runqueue data structure.
 *
 * Locking rule: those places that want to lock multiple runqueues
 * (such as the load balancing or the thread migration code), lock
 * acquire operations must be ordered by ascending &runqueue.
 */
struct runqueue {
        spinlock_t lock;

        /*
         * nr_running and cpu_load should be in the same cacheline because
         * remote CPUs use both these fields when doing load calculation.
         */
        unsigned long nr_running;
#ifdef CONFIG_SMP
        unsigned long cpu_load[3];
#endif
        unsigned long long nr_switches;

        /*
         * This is part of a global counter where only the total sum
         * over all CPUs matters. A task can increase this counter on
         * one CPU and if it got migrated afterwards it may decrease
         * it on another CPU. Always updated under the runqueue lock:
         */
        unsigned long nr_uninterruptible;

        unsigned long expired_timestamp;
        unsigned long long timestamp_last_tick;
        task_t *curr, *idle;
        struct mm_struct *prev_mm;
        prio_array_t *active, *expired, arrays[2];
        int best_expired_prio;
        atomic_t nr_iowait;

#ifdef CONFIG_SMP
        struct sched_domain *sd;

        /* For active balancing */
        int active_balance;
        int push_cpu;

        task_t *migration_thread;
        struct list_head migration_queue;
        int cpu;
#endif

#ifdef CONFIG_SCHEDSTATS
        /* latency stats */
        struct sched_info rq_sched_info;

        /* sys_sched_yield() stats */
        unsigned long yld_exp_empty;
        unsigned long yld_act_empty;
        unsigned long yld_both_empty;
        unsigned long yld_cnt;

        /* schedule() stats */
        unsigned long sched_switch;
        unsigned long sched_cnt;
        unsigned long sched_goidle;

        /* try_to_wake_up() stats */
        unsigned long ttwu_cnt;
        unsigned long ttwu_local;
#endif
};
  1. 复杂的宏定义和变量声明也需要注释。例如内核源代码目录下的include/linux/jiffies.h文件中的定义:
/* TICK_USEC_TO_NSEC is the time between ticks in nsec assuming real ACTHZ and  */
/* a value TUSEC for TICK_USEC (can be set bij adjtimex)                */
#define TICK_USEC_TO_NSEC(TUSEC) (SH_DIV (TUSEC * USER_HZ * 1000, ACTHZ, 8))

/* some arch's have a small-data section that can be accessed register-relative
 * but that can only take up to, say, 4-byte variables. jiffies being part of
 * an 8-byte variable may not be correctly accessed unless we force the issue
 */
#define __jiffy_data  __attribute__((section(".data")))

/*
 * The 64-bit value is not volatile - you MUST NOT read it
 * without sampling the sequence number in xtime_lock.
 * get_jiffies_64() will do this for you as appropriate.
 */
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

标识符命名

  1. 标识符命名要清晰明了,可以使用完整的单词和易于理解的缩写。短的单词可以通过去元音形成缩写,较长的单词可以取单词的头几个字母形成缩写。看别人的代码看多了就可以总结出一些缩写惯例,例如count写成cnt,block写成blk,length写成len,window写成win,message写成msg,number写成nr,temporary可以写成temp,也可以进一步写成tmp,最有意思的是internationalization写成i18n,词根trans经常缩写成x,例如transmit写成xmt。我就不多举例了,请读者在看代码时自己注意总结和积累。

  2. 内核编码风格规定变量、函数和类型采用全小写加下划线的方式命名,常量(比如宏定义和枚举常量)采用全大写加下划线的方式命名,比如上一节举例的函数名radix_tree_insert、类型名struct radix_tree_root、常量名RADIX_TREE_MAP_SHIFT等。

  3. 微软发明了一种变量命名法叫匈牙利命名法(Hungarian notation),在变量名中用前缀表示类型,例如iCnt(i表示int)、pMsg(p表示pointer)、lpszText(lpsz表示long pointer to a zero-ended string)等。Linus在[CodingStyle]中毫不客气地讽刺了这种写法:“Encoding the type of a function into the name (so-called Hungarian notation) is brain damaged - the compiler knows the types anyway and can check those, and it only confuses the programmer. No wonder MicroSoft makes buggy programs.”代码风格本来就是一个很有争议的问题,如果你接受本章介绍的内核编码风格(也是本书所有范例代码的风格),就不要使用大小写混合的变量命名方式(大小写混合的命名方式是Modern C++风格所提倡的,在C++代码中很普遍,称为CamelCase),大概是因为有高有低像驼峰一样。),更不要使用匈牙利命名法。

  4. 全局变量和全局函数的命名一定要详细,不惜多用几个单词多写几个下划线,例如函数名radix_tree_insert,因为它们在整个项目的许多源文件中都会用到,必须让使用者明确这个变量或函数是干什么用的。局部变量和只在一个源文件中调用的内部函数的命名可以简略一些,但不能太短。尽量不要使用单个字母做变量名,只有一个例外:用i、j、k做循环变量是可以的。

  5. 针对中国程序员的一条特别规定:禁止用汉语拼音做标识符,可读性极差。

函数

每个函数都应该设计得尽可能简单,简单的函数才容易维护。应遵循以下原则:

  1. 实现一个函数只是为了做好一件事情,不要把函数设计成用途广泛、面面俱到的,这样的函数肯定会超长,而且往往不可重用,维护困难。

  2. 函数内部的缩进层次不宜过多,一般以少于4层为宜。如果缩进层次太多就说明设计得太复杂了,应考虑分割成更小的函数(Helper Function)来调用。

  3. 函数不要写得太长,建议在24行的标准终端上不超过两屏,太长会造成阅读困难,如果一个函数超过两屏就应该考虑分割函数了。如果一个函数在概念上是简单的,只是长度很长,这倒没关系。例如函数由一个大的switch组成,其中有非常多的case,这是可以的,因为各case分支互不影响,整个函数的复杂度只等于其中一个case的复杂度,这种情况很常见,例如TCP协议的状态机实现。

  4. 执行函数就是执行一个动作,函数名通常应包含动词,例如get_current、radix_tree_insert。

  5. 比较重要的函数定义上侧必须加注释,说明此函数的功能、参数、返回值、错误码等。

  6. 另一种度量函数复杂度的办法是看有多少个局部变量,5到10个局部变量已经很多了,再多就很难维护了,应该考虑分割成多个函数。

indent工具

indent工具可以把代码格式化成某种风格

$ indent -kr -i8 main.c

-kr选项表示K&R风格,-i8表示缩进8个空格的长度。如果没有指定-nut选项,则每8个缩进空格会自动用一个Tab代替。注意indent命令会直接修改原文件,而不是打印到屏幕上或者输出到另一个文件,这一点和很多UNIX命令不同。可以看出,-kr -i8两个选项格式化出来的代码已经很符合本章介绍的代码风格了,添加了必要的缩进和空白,较长的代码行也会自动折行。美中不足的是没有添加适当的空行,因为indent工具也不知道哪几行代码在逻辑上是一组的,空行还是要自己动手添,当然原有的空行肯定不会被indent删去的。
Linux indent命令 | 菜鸟教程

参考文档:编码风格

标准IO

标准介绍

输入输出中C标准和POSIX标准的不同:

1.头文件stdio.h中:

标准输入输出流 FILE * 类型的文件指针

/* Standard streams. */
extern FILE stdin; / Standard input stream. */
extern FILE stdout; / Standard output stream. */
extern FILE stderr; / Standard error output stream. /
/
C89/C99 say they’re macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr

2.头文件unistd.h中:

unistd:unix standard,POSIX标准
标准文件描述符 一个非负整数的形式

/* Standard file descriptors. /
#define STDIN_FILENO 0 /
Standard input. /
#define STDOUT_FILENO 1 /
Standard output. /
#define STDERR_FILENO 2 /
Standard error output. *


man手册简要指南

man手册很有用,市面上的书多多少少内容都参考man手册
man手册:第三章–>库函数 第二章–>系统调用

对程序员来说最有用的在第七章:
第七章讲机制
如果你不明白什么叫TCP: man 7 tcp 他会给你解释tcp是什么
如果你不明白什么叫socket: man 7 socket

在查询库函数时有如下大标题:
NAME
SYNOPSIS 概要
DESCRIPTION 描述
RETURN VALUE 返回值
ERRORS
ATTRIBUTES 属性
CONFORMING TO 符合:后面是一些标准 C99.C89是官方标准,POSIX及其他是一些方言标准(表示可移植性差)
使用这些函数时,man手册中出现的头文件都要包含

I/O:input & output,是一切实现的基础 让数据可以保存到文件而不是只显示到终端
stdio 标准IO
sysio 系统调用IO(文件IO)
当两种方式都能用时,优先使用标准IO
原因:

  • 标准IO可移植性好
  • 标准IO合并系统调用,优化buffer和cache

stdio: FILE类型(结构体)贯穿始终

1. fopen();

函数原型:FILE *fopen(const char *pathname, const char *mode);

1.1 返回值:(通用规则:谁打开谁关闭)

(当看到一个函数返回值是一个指针,要多问自己这个指针到底指向的内容是静态区上的内容还是堆上的内容)

栈区?
如果保存在栈区,则函数中会有定义局部变量FILE tmp这样的语句,当函数结束后,内容空间释放,这个指针就失效了。

静态存储区?
如果保留在静态存储区,则函数中会有static FILE tmp这样的句子,而这样自始至终只有一个结构体变量,当打开第二个文件时,上一个文件的struct FILE 区域会被覆盖

堆区?
如果保存在堆区,则函数中会有FILE * tmp = malloc(sizeof(FILE))这样的语句
实际上fopen函数中有malloc()申请内存,fclose函数释放内存,一般有配对的函数free()。

如果一个函数有互逆操作,(如fopen有fclose),则这块内存一定保存在堆区,如果没有,就需要你验证一下是否在堆区了

man手册中的描述:
RETURN VALUE
Upon successful completion fopen(), fdopen() and freopen() return a FILE pointer. Otherwise, NULL is returned and errno is set to indicate the error.
返回一个FILE *类型,如果发生错误,返回NULL,并且设置全局变量errno(error number)的值,如果不尽快打印该值,该值可能稍后被覆盖

errno定义在/usr/include/asm-generic/errno-base.h errno.h 中

#include <errno.h>
errno;
a;

预处理之后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0B97aNal-1637145819826)(2.png)]
如果你还能看到errno这个变量,那就说明这是一个整型的变量,但这里errno实际上已经被私有化了,已经变成了一个宏的实现,然后把当前出错的内容映射到你当前的地址空间里面来,所以不会和别人冲突,私有化数据

1.2 mode:

r / r+ 打开一个文件的时候要求这个文件必须存在,其他的mode在文件不存在时都会创建新文件.

manual中讲到:The argument mode points to a string beginning with one of the following sequences (possibly followed by additional characters, as described below):
即识别一个字符串的开头,如果传入参数为"readwrite",只会识别到r

是否需要+b:在Windows环境下文本流和二进制流是不一样的,如果程序需要移植,那么保险起见+b

1.3 const

让用户放心使用,函数实现者表示不会改变用户传入的内容

面试题:
问:
char * ptr = “abc”;
ptr[0] = ‘x’;
字符串是"xbc"吗?
答:
“abc"是串常量,串常量不能更改,但不同环境下结果不一定,如Windows环境下turbo C编译器可以得到"xbc”

1.4 最多打开文件个数:(通用规则:是资源就有上限)

只要是资源,就会有使用的限制。

//查看fopen函数最大打开文件的个数
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>  //errno
#include <string.h> //strerror()

int main(void)
{
    int count = 0;    //打开文件数量计数

    while (1)
    {
        FILE *fp = fopen("tmp", "w+");
        if (fp == NULL)
        {
            fprintf(stderr, "fopen() failed! %s\n", strerror(errno));
            break;
        }
        count++;
    }

    printf("count = %d\n", count);

    return 0;
}

由maxfopen.c结果输出可以看到count的值为1021,但一个进程运行时默认打开三个流:
1.stdin 2.stdout 3.stderr
所以最多打开1024个文件?实际上这个值可以通过系统命令**$ ulimit -a | grep “file descriptors”**查看。
其中定义文件最大打开个数为1024.同时也可以人为修改这个值

这个errno可以在/usr/include/asm-generic/路径下的errno-base.h和errno.h中看到

1.5 有两个函数查看errno代表的错误信息:

perror(): print a system error message
strerror(): char * strerror(int errnum);

1.6 fopen创建新文件时的权限:

0666 & ~umask (0666 & umask按位取反)

如umask=0022(八进制数 000 010 010);~umask = 0755(111 101 101) ;0666&0755=0644 =-rw-r–r--

2. fclose();

int fclose(FILE *stream);

RETURN VALUE:
成功返回0,失败返回EOF(宏)

3. fgetc();

int getc(FILE *stream);

int fgetc(FILE *stream);

int getchar(void);

man fget
函数getchar等同于getc(stdio).前两个函数的区别是,getc可被实现为宏.这意味着以下几点:
getc返回的是一个无符号型的字符(unsigned char),但为了防止出错,使用了int类型来代替

fgetc和getc这两个函数参数和返回值都相同,从原始定义的角度来说,getc被定义成宏来使用,fgetc被定义成函数来使用,宏不占用调用时间而占用编译时间,而函数恰恰相反.从应用开发的角度来说,我们应该多使用函数以保证稳定和安全

fgetc函数统计文件字符个数

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

int main(int argc, const char *argv[])
{
     if (argc < 2)
     {
          fprintf(stderr, "Usage...\n");
          exit(1);
     }
     FILE *fp = fopen(argv[1], "r");
     if (fp == NULL)
     {
          perror("fopen()");
          exit(1);
     }
     int count = 0;
     while (fgetc(fp) != EOF)
          count++;

     printf("count = %d\n", count);
     fclose(fp);

     return 0;
}

3. fputc();

int putc(int c, FILE *stream); //输出到任意流

int fputc(int c, FILE *stream); //输出到任意流

int putchar(int c); //输出到stdout

fgetc和fputc函数实现文件拷贝(模拟cp命令)

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

int main(int argc, char **argv)
{
	FILE *src = fopen(argv[1], "r"); //选择r的另外一层意思是文件必须存在
	int ch;				 //用于接收函数返回值,定义成int类型而不是char类型
	if (argc < 3)
	{
		fprintf(stderr, "Usage:%s <src_file> dest_file\n", argv[0]);
		exit(1);
	}
	if (src == NULL)
	{
		perror("fopen()");
		exit(1);
	}
	FILE *dest = fopen(argv[2], "w");
	if (dest == NULL)
	{
		fclose(src); //目标文件打开失败后会exit退出而此时源文件打开成功,则要关闭源文件防止内存泄漏(后续可以用更高级的钩子函数)
		perror("fopen()");
		exit(2);
	}

	while (1)
	{
		ch = fgetc(src);
		if (ch == EOF)
			break;
		fputc(ch, dest);
	}

	fclose(dest); //首先要关闭依赖别人的对象
	fclose(src);  //然后关闭被依赖的对象

	return 0;
}

比较两个文件的不同:diff命令

4. fgets();

char *gets(char *s); //Never use this function,use fgets instead

从终端中输入的内容没有放到指定的地址中去,而是放到了输入缓冲区中,直到回车之后才放到指定的地址中

char * fgets(char *s,int size,FILE * stream);

fgets函数每次从流中读入 size-1 个字符然后存放到缓冲区中,遇到 EOF 或 ‘\n’ 时停止,如果size-1个字符被存放到缓冲区中,fgets函数会在最后一个字符后面跟加上\0.

4. fputs();

int puts(const char *s);

int fputs(const char *s,FILE *stream);

使用fgets和fputs实现文件复制

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

#define SIZE 1024

int main(int argc, char **argv)
{
	char buf[BUFSIZ];   //定义一个缓冲区

	FILE *src = fopen(argv[1], "r");

	if (argc < 3)
	{
		fprintf(stderr, "Usage:%s <src_file> dest_file\n", argv[0]);
		exit(1);
	}
	if (src == NULL)
	{
		perror("fopen()");
		exit(1);
	}
	FILE *dest = fopen(argv[2], "w");
	if (dest == NULL)
	{
		fclose(src);
		perror("fopen()");
		exit(2);
	}

	while (fgets(buf, BUFSIZ, src))
		fputs(buf, dest);

	fclose(dest);
	fclose(src);

	return 0;
}

5. fread()和fwrite()

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
从stream读到ptr中,读nmemb个对象,每个对象为size大小

size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
从ptr写到stream中,写nmemb个对象,每个对象为size大小

只适合操作工工整整,大小确定的数据块。
例如定长结构体
致命缺陷:没有验证边界,中间有一个数据大小不为定长,就全部错了

例:
fread(buf,size,nmemb,fp);

  1. 数据量足够
  2. 只有5个字节

fread(buf,1,10,fp);
1-> 10 ->10字节
2-> 5 ->5字节

fread(buf,10,1,fp);
1-> 1 ->10字节
2-> 0 ->??? 到底文件中剩余多少字节?

建议将fread当作fgetc来使用,每个对象的大小设为1字节

使用fread和fwrite函数实现文件复制

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

int main(int argc, char **argv)
{
	FILE *src = fopen(argv[1], "r");
	char buf[BUFSIZ];

	if (argc < 3)
	{
		fprintf(stderr, "Usage:%s <src_file> dest_file\n", argv[0]);
		exit(1);
	}
	if (src == NULL)
	{
		perror("fopen()");
		exit(1);
	}
	FILE *dest = fopen(argv[2], "w");
	if (dest == NULL)
	{
		fclose(src); //目标文件打开失败后会exit退出而此时源文件打开成功,则要关闭源文件防止内存泄漏(后续可以用更高级的钩子函数)
		perror("fopen()");
		exit(2);
	}
	int n;
	while ((n = fread(buf, 1, BUFSIZ, src))) //读到n个字节,就写n个字节
		fwrite(buf, 1, n, dest);

	fclose(dest);
	fclose(src);

	return 0;
}

6. printf()族; //在Linux C的视频当中有专门的讲到

int printf(const char *format, …);

int fprintf(FILE *stream, const char *format, …);

int sprintf(char *str, const char *format, …);j

int snprintf(char *str, size_t size, const char *format, …);

int atoi(const char *nptr); //把一个字符串转换成整型数

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

int main(void)
{

	char str[] = "123a456";
	printf("%d\n", atoi(str));

	char buf[1024];
	int year = 2021,mouth = 11,day = 13;
	sprintf(buf,"%d-%d-%d",year,mouth,day);  //可以把sprintf函数当作atoi函数的反向功能,因为没有函数叫做itoa
	puts(buf);

	return 0;
}

sprintf函数不检查缓冲区大小,snprintf用于防止越界

8. scanf()族;

scanf(const char *format,…);

fscanf(FILE * stream,const char * format,…);

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

scanf函数仍然会遇到缓冲区溢出的问题
scanf函数使用%s输入是危险的!!

#include <stdio.h>

#define SIZE 3

int main(void)
{
     char str[SIZE];
     scanf("%s", str);
     puts(str);

     return 0;
}
fjeijfiejf
fjeijfiejf
*** stack smashing detected ***: terminated    #程序检测到栈溢出错误
[1]    8328 abort      ./scanf

9. fseek和ftell函数 文件位置指针定位

int fseek(FILE *fp,long offset,int whence); //32位机下,范围为-231 ~ 2^31-1

int ftell(FILE *fp); //32位机下,范围为0 ~ 2^31-1

32位机下,这两个函数一起使用时,文件大小不能超过2G(有符号数 231-1),这个是古人留下的自作聪明(古人不认为以后的文件会大于2G,当时一张软盘16MB)。

替代函数

int fseeko(FILE *stream,off_t offset,int whence);

off_t ftello(FILE *stream);

(off_t是Linux命名规范的一个代表,其中t代表type)

On some architectures, both off_t and long are 32-bit types, but defining _FILE_OFFSET_BITS with the value 64 (before including any header files) will turn off_t into a 64-bit type.
在一些体系结构中off_t和long类型都是32位,这表示off_t的大小是未定义行为
解决方法:
定义宏将指定off_t的大小(参考man手册)
gcc xxx.c -o xxx -D FILE_OFFSET_BITS=64
或在makefile中写CFLAGS+=FILE_OFFSET_BITS=64

可以把fseek和ftell抛弃而去用fseeko和ftello吗?(参考man手册)
fseek和fteel遵行 POSIX.1-2001, POSIX.1-2008, C89, C99.,
fseeko和fteelo遵行 POSIX.1-2001, POSIX.1-2008, SUSv2 ,可移植性不如fseek和ftell
如果又要求移植,文件又大,那就要想别的办法了

使用fseek和ftell函数计算文件长度

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

int main(int argc, char **argv)
{
	if (argc < 2)
	{
		fprintf(stderr,"Usage:%s <filename>\n",argv[0]);
		exit(1);
	}
	FILE *fp = fopen(argv[1], "r");
	if (fp == NULL)
	{
		perror("fopen");
		exit(1);
	}
	fseek(fp, 0, SEEK_END);  //文件位置指针定位到文件尾
	printf("the size of %s file is %ld\n", argv[1], ftell(fp));  //返回文件位置指针的位置

	fclose(fp);

	return 0;
}

没有简单的程序,只有头脑简单的程序员,一个计算文件长度的程序可以有多种方式实现,你想到一个实现方法时就要动手写下来,在这些选择中,你还要能找到最简单的实现方法

void rewind(FILE * stream);

  • 等价于 (void) fseek(stream,0L,SEEK_SET);
  • L是将0转换成long类型
  • SEEK_SET:文件首
  • 功能:将文件指针指向首部

空洞文件
用迅雷建立下载文件时,文件还没有下载完成时大小就是文件下载后的大小,这就是空洞文件.这个过程就是调用fseek将文件内容全部变为空字符’\0’,然后切成片使用多线程/多进程的方式把文件分为很多块后下载

fflush() 强制刷新缓冲区

int fflush(FILE *stream);

这个程序打印什么?

#include <stdio.h>
int main(void)
{
     int i;
     printf("Before while()");
     while (1)
          ;
     printf("After while()");
     return 0;
}

这个程序什么也不打印,标准输出是典型的行缓冲模式,行满了才刷新缓冲区,在printf中加入\n就可以打印出来

fflush(NULL);
If the stream argument is NULL, fflush() flushes all open output streams.

缓冲区的作用:大多数情况下是好事,合并系统调用

行缓冲:
换行时候刷新,满了的时候刷新,强制刷新(标准输出是这样的,因为是终端设备)

全缓冲:
满了的时候刷新,强制刷新(默认,只要不是终端设备)

无缓冲:
如stderr,需要立即输出的内容

int setvbuf(FILE * stream,char * buf,int mode,size_t size);

用来修改文件的缓冲模式(一般情况下用不到修改文件的缓冲模式)

mode取值:
_IONBF 无缓冲模式
_IOLBF 行缓冲模式
_IOFBF 全缓冲模式

光标指向函数的位置按 shift+k ,vim能直接跳到这个函数的man手册

getline 获得完整的一行

man手册是包含大量有用内容

ssize_t getline(char **lineptr,size_t *n,FILE * stream);

如果成功,getline函数返回成功读到的字符数(包含分隔符:空格,回车)
getline本质是先malloc一块内存,然后如果不够就继续realloc

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

int main(int argc, const char *argv[])
{
     if (argc < 2)
     {
          fprintf(stderr, "Usage...\n");
          exit(1);
     }
     FILE *fp = fopen(argv[1], "r");
     if (fp == NULL)
     {
          perror("fopen()");
          exit(1);
     }

     //置NULL和置0非常重要!!
     char * linebuf = NULL;  
     size_t linesize = 0;

     while(1)
     {
          if (getline(&linebuf, &linesize, fp) < 0)  //使用 <0 比使用 ==-1 更好
               break;
          printf("%ld\n", strlen(linebuf));
          printf("%ld\n",linesize);
     }

     fclose(fp);

     return 0;
}
//置NULL和置0非常重要!!
char * linebuf = NULL;  
size_t linesize = 0; //如果不置空,linesize为随机值,程序就不知道自己是第一次使用getline还是第N次使用。
//实际上linesize的增长顺序是:0 120 240 ......

程序内含有可控的内存泄漏:
内存泄漏:getline函数退出时没有释放内存的操作
可控:多次调用这个函数时泄漏的内存都是从linebuf位置开始的一段内存空间

解决方式:free(linebuf)?
内存申请的函数不是只有malloc和free,如果函数内是使用new申请的呢?

man手册中关于getline的例子

#define _GNU_SOURCE //这行一般要写在makefile中,写在这里不美观也看不懂
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	FILE *stream;
	char *line = NULL; //置空,表示是第一次使用getline函数
	size_t len = 0;	   //置零,表示是第一次使用getline函数
	ssize_t nread;

	if (argc != 2)
	{
		fprintf(stderr, "Usage: %s <file>\n", argv[0]);
		exit(EXIT_FAILURE);
	}

	stream = fopen(argv[1], "r");
	if (stream == NULL)
	{
		perror("fopen");
		exit(EXIT_FAILURE);
	}

	while ((nread = getline(&line, &len, stream)) != -1) //返回成功读到的字符数,读到文件尾或失败返回-1
	{
		printf("Retrieved line of length %zu:\n", nread);
		fwrite(line, nread, 1, stdout); //将读到的一行打印到标准输出
	}
	free(line); //官方代码这里用的是free函数
	fclose(stream);
	exit(EXIT_SUCCESS); //用的不是return 0
}

man手册摘录
getline(), getdelim():
Since glibc 2.10:_POSIX_C_SOURCE >= 200809L
Before glibc 2.10:_GNU_SOURCE

glibc:GNU C Library
现在最新的glic是glic 2.34 2021-08-01
getline函数是典型的方言中的方言,可移植性差,但功能非常需要

临时文件

处理用户临时发来的数据
需要考虑:
1.如何不和其他文件冲突 2.及时销毁

char *tmpnam(char *s)
在并发的情况下不能保证安全(参考man手册)

FILE *tmpfile(void)
和tmpnam函数功能相同.程序员并不关心文件名,文件打开时只要有FILE *就够了.所以这产生的是匿名文件,ls命令看不到而且不是隐藏文件,这样就不会冲突.也不用考虑及时销毁的问题(即使忘了fclose也不会内存泄漏,前提条件:这个程序不是一直运行下去)

有些函数的副作用也能创建临时文件----后续讲解.

参考学习视频:Linux系统编程(李慧琴)

系统调用IO

写程序首当其冲的是可读性,开始不要注重效率问题,待代码量足够了再考虑效率

文件描述符(fd:file descriptor)是在文件IO中贯穿始终的类型

1.文件描述符的概念:

实质:打开文件会得到一个包含文件信息的结构体,但该数据结构被隐藏,地址保存在数组中,用户拿到的文件描述符是保存位置的数组下标(整型数),文件描述符优先使用当前可用范围内最小的数组下标

这个数组存在每个进程空间中

一个文件打开两次:同一个文件有两个文件描述符,关联的是同一个文件

数组中两个元素都指向同一个结构体,那么关闭这个文件会让另一个指针变成野指针吗?
Linux系统不会有这样的漏洞,因为该结构体中有文件打开次数计数

在符合POSIX.1的应用程序中,幻数0,1,2虽然已被标准化,但应当把它们替换成符号常量STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO(<unistd.h>)以提高可读性.
幻数:在C语言中,把直接使用的常数叫做幻数,在编程中应尽量避免使用幻数而是使用宏.

2.文件IO操作:open,close,read,write,lseek

int open(const char *pathname,int flags);
int open(const char *pathname,int flags,mode_t mode); //flags中有create,用三参的实现

这不是函数重载(C语言中没有),这是类似于printf的变参函数

在不知道什么语言的情况下(C/C++),如何判断一个函数是重载函数还是变参函数?
给这个函数传多个参数,如果报语法错误,那就是定参,是重载实现的。如果不报错,那就是变参实现的

写程序中的警告,除非是能解释的警告,不然程序中警告都要视为错误解决.什么是能解释的警告?如gets这样的函数,你知道为什么警告。

flags: 是一个位图

1.file creation flags(文件的创建选项)

2.file status flags(文件的状态选项)

The argument flags must include one of the following access modes: O_RDONLY, O_WRONLY, or O_RDWR. These request opening the file read-only, write-only, or read/write, respectively.

O_CREATE 有则清空,无则创建

O_TRUNC 如果此文件存在,而且为只写或读写成功打开,则将其长度截断为0

O_EXCL 如果同时指定了O_CREATE,而文件已经存在,则报错

在tmpnam函数的man手册中写道:
Although these functions generate(产生) names that are difficult to guess, it is nevertheless possible(然而这是可能的) that between the time that the pathname is returned and the time that the program opens it, another program might create that pathname using open(2), or create it as a symbolic link. This can lead to security holes. To avoid such possibilities, use the open(2) O_EXCL flag to open the pathname. Or better yet, use mkstemp(3) or tmpfile(3).

O_APPEND 追加内容

O_ASYNC 信号驱动IO,后面专题讲到

O_DIRECT 最小化cache。cache和buffer不同,buffer是写的缓冲区,先写到写缓冲区,cache是读的缓冲区,读内容是先读到读缓冲区。(cache是读的加速机制,buffer是写的加速机制)

O_DIRECTORY 如果 pathname 参数不是一个目录,打开会失败

O_LARGFILE 打开大文件

O_NOATIME 最后时间专题会提到ATIME,CTIME,MTIME,分别是文件最后读的时间,最后写的时间,亚数据修改的时间。O_NOATIME是不需要修改文件的最后读时间

O_NOFOLLOW 如果 pathname 是符号链接文件,则打开失败,这是FREEBSD上的策略

O_NONBLOCK 非阻塞(后面才学到)

阻塞:
例:读打印机,忙…等…忙…等…忙…等
非阻塞:
例:读打印机,忙…走,不等了
高级IO部分课程才用到非阻塞的IO,之前用到的都是阻塞的IO

O_SYNC 同步

标准IO到文件IO的转换:

r -> O_RDONLY

r+ -> O_RDWR

w -> O_WRONLY|O_CREAT|O_TRUNC 只写,有则清空无则创建

w+ -> O_RDWR|O_CREAT|O_TRUNC

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

成功:返回成功读到的字节数,读到文件尾返回0
失败:设置errno,返回-1
一个大小为30的文件,如果要求读100字节,则read函数第一次返回30,第二次返回0.
返回值为ssize_t类型(带符号整型),以保证能够返回正整数字节数,0(表示文件尾端),-1(出错)

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

成功:返回成功写入的字节数,可以写入0个(可能是阻塞的情况)
失败:设置errno,返回-1
其返回值通常与参数count相同,否则表示出错

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

whence取值:(和标准IO中的fseek一样) SEEKSET SEEK_CUR SEEK_END
返回值:从文件开始处到文件位置指针的距离
lseek就相当于fseek和ftell的综合

用系统调用IO实现cp命令

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

#define BUFSIZE 1024

int main(int argc, const char *argv[])
{
	int src = 0, dest = 0;
	char buf[BUFSIZE];
	int read_len, write_len;
	int position;

	if (argc < 3)
	{
		fprintf(stderr, "Usage:%s <src_file> <dest_file>\n", argv[0]);
		exit(EXIT_FAILURE);
	}

	src = open(argv[1], O_RDONLY);
	if (src < 0)
	{
		perror("open()");
		exit(EXIT_FAILURE);
	}
	printf("读文件描述符 = %d\n", src); //常规情况下是3

	dest = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0600); //使用三参数的open函数
	if (dest < 0)
	{
		close(src);
		perror("open()");
		exit(EXIT_FAILURE);
	}
	printf("写文件描述符 = %d\n", dest);

	while (1)
	{
		read_len = read(src, buf, BUFSIZE);
		if (read_len < 0)
		{
			perror("read");
			break; //用break而不是exit是防止内存泄漏(使用exit就不会执行close语句)
		}
		else if (read_len == 0)
			break; //读到文件尾了

		position = 0;
		//如果读到10个字节而只写入3个字节呢?(可能出现在没写够10个字节的情况下就被信号打断了)
		while (read_len)
		{
			write_len = write(dest, buf + position, read_len);
			if (write_len < 0)
			{
				perror("write:");
				exit(EXIT_FAILURE); //这里用 break 只能跳出内层循环而不能跳出外层循环,目前(初学)我们允许它产生小的内存泄漏现象
			}
			position += write_len; //位置移动到实际写入字节数的下一位置
			read_len -= write_len; //计算没写完的字节数,继续写入
		}
	}

	close(dest);
	close(src);

	exit(EXIT_SUCCESS);
}

3.文件IO与标准IO的区别

举例:传达室老大爷跑邮局(20封信跑一趟和一封信跑一趟)
标准IO相当于20封信跑一趟 合并系统调用
文件IO相当于1封信跑一趟 用户态到内核态切换
区别:响应速度&吞吐量

面试:如何使一个程序变快?
两个方面:吞吐量&响应速度
响应速度快:文件IO
用户体验的角度:变快是吞吐量大,所以为什么前面推荐你使用标准IO
提醒:标准IO与文件IO不可混用,由于两者缓冲机制不同,导致两结构体内部的文件位置指针指向也不相同

例子解释:
这个程序输出什么?

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

int main(int argc, const char *argv[])
{
     putchar('a');                 //标准IO
     write(STDOUT_FILENO, "b", 1); //系统调用

     putchar('a');
     write(STDOUT_FILENO, "b", 1);

     putchar('a');
     write(STDOUT_FILENO, "b", 1);
     printf("\n");

     return 0;
}

命令strace可以用来跟踪一个程序的系统调用过程

strace ./ab

关键输出结果:
write(1, “b”, 1b) = 1
write(1, “b”, 1b) = 1
write(1, “b”, 1b) = 1
write(1, “aaa\n”, 4aaa) = 4

FILE结构体和文件描述符的转换:

int fileno(FILE *stream);
FILE *fdopen(int fd,const char *mode);

4. IO的效率问题

习题
修改上上个用系统调用IO实现cp命令的程序的BUFSIZE从128字节到16MB(每次翻倍),复制一个5G大小的文件做测试,使用time命令计算不同缓冲区大小下程序所消耗的时间,并设置一个计数器计算程序内循环执行的次数,得到一张表。求出性能最佳拐点时的BUFSIZE值,以及何时程序会出问题。

➜ time ./mycpy /etc/services out
real    0m0.001s
user    0m0.001s
sys     0m0.000s

user:用户态时间
sys:内核态时间
real:user+system 总运行时间

5.文件共享

文件共享:多个任务共同操作一个文件或者协同完成任务

面试:写程序删除一个文件的第十行。

方式1:
一个文件打开两次:多线程
1->open r ->fd1 -> lseek 11
2->open r+ ->fd2 ->lseek 10
while()
{
//读一块,写一块
1->fd1->read
2->fd2->write
}

方式2:
用两个进程来处理方式1的过程,要进程间通信
process1->open->r
process2->open->r+

补充函数:

int truncate(const char *path,off_t length); //把一个未打开的文件截断到多长
int ftruncate(int fd,off_t length); //把一个已打开的文件截断到多长

  1. 使用多线程/多进程的方式将第10行的内容覆盖
  2. 利用ftruncate函数,由第10行的开始和第11行的开始可以计算出第10行的大小,用原文件的大小减去第10行的大小就得到新文件的大小

6.原子操作

原子:不可分割的最小单位
原子操作:不可分割的操作
原子操作的作用:解决竞争和冲突

例如:

  1. tmpnam函数是给你一个文件名而并不帮你打开这个文件,tmpfile函数则是帮你创建这个文件并且帮你打开
  2. 检查文件是否存在和创建文件这两个操作是作为一个原子操作:open函数的O_CREATE和O_EXCL选项

7.程序中的重定向:dup,dup2

int dup(int fd);

dup使用当前可用范围内最小的文件描述符作为某文件的新的文件描述符,此时该文件有两个文件描述符并且指向同一个结构体

将puts函数输出的内容输出到out文件

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

#define FNAME "out"

int main(int argc, const char *argv[])
{
     // close(STDOUT_FILENO);                           //关闭标准输出   1
     int fd = open(FNAME, O_WRONLY | O_CREAT | O_TRUNC, 0600); // out作为标准输出
     if (fd < 0)
     {
          perror("open()");
          exit(EXIT_FAILURE);
     }

     close(STDOUT_FILENO); //关闭标准输出   1
     dup(fd);              //当前可用最小的文件描述符为1
     close(fd);

     //===================================================
     puts("hello!"); // puts() writes the string to stdout

     exit(EXIT_SUCCESS);
}

这个程序存在的问题:

  1. 进程默认情况下打开3个文件描述符,那么非默认情况下没有打开呢?这个程序会使fd的初值就是1,然后close(1),再dup(1),此时dup的参数就有问题
  2. 你旁边还有一个兄弟再跑,当你关闭掉1(文件描述符)后你的兄弟就开了一个文件用掉了1,然后你的puts就输出到别人的文件中去了
    原因:这两步操作不是原子操作,需要close之后马上复制上去,dup2就是这两步的原子操作

一个不懂的程序

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

#define FNAME "out"

int main(int argc, const char *argv[])
{
     close(STDOUT_FILENO);                                     //关闭标准输出   1
     int fd = open(FNAME, O_WRONLY | O_CREAT | O_TRUNC, 0600); // out作为标准输出
     if (fd < 0)
     {
          perror("open()");
          exit(EXIT_FAILURE);
     }

     puts("hello!");

     close(fd);

     exit(EXIT_SUCCESS);
}

out文件中没有产生"hello!"
使用strace命令的执行结果:可以看到puts函数调用的write函数在close之后执行

brk(NULL)                               = 0x556c72911000
brk(0x556c72932000)                     = 0x556c72932000
close(1)                                = 0
write(1, "hello!\n", 7)                 = -1 EBADF (错误的文件描述符)
exit_group(0)                           = ?
+++ exited with 0 +++

分析的:puts函数是标准IO中的,close函数是系统调用IO中的,存在标准IO和系统调用IO混用
如果把puts函数改成write(fd,“hello!”,6)就能成功输出,感觉是标准IO和系统调用IO缓冲区刷新方式不同造成的

int dup2(int fd,int fd2);

如果fd2已经打开,则先将其关闭.
如果fd2等于fd,则dup2返回fd2,而不关闭它,
dup2是原子操作而dup不是

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

#define FNAME "out"

int main(int argc, const char *argv[])
{
     int fd = open(FNAME, O_WRONLY | O_CREAT | O_TRUNC, 0600); // tmp/out作为标准输出
     if (fd < 0)
     {
          perror("open()");
          exit(1);
     }

     dup2(fd, STDOUT_FILENO);
     //解决fd=1的问题
     if (fd != 1)     
          close(fd);
     //===================================================
     puts("hello!"); // puts() writes the string to stdout

     exit(EXIT_SUCCESS);
}

仍然存在的问题:你永远要认为自己是在写一个小模块而不是main函数,所以在你做完你的这个模块之后要还原原来的状态,即将标准输出重新定位到标准终端
微观编程思想和宏观编程思想教育 22:20

8.同步:sync,fsync,fdatasync

void sync(void);

sync:commit filesystem caches to disk 同步内核层面的buffer和cache
比如关机时解除设备挂载,需要将buffer/cache中还没有同步的数据刷新一下

fsync(int fd);

刷新的是一个文件

fdatasync(int fd);

只刷数据不刷亚数据(文件 的修改时间、大小…)

9. 管家级函数 fcntl(); file control

文件描述符所变的魔术都来自于这个函数

int fcntl(int fd,int cmd,…)

cmd参数(命令)不同,给这个函数传的参数不同,返回值也不同

10. 管家级函数 ioctl(); io control

设备相关的内容
声卡的放音和录音可以看成读写操作
一切皆文件的设计原理方便了绝大多数人的使用,唯独损害了每天和设备打交道的程序员的利益()

11. /dev/fd (虚目录):

显示的是当前进程的文件描述符信息
当前进程是什么?
例如:下面操作的当前进程就是 ls -l 这个命令

lihao@Ubuntu:~/APUE/MyCode/sysio$ ls -l /dev/fd/
总用量 0
lrwx------ 1 lihao lihao 64 718 17:22 0 -> /dev/pts/0
lrwx------ 1 lihao lihao 64 718 17:22 1 -> /dev/pts/0
lrwx------ 1 lihao lihao 64 718 17:22 2 -> /dev/pts/0
lr-x------ 1 lihao lihao 64 718 17:22 3 -> /proc/902/fd

我们现在做的开发是系统级别的:介于底层和应用层(如JAVA开发)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值