makefile常用相关函数

makefile中函数的用法与变量类似,变量的用法是在变量前面加上$,函数的用法一样,也是使用$开头,$之后加一个括号,括号中的第一个参数是函数名,后面紧跟此函数需要的参数,用逗号分隔。下面介绍几个常用的函数。

wildcard函数

  • 参数: 一个正则表达式

wildcard的中文意思是通配符,它的功能类似于正则表达式,用于展开一列所有符合其参数描述的文件名,文件之间用空格分割。

实例:

1
SOURCE=$(wildcard *.cpp)

此时SOURCE的值为所有的以.cpp为后缀的文件集合,以空格隔开。

patsubst函数

其功能是一个匹配替换的函数(pattern substitute)。

  • 参数:第一个是需要匹配的样式,第二个是表示用什么替换它,第三个被处理的以空格隔开的字符串。

实例:

1
TARGET = $(patsubst *.cpp, *.o, $(SOURCE))

TARGET表示与SOURCE中同名的目标文件.

$@,$<, $^

上述几个变量的含义:

  • $@ 表示目标的文件名
  • $< 表示依赖中的第一个文件名
  • $^ 表示依赖中所有的文件名

实例:

1
all: library.cpp main.cpp

其中$@标识all, $<表示library.cpp ,$^表示library.cpp main.cpp

Comment and share

Unix标准IO文件流及缓冲类型

Unix标准IO文件流

文件IO相关函数的一节中,我们所有的I/O函数都是围绕着文件描述符来操作的,当打开一个文件的时候,即返回一个文件描述符,然后该文件描述符用于后续的文件操作。而对于标准IO库,对于文件的操作都是围绕这 文件流 file stream进行的。当我们使用标准IO库打开或创建一个文件的时候,我们已经使一个流和一个文件进行关联。

文件流

由于历史原因,C语言中原来表示流的数据结构是FILE,而不是叫做流。由于大多数的库函数使用到了FILE类型,有的时候在使用FILE指针的时候也叫其为流,这导致后来很多数据把FILE和流搞得十分混乱。实际上流就是标准IO库中程序与文件交互的一种方式。

标准IO函数fopen打开一个文件时返回一个指向FILE对象的指针,该对象通常是一个结构,它包含了标准IO库为管理该流所需要的所有信息,包括该文件的文件描述符,用于指向该流缓冲区的指针,缓冲区的长度,当前缓冲区中的字符数以及出错标志等等

标准输入,标准输出以及标准错误

标准库中对于每一个进程都预定义了三个流,分别是stdin,stdout以及stderr,他们分别对应与Linux文件IO中的STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO。它们的定义在stdio.h中

I/O文件流的缓冲类型

标准IO提供缓冲的目的是为了通过减少使用read和write调用的次数来提高IO读写的效率,它对每个IO流自动的进行缓冲处理,从而避免了用户程序在使用read和write需要考虑的这一点。

标准IO流提供了三种缓冲。分别是全缓冲(fully buffering),行缓冲(line Buffering)以及无缓冲(nonBuffering)。

全缓冲

在使用全缓冲的情况下,当数据填满整个缓冲区之后才进行实际的IO操作。对于驻留在磁盘上的文件的读写通常是使用全缓冲。通常如果不给文件流指定缓冲区的情况下,标准IO函数会首先调用malloc函数获取所需要的缓冲区。

行缓冲

在使用行缓冲的情况下,每当输入输出遇到换行或者缓冲区满了的情况下才会进行实际的IO操作,当涉及到终端输入输出的时候通常使用行缓冲。

对于行缓冲有两个限制。1.由于接收行缓冲的缓冲区的长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会进行IO操作。2.任何时候,只要通过IO库要求从一个不带缓冲的流或者一个行缓冲的流得到输入数据,那么就会冲洗所有缓冲输出流。

###不带缓冲
此时标准IO库不对字符进行缓冲存储。这就使得输入流要求IO立即进行,如标准错误流,若果出现错误,会立马输出。

flush一个流即刷新缓冲区有两个含义。

  • 在IO库方面,flush意味着将缓冲区中的内容写到磁盘上,该缓冲区可能还没有满
  • 在终端驱动方面表示丢弃已经存储在缓冲区中的内容。

##标准文件流与缓冲类型之间的关系

  • 当标准输入输出指向的是交互式设备(如终端)的时候,它们是行缓冲的,若不是则是全缓冲的。
  • 标准错误永远是无缓冲的。

与缓冲相关的函数

我们可以通过一下两个函数对将缓冲关闭或者改变缓冲的类型。其中这些函数应该在流被打开之后调用,而且也应该在对流进行一切操作之前调用。

1
2
3
#include <stdio.h>
void setbuf(FILE* restrict fd, char* restrict buf);
int setvbuf(FILE* restrict fd, char* restrict buf, int mode, size_t size);

使用setbuf函数打开或者关闭缓冲,当buf是一个有效缓冲区时,此时缓冲打开,若流指向的是终端设备,则此时该流是行缓冲的,否则该流是全缓冲的;当buf为NULL的时候,表示关闭该缓冲。

使用setvbuf可以精确的说明缓冲的类型,这里是使用mode来说明的,mode的值包括以下几个:

  • _IOFBF 全缓冲
  • _IOLBUF 行缓冲
  • _IONBUF 无缓冲

如果指定一个不带缓冲的流,则忽略buf和size参数。如果指定缓冲,则buf和size分别指定一个缓冲区域和缓冲区域的长度。若此时buf为NULL,则标准IO库将自动制定一个适合长度的缓冲区。

上述函数与缓冲之间的关系

函数 mode buf 缓冲区及长度 缓冲类型
setbuf 非空 长度为size的缓冲区buf 全缓冲或行缓冲
setbuf NULL 无缓冲区 不带缓冲
setvbuf _IOFBF 非空 长度为size的缓冲区buf 全缓冲
setvbuf _IOFBF NULL 合适长度的缓冲区buf 全缓冲
setvbuf _IOLBF 非空 长度为size的缓冲区buf 行缓冲
setvbuf _IOLBF NULL 合适长度的缓冲区buf 行缓冲
setvbuf _IONBF 忽略 无缓冲区 不带缓冲

我们还可以通过fflush强制冲洗一个流,此函数使该流所有未写的数据都被传送到内核。作为一种特殊的情况,当流的NULL时,所有的流将被冲洗:

1
2
#include <stdio.h>
int fflush(FILE* fd);

Comment and share

CprintNULL

in C++ problems

printf %s情况下字符串为NULL的输出结果

遇到问题

1
2
printf("this%s\n", s);
printf("%s\n", s);

当s为NULL的时候,执行上面三个语句两个语句执行结果分别为:

1
2
this(null)
segment fault

问题分析

在ANSI C中没有对printf时字符串为NULL的情况进行明确的定义,所以当出现这种情况时往往是未定义行为。
所以上面第一种情况会出现segment fault的情况是一种未定义行为,可能在其他编译器上不会出现段错误。

上面出现段错误的行为我们可以通过反汇编进行查看。

反汇编的结果如下:

1
2
3
4
5
804842d: c7 04 24 20 85 04 08 movl $0x8048520,(%esp)
8048434: e8 0b ff ff ff call 8048344 <printf@plt>
8048439: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048440: e8 df fe ff ff call 8048324 <puts@plt>
8048445: 83 c4 14 add $0x14,%esp

从上面可以看出第一个执行被汇编成真正的printf指令,而第二个简单的printf NULL的指令被汇编成puts。
所以才出现了上面的结果。

Comment and share

Unix环境变量

概述

在Unix中,每个进程都有自己的一组环境变量,这些环境变量,要么是一组全局字符串,要么是子进程从父进程继承而来的,如果子进程不对其修改则与父进程的环境变量一模一样。
Unix内核并不查看这些字符串,它们的解释权完全取决于各个应用程序。例如shell是Unix中一个可执行程序,通常shell的启动文件中会对环境变量进行设置。所以当我们进入shell之后可以查看path等环境变量。在当前shell中启动的进程会继承其父进程shell的环境变量,也就可以查看path等环境变量,环境变量可以在登录的时候自动设置,也可以由用户自行设置。

环境变量相关变量

每个程序都会接收到一张环境表。与参数表一样,环境表也是一个字符指针数组。其中每个指针都包含一个以NULL结尾的字符串的地址。全局变量
environ指向了这个数组的地址。

代码如下:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
extern char **environ;
if (environ!= NULL) {
for (i = 0; environ[i] != NULL; i++) {
printf("env: %s\n", environ[i]);
}
}

环境变量相关的函数

与环境变量相关的函数包括以下几种:取环境变量的值,添加环境变量、修改环境变量、以及删除环境变量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdlib.h>
//获取环境变量的值
char* getenv(const char* name); //若包含此key返回对应的值,否则返回NULL
/*
* function: 修改环境变量的值 前面两个分别为键值对,最后一个表示是否需要删除原有的定义重写。
* params:键没有存在,则创建此键值对。若键已经存在:rewrite=0时不覆盖原来的值;rewrite!=0覆盖原来的值
* return:成功返回0, 出错返回非0。
*/
int setenv(const char* name, const char* value, int rewrite);
/*
* function: 添加环境变量,若存在则删除原有的,添加新的,不存在则直接添加
* params: 参数为一个键值对字符串,如"name=test"
* return: 成功返回0,不成功返回-1
*/
int putenv(char *str);
/*
* function: 删除name的定义,即使不存在也不出错。
* params: 参数为键
* return:出错返回-1,不出错返回0
*/
int unsetenv(const char*name);
/*
* function: 清除所有的环境变量
* return: 成功返回0, 失败返回-1。
*/
int clearenv();

putenv和setenv的区别

putenv可以使用程序中已经定义的且形如”name=value”的字符串作为参数。此时系统不再为该环境变量分配内存,环境变量将使用程序中定义变量的内存。
并将该字符串的地址保存在环境变量中。所以要使用putenv一定要用全局变量作为参数,否则程序退出栈内存被释放,再次访问环境变量将会出现未定义行为,
导致环境变量不可用。

putenv也可用字符串常量做参数,这个时候系统将为其分配内存。

但是setenv去设置环境变量系统将会先malloc出一块内存给环境变量使用,所以此时不需要担心环境不可用的情况。

环境变量在进程空间中的存储位置

环境变量和环境字符串通常放在进程存储空间的顶部,也就是栈内存之上

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
extern char **environ;
int main() {
int i;
printf("the address of the environment: %p\n", environ);
printf("the adress of first i: %p\n", &i);
}

环境变量中进行增删改操作的实现机制

删除环境变量比较容易,当增加或者修改环境变量的时候由于环境表和环境字符串通常占用的是进程地址空间的顶部,所以它不能再向
高地址(向上)扩展,同时也不能在移动在它之下的各栈帧,所以也不能向下扩展。两者的组合使得该空间的长度不能再增加。

  • 删除环境变量:删除环境变量时只需要先找到该指针,然后将所有后续指针都向环境表的首部顺序移一个位置。
  • 修改环境变量:
    • 若新的环境变量value长度小于或者等于原有的值,则直接将其复制到旧值。
    • 否则,先调用malloc在堆上分配一块内存,然后将新字符串指向该空间,接着使环境变量表中针对name的指针指向新分区。
  • 增加环境变量: 增加新环境变量比较复杂。必须首先通过调用malloc为新的name=value分配内存空间,然后将字符串复制到此空间中。
    • 如果该name是第一次增加,则必须调用malloc为新的指针表分配内存空间,然后将原来的环境表复制到新的内存,并将指向新的name=value字符串
      的指针存放在该指针表的表尾,然后将空指针放在其后面。最后使environ指向新的环境表。这样就导致原来位于栈顶之上的环境表移到了堆内存中。
      但是大多数的环境指针仍然指向栈顶之上的name=value字符串。
    • 如果不是第一次新增加一个name,可知之前已经将环境表迁移到堆内存中,所以只需要调用realloc,以分配比原空间多存放一个指针的空间。然后将指针指向name=value
      字符串的指针,最后是一个NULL指针。

Comment and share

Unix文件IO相关函数

Unix中大多数文件的操作只需要用到五个函数open、read、write、lseek、和close。本章将详细讲解这些函数的用法以及参数详解。

文件描述符

对于内核而言,所有打开的文件都是通过文件描述符进行引用。文件描述符是一个非负整数。当打开或者创建一个文件的时候,内核向进程返回一个文件描述符。当对这个文件进行读写的时候,将这个参数传递给read或write。LINUX用于IO的数据结构一章中讲了STDIN_FILENO、STDOUT_FILENO以及STDERR_FILENO所对应的文件描述符。

为了保证系统资源的合理使用和安全性,Unix系统对于系统和用户能打开的文件描述符的格式都做了一定的限制。通过一下命令我们可以进行查看:

1
2
sysctl -a | grep fs.file-max //查看系统级限制的文件描述符的个数
ulimit -n //查看用户(进程)级别限制的文件描述符的个数,

文件描述符与文件指针的关系

文件描述符:内核会为每一个运行中的进程在进程控制块(pcb)中维护一个打开文件的记录表,每个表项都有一个指针指向打开的文件,文件描述符就是记录表的索引。

文件指针:C语言使用文件指针而不是文件描述符作为文件IO的句柄,文件指针是指向进程的用户空间中的一个FILE结构的数据结构,FILE结构中包括一个IO缓冲区和一个文件描述符,而文件描述符是
文件描述符表的一个索引,从某种意义上可以将文件指针理解为文件句柄的句柄。

1
2
3
4
5
6
7
8
9
10
typedef struct {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
} FILE;
  • 文件指针相比于文件描述符是高级的接口.
  • 文件指针使用fread()和fwrite()函数进行操作,文件描述符使用write()和read()函数进行操作
  • 文件指针具有缓冲区,是较高级别的IO,读写时具有缓冲,具有错误指示和EOF检测;文件描述符没有
  • 文件指针具有移植性,文件描述符不能移植到除Unix之外的系统。
  • fopen在stdio.h中,open在fcntl.h中
  • fopen是标准C中定义的,而open是posix中定义的。
  • fwrite/fread处理的速度快于read/write,但是在内存方面read/write性能较好。

函数open()和openat()

函数原型

1
2
int open(const char* path, int oflag, .../*mode_t mode*/);
int openat(int fd, const char* path, int oflag, .../*mode_t mode*/)

返回值说明

若文件打开失败返回-1,打开失败原因可以通过errno或者strerror(errno)查看;

若成功将返回最小的未用的文件描述符的值。

参数说明

  • path为要打开的文件的文件路径
  • oflag为文件打开模式.
  • …为可变参数,可以视情况添加

文件打开模式

文件打开模式标识当前进程对打开文件的操作权限。通常用一个或者多个权限的或来表示。权限列表如下:

flag 含义
O_RDONLY 只读权限
O_WRONLY 只写权限
O_RDWR 读写权限
O_EXEC 可执行权限
O_SEARCH 搜索权限(针对目录)
O_APPEND 每次写都追加到文件的末端
O_CLOEXEC 把close_on_exec设置为文件描述符标识
O_CREATE 若文件不存在,则创建它。使用此选项的时候,需要使用第三个参数指定该新文件的访问权限位
O_DIRECTORY 如果path不是目录则出错
O_EXCL 若同时执行了O_CREATE,若文件存在则出错,可以用此选项测试文件是否存在
O_NOCTTY 如果PATH引用的是终端设备,则不将该终端设备作为该进程的控制终端
O_NOFOLLOW 若PATH引用的是符号链接,则出错
O_NONBLOCK 如果path引用的是FIFO,一个块特殊文件或者一个字符特殊文件,则将文本打开操作和后续的IO设置为非阻塞模式
O_SYNC 使每次write等待物理IO完成,包括该write属性引起的文件属性更新需要的IO
O_TRUNC 如果文件存在,且打开模式为只写或者读写,则将文件内容截断为0
O_TTY_INIT 如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合Single Unix Specification
O_DSYNC 每次write要等待物理IO操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需要等待文件属性被更新
O_RSYNC 使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作完成

文件访问权限mode_t

mode的值表示了对文件的访问权限。这个访问权限与使用shell命令chmod去修改文件的权限的含义相同,文件的权限包括三大类,分别是当前文件对于文件所有者(u),文件所有者所在的组(g)以及其他
用户(o)而言,对这三种角色又分别具有读写可执行的权限。所以这个参数只有在创建文件的时候才用到,用来指定当前创建的文件所对应的权限。

mode 含义
S_IRUSR 用户读
S_IWUSR 用户写
S_IXUSR 用户可执行
S_IRGRP 组读
S_IWGRP 组写
S_IXGRP 组可执行
S_IROTH 其他读
S_IWOTH 其他写
S_IXOTH 其他可执行

需要注意的是,目录的可执行权限以及读权限与文件的相应权限完全不同。目录的可执行权限表示搜索位,即可搜索权限,若目录不具有可执行权限则不能cd进入文件;
目录的读权限是可以查看目录中文件内容的权限,若文件夹不具有读权限,则ls不能显示目录内的内容。

open与openat的区别

open和openat的区别主要在fd上

  • path参数指定的是绝对路径名,在这种情况下,open与openat相同,fd忽略.
  • path参数是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数通过打开相对路径名所在的文件目录获取。即此时fd为打开相对路径所获取的文件描述符。
  • path参数为相对路径,fd参数为AT_FDCWD,此时相对路径为当前目录,作用于open相同。

函数create()

函数原型

1
int create(const char *path, mode_t mode);

返回值说明

若文件创建失败返回-1;
若创建成功返回当前创建文件的文件描述符。

参数说明

参数与open中对应的参数含义相同

函数功能说明

create(path, mode)函数功能为创建新文件,与open(path, O_CREATE|O_TRUNC|O_WRONLY)功能相同。

函数close()

函数原型

1
int close(int fd);

返回值说明

文件关闭成功返回0,关闭失败返回-1.

函数功能介绍

该函数的作用是关闭指定文件描述符的文件,关闭文件时还会释放该进程加在该文件上的所有的记录锁。当一个进程终止时,内核自动关闭它所有打开的文件。很多程序都是利用这一功能而不是close
函数关闭打开的文件。但是对于长期运行的函数,最好还是使用close关闭打开的文件。

lseek()函数

每个打开的文件在文件表项中存在着对应的当前文件的偏移量(current file offset),通常为非负整数。文件的读写操作都是从当前偏移量开始,并在操作后将偏移量增加相应的字节数。当打开文件
模式为O_APPEND时,该文件的offset是文件末尾,除此之外,其他情况都被初始化为0.

函数原型

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

返回值说明

成功则返回新的文件的偏移量;
失败则返回-1.

函数功能

使用lseek()函数显式的为一个打开的文件设置偏移量。lseek仅将文件的偏移量记录在内核中,并不引起IO开销。

参数说明

参数offset的解释与whence相关。

  • 若whence为SEEK_SET,则将该文件的偏移量设置为距离当前文件开始处offset字节。
  • 若whence为SEEK_CUR,则将该文件的偏移量设置为距离当前偏移量加offset个字节,此时offset可正可负。
  • 若whence为SEEK_END,则将该文件的偏移量设置为当前文件长度加offser个字节,此时offset可正可负。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <fcntl.h>
#include <stdio.h>
#include <sys/types.h>
int main() {
int fd = open("./data", O_WRONLY | O_CREAT | O_TRUNC);
lseek(fd, 20, SEEK_END);
write(fd, "haha", 4);
//可以用这种方法查看打开文件的当前偏移量。
//这种方法可以用来确定当前文件是否可以设置偏移量
//若文件是管道、FIFO或者网络套接字,则lseek返回-1,并将errno设置为ESPIPE
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
}

注意一般情况下文件的偏移量不能为负值,但是一些特殊的文件允许偏移量为负值,如在FreeBSD上运行的设备/dev/kmem支持负的偏移量。
因为偏移量可能为负值,所以在比较偏移量的时候不能直接判断其是否小于0,而是要判断其是否等于-1.

文件的偏移量允许大于文件的长度,这时会在文件中出现一些空洞,但是是被允许的。文件中的空洞并不在文件中占磁盘块(block)。

read()函数

函数原型

1
2
3
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);

返回值说明

若读取成功,读到文件末尾返回0,未读到文件末尾返回当前读的字节数。
若读取失败,返回-1。

参数说明

fd为要读取文件的文件描述符。buf为读取文件数据缓冲区,nbytes为期待读取的字节数,通常为sizeof(buf)。

注意read函数默认读入多行,遇到换行不会停止读入,直到读到文件末尾,下一次读取返回值为0.

write()函数

函数原型

1
2
3
#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t ntyes);

返回值说明

若写入成功则返回写入的字节数;失败返回-1.

参数说明

buf为写入内容的缓冲区,ntyes为期待写入的字节数,通常为sizeof(buf)。一般情况下返回值与ntypes相等,否则写入失败。

当指定O_APPEND选项,内容将从文件末尾写入,否则从文件开始写入。一般情况下将缓冲区的长度设置为磁盘块的大小可以最大程度的提升程序读写的性能。

Comment and share

Unix错误处理

当Unix系统函数出错的时候,通常会返回一个负值,同时整型变量errno通常被设置为具有特定信息的值。例如当使用open打开文件的时候,若当前文件不存在,此时open的返回值为-1,errno被设置为2(ENOENT)。
系统函数出错的返回值不一定为负数,是根据具体函数具体定义的,如当系统函数返回一个指针时,若出错,将会返回NULL。

Linux系统中errno.h中定义了一系列的错误宏,他们之处了不同错误对应的错误ID,为整型变量,可被赋值。

关于errno需要注意的两点:

  • 如果没出错,errno将不会被进程设置,所以一般当利用函数返回值确认已经出错的时候,再去查看相应的errno
  • 任何函数不会讲errno的值设置为0,而且在errno.h中定义的所有宏定义都不为0

Unix错误处理的两个相关函数

strerror函数

函数原型

1
2
3
#include <string.h>
char* strerror(int errno);

函数功能

此函数的功能为将errno转化为其对应的具体错误信息。

示例代码

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
int fd = open("data");
if (fd == -1) {
printf("%s", strerror(errno));
}
}

perror函数

函数原型

1
2
#include <stdio.h>
void perror(const char* msg);

函数功能

perror基于当前errno的值,在标准错误流上输出一个出错信息。输出内容首先输出msg所指的字符串,然后一个冒号,空格,接着是errno对应的错误字符串,最后是一个换行符。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int fd = open("data");
if (fd == -1) {
printf("%s", strerror(errno));
perror(argv[0]);
}
}

一些特殊的错误

EAGAIN,EWOULDBLOCK和EINTR

在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。

从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。

  例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返 回,

read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。

  又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。

EAGAIN:Linux - 非阻塞socket编程处理EAGAIN错误

  在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable,errno代码为11(EAGAIN),这是什么意思?

这表明你在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误,这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以。

对非阻塞socket而言,EAGAIN不是一种错误。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。

另外,如果出现EINTR即errno为4,错误描述Interrupted system call,操作也应该继续。

最后,如果recv的返回值为0,那表明连接已经断开,我们的接收操作也应该结束。

错误恢复

我们可以将errno.h中定义的错误分为两种,分别是致命的和非致命的,对于致命性错误,无法执行恢复操作。对于非致命性错误,大多数来说是暂时的。

对于非致命性错误,最常用的做法就是延迟一段时间,然后重试。例如当错误表示网络不可用,这时我们将可以通过延迟一段时间,进行重新连接。

errno宏定义及相应解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124 EMEDIUMTYPE Wrong medium type
  123 ENOMEDIUM No medium found
  122 EDQUOT Disk quota exceeded
  121 EREMOTEIO Remote I/O error
  120 EISNAM Is a named type file
  119 ENAVAIL No XENIX semaphores available
  118 ENOTNAM Not a XENIX named type file
  117 EUCLEAN Structure needs cleaning
  116 ESTALE Stale NFS file handle
  115 EINPROGRESS +Operation now in progress
  114 EALREADY Operation already in progress
  113 EHOSTUNREACH No route to host
  112 EHOSTDOWN Host is down
  111 ECONNREFUSED Connection refused
  110 ETIMEDOUT +Connection timed out
  109 ETOOMANYREFS Too many references: cannot splice
  108 ESHUTDOWN Cannot send after transport endpoint shutdown
  107 ENOTCONN Transport endpoint is not connected
  106 EISCONN Transport endpoint is already connected
  105 ENOBUFS No buffer space available
  104 ECONNRESET Connection reset by peer
  103 ECONNABORTED Software caused connection abort
  102 ENETRESET Network dropped connection on reset
  101 ENETUNREACH Network is unreachable
  100 ENETDOWN Network is down
  99 EADDRNOTAVAIL Cannot assign requested address
  98 EADDRINUSE Address already in use
  97 EAFNOSUPPORT Address family not supported by protocol
  96 EPFNOSUPPORT Protocol family not supported
  95 EOPNOTSUPP Operation not supported
  94 ESOCKTNOSUPPORT Socket type not supported
  93 EPROTONOSUPPORT Protocol not supported
  92 ENOPROTOOPT Protocol not available
  91 EPROTOTYPE Protocol wrong type for socket
  90 EMSGSIZE +Message too long
  89 EDESTADDRREQ Destination address required
  88 ENOTSOCK Socket operation on non-socket
  87 EUSERS Too many users
  86 ESTRPIPE Streams pipe error
  85 ERESTART Interrupted system call should be restarted
  84 EILSEQ Invalid or incomplete multibyte or wide character
  83 ELIBEXEC Cannot exec a shared library directly
  82 ELIBMAX Attempting to link in too many shared libraries
  81 ELIBSCN .lib section in a.out corrupted
  80 ELIBBAD Accessing a corrupted shared library
  79 ELIBACC Can not access a needed shared library
  78 EREMCHG Remote address changed
  77 EBADFD File descriptor in bad state
  76 ENOTUNIQ Name not unique on network
  75 EOVERFLOW Value too large for defined data type
  74 EBADMSG +Bad message
  73 EDOTDOT RFS specific error
  72 EMULTIHOP Multihop attempted
  71 EPROTO Protocol error
  70 ECOMM Communication error on send
  69 ESRMNT Srmount error
  68 EADV Advertise error
  67 ENOLINK Link has been severed
  66 EREMOTE Object is remote
  65 ENOPKG Package not installed
  64 ENONET Machine is not on the network
  63 ENOSR Out of streams resources
  62 ETIME Timer expired
  61 ENODATA No data available
  60 ENOSTR Device not a stream
  59 EBFONT Bad font file format
  57 EBADSLT Invalid slot
  56 EBADRQC Invalid request code
  55 ENOANO No anode
  54 EXFULL Exchange full
  53 EBADR Invalid request descriptor
  52 EBADE Invalid exchange
  51 EL2HLT Level 2 halted
  50 ENOCSI No CSI structure available
  49 EUNATCH Protocol driver not attached
  48 ELNRNG Link number out of range
  47 EL3RST Level 3 reset
  46 EL3HLT Level 3 halted
  45 EL2NSYNC Level 2 not synchronized
  44 ECHRNG Channel number out of range
  43 EIDRM Identifier removed
  42 ENOMSG No message of desired type
  40 ELOOP Too many levels of symbolic links
  39 ENOTEMPTY +Directory not empty
  38 ENOSYS +Function not implemented
  37 ENOLCK +No locks available
  36 ENAMETOOLONG +File name too long
  35 EDEADLK +Resource deadlock avoided
  34 ERANGE +Numerical result out of range
  33 EDOM +Numerical argument out of domain
  32 EPIPE +Broken pipe
  31 EMLINK +Too many links
  30 EROFS +Read-only file system
  29 ESPIPE +Illegal seek
  28 ENOSPC +No space left on device
  27 EFBIG +File too large
  26 ETXTBSY Text file busy
  25 ENOTTY +Inappropriate ioctl for device
  24 EMFILE +Too many open files
  23 ENFILE +Too many open files in system
  22 EINVAL +Invalid argument
  21 EISDIR +Is a directory
  20 ENOTDIR +Not a directory
  19 ENODEV +No such device
  18 EXDEV +Invalid cross-device link
  17 EEXIST +File exists
  16 EBUSY +Device or resource busy
  15 ENOTBLK Block device required
  14 EFAULT +Bad address
  13 EACCES +Permission denied
  12 ENOMEM +Cannot allocate memory
  11 EAGAIN +Resource temporarily unavailable
  10 ECHILD +No child processes
  9 EBADF +Bad file descriptor
  8 ENOEXEC +Exec format error
  7 E2BIG +Argument list too long
  6 ENXIO +No such device or address
  5 EIO +Input/output error
  4 EINTR +Interrupted system call
  3 ESRCH +No such process
  2 ENOENT +No such file or directory
  1 EPERM +Operation not permitted
  0 Success

Comment and share

Unix基础知识

Unix体系结构

从严格意义上讲,可以将操作系统定义为一种软件,它相当于一种控制计算机硬件资源,为程序提供运行环境的软件。我们通常将这种软件叫做内核,因为它相对比较小,并且位于环境的核心。Unix体系结构如下图:

内核的接口被称为系统调用(system Call),公共库函数建立在系统调用接口之上,应用程序既可以使用公共函数库,也可以使用系统调用。shell是一个特殊的应用程序,为运行其他应用程序提供了接口。

Unix文件和目录

文件系统

Unix文件系统是文件和目录的一种层次结构,所有文件的起点都是根目录(root),名称为”/“.

目录是一个包含目录项的文件。逻辑上,可以认为每个目录项都包含一个文件,同时还说明该文件属性的信息。文件属性包括文件类型(普通文件还是目录)、文件权限、;链接到改文件的进程数、文件所有者、文件所有者所在的组 文件大小以及文件最后修改的时间等。stat和fstat函数返回一个文件属性的信息结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <sys/stat.h>
//通过文件名获取文件信息,并保存在buf所指的结构体stat中
//返回值:成功返回0,失败-1,错误码存在errno中
int stat(const char* file_name, struct stat* buf);
数据类型:
struct stat {
dev_t st_dev; /* ID of device containing file -文件所在设备的ID*/
ino_t st_ino; /* inode number -inode节点号*/
mode_t st_mode; /* protection -保护模式?*/
nlink_t st_nlink; /* number of hard links -链向此文件的连接数(硬连接)*/
uid_t st_uid; /* user ID of owner -user id*/
gid_t st_gid; /* group ID of owner - group id*/
dev_t st_rdev; /* device ID (if special file) -设备号,针对设备文件*/
off_t st_size; /* total size, in bytes -文件大小,字节为单位*/
blksize_t st_blksize; /* blocksize for filesystem I/O -系统块的大小*/
blkcnt_t st_blocks; /* number of blocks allocated -文件所占块数*/
time_t st_atime; /* time of last access -最近存取时间*/
time_t st_mtime; /* time of last modification -最近修改时间*/
time_t st_ctime; /* time of last status change - */
}

使用C语言实现ls功能

涉及到的相关函数:

  • 文件夹操作函数opendir,readdir,closedir
  • opendir返回指向dir结构的指针,将该指针传递给readdir,无需关心dir结构中存在什么数据,直接读取即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>
void err_quit(const char* message) {
printf("%s\n", message);
exit(0);
}
int main(int argc, char *argv[])
{
if (argc < 2)
{
err_quit("usage: ls directory_name\n");
exit(0);
}
DIR *dp;
struct dirent *dirp;
if ((dp = opendir(argv[1])) == NULL)
err_quit("can not open file\n");
while ((dirp = readdir(dp)) != NULL)
printf("%s\n", dirp->d_name);
closedir(dp);
return 0;
}

输入和输出

文件描述符

文件描述符是一个很小的非负整数,内核用文件描述符来标识一个特定进程正在访问的文件。当内核打开或创建文件时,他都会返回一个文件描述符。在读写文件时,都可以使用这个描述符(根据打开时的模式赋予权限).

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

按照惯例,每当运行一个新程序的时候,所有的shell都会为改程序默认打开三个文件描述符,即标准输入,标准输出和标注错误。如果不做特殊处理,这三个描述符都连接到终端,也可以将其重定向到文件。如ls > test.data

Comment and share

函数dup与dup2

函数原型

1
2
3
4
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);

函数功能

这两个函数的功能都是用来复制一个现有的文件描述符。返回的文件描述符与原有的文件描述符共用同一个文件表项,但是文件描述符标志将被清除,即当进程调用exec时文件描述符将不会被关闭。

返回值

dup返回当前可用的最小的文件描述符。dup2返回fd2,若fd2所表示的文件已经打开,则将该文件描述符先关闭,然后再将fd复制到fd2上返回。若fd2与fd相同,则直接返回fd、.

等效函数

dup函数与fcntl的F_DUPFD功能相同。

dup(fd)等效于fcntl(fd, F_DUPFD, 0)

dup2(fd, fd2)等效于close(fd), fcntl(fd, F_DUPFD, fd2)

但是dup2是原子的上述先关闭然后在复制不是原子操作。

作用及用途

dup函数常用来重定向进程的stdin、stdout以及stderr。原理与CGI类似,即将标准输入标准输出重定向到一个文件或者socket流等。

重定向标准输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <stdio.h>
int main() {
int fd = dup(STDIN_FILENO);
if (fd < 0) {
printf("dup error\n");
exit(1);
}
char buf[] = "this is a test of dup\n";
write(fd, buf, sizeof(buf));
return 0;
}

Comment and share

CGI原理

CGI(Common Gateway Interface)通用网关接口,CGI描述了服务器和请求处理程序之间传输数据局的一种标准。在理解的过程中我们需要区分CGI和CGI程序,CGI是一种数据传输的标准,而CGI程序是实际处理业务的一个程序。webserver每请求一次,CGI程序就会fork出一个子进程进行处理。CGI程序的参数通过环境变量和标准输入获得,它的相应通过标准输出传递给webServer。

CGI的工作原理是:

  • 客户端通过http将请求发送到web服务器
  • web服务器接收并收集用户请求,然后交给CGI程序进行处理
  • CGI程序把处理后的结果发送给服务器
  • 服务器将结果传送给浏览器

其中上述第二步web服务器通过环境变量或标准输入将请求发送给CGI程序,第三步CGI通过标准输出将结果发送给webServer

CGI接口标准:标准输入,环境变量以及标准输出

借口标准 介绍
标准输入 CGI程序像其他可执行程序一样,可通过标准输入(stdin)从Web服务器得到输入信息,如Form中的数据,这就是所谓的向CGI程序传递数据的POST方法。这意味着在操作系统命令行状态可执行CGI程序,对CGI程序进行调试。POST方法是常用的方法。
环境变量 操作系统提供了许多环境变量,它们定义了程序的执行环境,应用程序可以存取它们。Web服务器和CGI接口又另外设置了自己的一些环境变量,用来向CGI程序传递一些重要的参数。CGI的GET方法还通过环境变量QUERY-STRING向CGI程序传递Form中的数据。
标准输出 CGI程序通过标准输出(stdout)将输出信息传送给Web服务器。传送给Web服务器的信息可以用各种格式,通常是以纯文本或者HTML文本的形式,这样我们就可以在命令行状态调试CGI程序,并且得到它们的输出。

###常用的环境变量
Linux中的环境变量是一系列的键值对集合,它们的值可以通过shell设置,也可以被其他进程或程序设置和访问,它们是web服务器传递给CGI程序的一种最简单的方式,之所以叫做环境变量是因为它们是全局变量,任何程序都可以存取它们。

key 意义
SERVER_NAME CGI脚本运行时的主机名和IP地址
SERVER_SOFTWARE 你的服务器的类型如: CERN/3.0 或 NCSA/1.3.
GATEWAY_INTERFACE 运行的CGI版本. 对于UNIX服务器, 这是CGI/1.1
SERVER_PROTOCOL 服务器运行的HTTP协议. 这里当是HTTP/1.0.
SERVER_PORT 服务器运行的TCP口,通常Web服务器是80.
REQUEST_METHOD POST 或 GET, 取决于你的表单是怎样递交的
HTTP_ACCEPT 浏览器能直接接收的Content-types, 可以有HTTP Accept header定义.
HTTP_USER_AGENT 递交表单的浏览器的名称、版本 和其他平台性的附加信息。
HTTP_REFERER 递交表单的文本的 URL,不是所有的浏览器都发出这个信息,不要依赖它
PATH_INFO 附加的路径信息, 由浏览器通过GET方法发出.
PATH_TRANSLATED 在PATH_INFO中系统规定的路径信息.
SCRIPT_NAME 指向这个CGI脚本的路径, 是在URL中显示的(如, /cgi-bin/thescript).
QUERY_STRING 脚本参数或者表单输入项(如果是用GET递交). QUERY_STRING包含URL中问号后面的参数
REMOTE_HOST 递交脚本的主机名,这个值不能被设置.
REMOTE_ADDR 递交脚本的主机IP地址.
REMOTE_USER 递交脚本的用户名. 如果服务器的authentication被激活,这个值可以设置。
REMOTE_IDENT 如果Web服务器是在ident (一种确认用户连接你的协议)运行, 递交表单的系统也在运行ident, 这个变量就含有ident返回值.
CONTENT_TYPE 如果表单是用POST递交, 这个值将是 application/x-www-form-urlencoded. 在上载文件的表单中, content-type 是个 multipart/form-data.
CONTENT_LENGTH 对于用POST递交的表单,标准输入口的字节数.

CGI的工作原理图

Comment and share

Wait函数详解

kill

头文件

sys/types.h
signal.h

函数功能

注意此函数的功能是向指定进程发送信号。而不是杀死某个进程.名字为kill的原因是早期的Unix系统对信号的默认处理方式大部分是终止进程。

函数原型

int kill(pid_t pid, int sig);

返回值:执行成功返回0,执行失败返回-1。

参数说明

pid为进程ID,sig标识要发送的信号。

kill()函数的作用是用来向指定的进程或进程组发送信号。其中pid有一下
几种类型:

  • pid > 0: 发送信号给进程标识为pid的进程
  • pid = 0: 发送信号给当前进程相同进程组的所有进程
  • pid = -1:发送信号给系统内除了1号进程以外的所有进程
  • pid < -1:发送信号给进程组标识为-pid的进程。

当sig=0时没有信号发出,但是系统会执行错误检查,通常会利用sig值为0来检查某个进程是否在执行。

  • 若进程不存在,errno为ESRCH
  • 若errno为EPERM(无权向目标进程发送信号,但是存在)或调用成功,表示进程存在。

Example

相关知识:WIFSIGNALED和WIFEXITED两个宏是用来判断当前程序的子进程的退出方式,是接收到信号异常退出
还是正常调用exit()或return退出。

  • WIFEXITED: 调用exit()或return退出
  • WIFSIGNALED:接收到信号异常退出 此时可以使用WTERMSIG(status)获取其接受信号的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
main()
{
pid_t pid;
int status;
if(!(pid= fork()))
{
printf("Hi I am child process!\n");
sleep(10);
return;
}
else
{
printf("send signal to child process (%d) \n", pid);
sleep(1);
kill(pid, SIGABRT);
wait(&status);
if(WIFSIGNALED(status))
printf("chile process receive signal %d\n", WTERMSIG(status));
}
}

raise和killpg函数

除了kill函数可以向进程发送信号外,还可以通过raise和killpg函数发送信号。

函数原型

int raise(int sig);
int killpg(pid_t pgrpid, int sig);

函数功能

raise函数的功能是向进程自身发送信号.在单线程程序中,调用raise函数相当于调用kill(getpid(), sig).
对于支持线程的系统,一般将raise实现为:

1
pthread_kill(pthread_self(), sig);

此时信号只发送给当前进程的当前线程。而不影响其他线程。

killpg函数的作用是向同组的所有进程发送信号。相当于:

1
kill(-pgrpid, sig);

函数返回值

raise函数的错误返回值只有一个,即EINVAL,表示信号无效。其他时候返回都为成功。
而killpg的返回值与kill类似。

wait()与waitpid()

头文件

sys/wait.h

函数原型

1
2
pid_t wait(int* status);
pid_t waitpid(pid_t pid, int* status, int options);

函数说明

父进程创建子进程之后,父进程使用wait和waitpid具有监视子进程的运行状态的能力。这两个函数用于等待子进程的状态发生变化回调并且获取状态发生变化的信息,所能获取的状态变化包括:子进程运行结束,子进程被信号量暂停,子进程被信号量回复运行

父进程执行wait函数之后,父进程会被阻塞在此处,如果子进程状态发生变化,则wait函数会立即返回结果;否则wait函数会一直阻塞直到子进程状态发生变化。

通常意义上,,如果子进程状态发生了变化,但是还是未被其父进程或者其他系统回调执行wait函数,此时的子进程被称为可等待的。

子进程运行结束后父进程执行wait函数可以推动系统释放与子进程相关的资源;否则子进程将会被维持在僵尸进程(子进程已结束,而父进程还在运行)的状态下一直存在。

返回值说明

上述函数的返回值有-1,0,>0三种情况。分别对应于以下三种情况返回。

  • -1:调用出错,此时出错信息在errno中
  • 0:若waitpid的options设置了WNOHANG,且调用中没有子进程退出,立即返回0
  • >0:若大于0,返回退出进程的pid。

参数说明

  • pid:要监听的进程的ID(<-1, =-1, =0, > 0)
  • status: 用于存储出发状态变化时的信号值和exit(code)中的code值。
  • options 提供一些额外的选项控制waitpid,目前linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数宏,可以使用|连接使用.

pid详细说明:

  • pid < -1 : 监听范围为进程组为-pid的所有子进程
  • pid = -1 : 监听范围为调用wait/waitpid的所有子进程
  • pid = 0: 监听范围为子进程的进程组ID(与父进程相等)
  • pid > 0: 监听特定pid的进程

status详细说明

status用于保存出发wait的信号值或者退出时exit(code)中的code值

options详细说明

  • WNOHANG : 使用此参数调用waitpid,即使子进程没有退出,他也会立即返回,而不是像wait一直等下去
  • WUNTRACED : 用于调试,极少用

一般情况下使用值为0即可。

wait与waitpid关系

wait实质上是waitpid中pid=-1,options=0时封装,即

wait(&status)与waitpid(-1, &status, 0)完全相同

相关宏

wait.h中定义了一些宏用于解析status的值:

含义
WIFEXITED(status) 子进程正常退出返回true否则false
WEXITSTATUS(status) 当正常退出时,返回exit(code)中的code
WIFSIGNALED 子进程接受信号退出时返回true,否则false
WTERMSIG 被信号量杀死时,返回信号量的值
WIFSTOPED(status) 当子进程被信号量暂停时返回true
WSTOPSIG(status) 被信号量暂停时信号量的值

options值:

常量 含义
WNOHANG 调用wait时制定pid仍未返回,wait立即返回0,用于判断子进程有没有结束
WUNTRACED 当子进程被暂停时,则wait立即返回子进程的pid
WCONTINUED 当被暂停的子进程又被信号量恢复后,则wait立即返回子进程的pid。Linux 2.6.10及以后生效。在Mac 0S X 10.9.5上未生效。

Comment and share

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China