主要介绍linux系统的一些机制,为后面linux系统c应用开发以及python应用开发做铺垫
系统调用(system call)其实是 Linux 内核提供给应用层的应用编程接口(API),是 Linux 应用层进入内核的入口。不止 Linux 系统,所有的操作系统都会向应用层提供系统调用,应用程序通过系统调用来使用操作系统提供的各种服务。
通过系统调用,Linux 应用程序可以请求内核以自己的名义执行某些事情,譬如打开磁盘中的文件、读写文件、关闭文件以及控制其它硬件外设。通过系统调用 API,应用层可以实现与内核的交互,其关系可通过下图简单描述:
调用 open 函数会有一个返回值,这是一个 int 类型的数据,在 open函数执行成功的情况下,会返回一个非负整数,该返回值就是一个文件描述符(file descriptor),这说明文件描述符是一个非负整数;对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引。
当调用 open 函数打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符,用于指代被打开的文件,所有执行 IO 操作的系统调用都是通过文件描述符来索引到对应的文件,当调用 read/write 函数进行文件读写时,会将文件描述符传送给 read/write 函数
在 Linux 系统下,可以通过 man 命令(也叫 man 手册)来查看某一个 Linux 系统调用的帮助信息,man 命令后面跟着两个参数,数字 2 表示系统调用,man 命令除了可以查看系统调用的帮助信息外,还可以查看 Linux 命令(对应数字 1)以及标准 C 库函数(对应数字 3)所对应的帮助信息;最后一个参数 open 表示需要查看的系统调用函数名。
man 2 open #查看 open 函数的帮助信息
在 Linux 系统中要操作一个文件,需要先打开该文件,得到文件描述符,然后再对文件进行相应的读写操作(或其他操作),最后在关闭该文件;
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
·pathname:要打开或创建的文件名;
·flag:指定文件的打开方式,具体有以下参数,见下表flag 参数值:
flag | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件,该参数与O_WRONLY和O_RDWR只能三选一 |
O_WRONLY | 以只写的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 创建一个新文件 |
O_APPEND | 将数据写入到当前文件的结尾处 |
O_TRUNC | 如果pathname文件存在,则清除文件内容 |
·mode:当open 函数的flag 值设置为O_CREAT 时,必须使用mode 参数来设置文件与用户相关的权限:
参数 | 含义 |
---|---|
当前用户 | |
S_IRUSR | 用户拥有读权限 |
S_IWUSR | 用户拥有写权限 |
S_IXUSR | 用户拥有执行权限 |
S_IRWXU | 用户拥有读、写、执行权限 |
当前用户组 | |
S_IRGRP | 当前用户组的其他用户拥有读权限 |
S_IWGRP | 当前用户组的其他用户拥有写权限 |
S_IXGRP | 当前用户组的其他用户拥有执行权限 |
S_IRWXG | 当前用户组的其他用户拥有读、写、执行权限 |
其他用户 | |
S_IROTH | 其他用户拥有读权限 |
S_IWOTH | 其他用户拥有写权限 |
S_IXOTH | 其他用户拥有执行权限 |
S_IROTH | 其他用户拥有读、写、执行权限 |
用于从文件中读取若干个字节的数据,保存到数据缓冲区buf 中,并返回实际读取的字节数
#include
ssize_t read(int fd, void *buf, size_t count);
·fd:文件对应的文件描述符,可以通过fopen 函数获得。另外,当一个程序运行时,Linux默认有0、1、2 这三个已经打开的文件描述符,分别对应了标准输入、标准输出、标准错误输出,即可以直接访问这三种文件描述符;
·buf:指向数据缓冲区的指针;
·count:读取多少个字节的数据。
用于往文件写入内容,并返回实际写入的字节长度
ssize_t write(int fd, const void *buf, size_t count);
·fd:文件对应的文件描述符,可以通过fopen 函数获得。
·buf:指向数据缓冲区的指针;
·count:往文件中写入多少个字节。
可调用 close 函数关闭一个已经打开的文件
#include
int close(int fd);
fd:文件描述符,需要关闭的文件所对应的文件描述符。
返回值:如果成功返回 0,如果失败则返回-1。
除了使用 close 函数显式关闭文件之外,在 Linux 系统中,当一个进程终止时,内核会自动关闭它打开的所有文件,也就是说在我们的程序中打开了文件,如果程序终止退出时没有关闭打开的文件,那么内核会自动将程序中打开的文件关闭。很多程序都利用了这一功能而不显式地用 close 关闭打开的文件。
显式关闭不再需要的文件描述符往往是良好的编程习惯,会使代码在后续修改时更具有可读性,也更可靠,进而言之,文件描述符是有限资源,当不再需要时必须将其释放、归还于系统。
可以用与设置文件指针的位置,并返回文件指针相对于文件头的位置
#include
#include
off_t lseek(int fd, off_t offset, int whence);
·fd:文件描述符。
·offset:偏移量,以字节为单位。
·whence:用于定义参数 offset 偏移量对应的参考值,该参数为下列其中一种(宏定义):
参数 | 偏移量 |
---|---|
SEEK_SET | offset是一个绝对位置 |
SEEK_END | offset是以文件尾为参考点的相对位置 |
SEEK_CUR | offset是以当前位置为参考点的相对位置 |
保留
打开一个文件,系统内部会将这个过程分为三步:
①系统找到这个文件名所对应的 inode 编号;
②通过inode编号从 inode table 中找到对应的 inode 结构体;
③根据inode结构体中记录的信息,确定文件数据所在的 block,并读出数据。
本小节就给大家介绍了 3 中终止进程的方法:
main 函数中使用 return 后返回,return 执行后把控制权交给调用函数,结束该进程。调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统。
调用函数需要传入 status 状态标志,0 表示正常结束、若为其它值则表示程序执行过程中检测到有错误发生
#include
void _exit(int status);
#include
void _Exit(int status);
exit()函数_exit()函数都是用来终止进程的,exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。执行 exit()会执行一些清理工作,最后调用_exit()函数。
#include
void exit(int status);
库函数也就是 C 语言库函数,C 语言库是应用层使用的一套函数库,在 Linux 下,通常以动态(.so)库文件的形式提供,存放在根文件系统/lib 目录下,C 语言库函数构建于系统调用之上,也就是说库函数其实是由系统调用封装而来的,当然也不能完全这么说,原因在于有些库函数并不调用任何系统调用,譬如一些字符串处理函数 strlen()、strcat()、memcpy()、memset()、strchr()等等;而有些库函数则会使用系统调用来帮它完成实际的操作,譬如库函数 fopen 内部调用了系统调用 open()来帮它打开文件、库函数 fread()就利用了系统调用 read()来完成读文件操作、fwrite()就利用了系统调用 write()来完成写文件操作。
Linux 系统内核提供了一系列的系统调用供应用层使用,我们直接使用系统调用就可以了呀,那为何还要设计出库函数呢?事实上,有些系统调用使用起来并不是很方便,于是就出现了 C 语言库,这些 C 语言库函数的设计是为了提供比底层系统调用更为方便、更为好用、且更具有可移植性的调用接口。
来看一看它们之间的区别:
(1) 库函数是属于应用层,而系统调用是内核提供给应用层的编程接口,属于系统内核的一部分;
(2) 库函数运行在用户空间,调用系统调用会由用户空间(用户态)陷入到内核空间(内核态);
(3) 库函数通常是有缓存的,而系统调用是无缓存的,所以在性能、效率上,库函数通常要优于系统调用;
(4) 可移植性:库函数相比于系统调用具有更好的可移植性,通常对于不同的操作系统,其内核向应用层提供的系统调用往往都是不同;而对于 C 语言库函数来说,由于很多操作系统都实现了 C 语言库,所以库函数在不同操作系统之间相比于系统调用具有更好的可移植性。
对于标准 I/O 库函数来说,它们的操作是围绕 FILE 指针进行的,当使用标准 I/O 库函数打开或创建一个文件时,会返回一个指向 FILE 类型对象的指针(FILE *),使用该 FILE 指针与被打开或创建的文件相关联,然后该 FILE 指针就用于后续的标准 I/O 操作(使用标准 I/O 库函数进行 I/O 操作),所以由此可知,FILE 指针的作用相当于文件描述符,只不过 FILE 指针用于标准 I/O 库函数中、而文件描述符则用于文件I/O 系统调用中。
FILE 是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,包括用于实际I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等。FILE数据结构定义在标准 I/O 库函数头文件 stdio.h 中。
#include
FILE *fopen(const char *pathname, const char *mode);
·返回值:调用成功返回一个指向 FILE 类型对象的指针(FILE *),该指针与打开或创建的文件相关联,后续的标准 I/O 操作将围绕 FILE 指针进行。如果失败则返回 NULL,并设置 errno 以指示错误原因。
·pathname 参数用于指定要打开或创建的文件名。
·mode 参数用于指定文件的打开方式,注意该参数是一个字符串,输入时需要带双引号:
mode | 说明 | 对应于open()函数的 flags 参数取值 |
---|---|---|
r | 以只读方式打开文件。 | O_RDONLY |
r+ | 以可读、可写方式打开文件。 | O_RDWR |
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 |
#include
//用于从文件流中读取数
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
//写文件,用于把数据写入到文件流
size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream);
·ptr:fread()将读取到的数据存放在参数 ptr 指向的缓冲区中;
·size:fread()从文件读取 nmemb个数据项,每一个数据项的大小为size个字节,所以总共读取的数据大小为nmemb * size个字节。
·stream:FILE 指针,是使用fopen 打开的文件流,fread 通过它指定要访问的文件,它从该文件中读取
·nmemb项数据,每项的大小为size,读取到的数据会被存储在ptr 指向的数组中。
·返回值:为成功读取的项数(项的单位为size)。
用于关闭指定的文件流,关闭时它会把尚未写到文件的内容都写出。因为标准库会对数据进行缓冲,所以需要使用fclose 来确保数据被写出。
#include
int close(int fd);
用于把尚未写到文件的内容立即写出。常用于确保前面操作的数据被写入到磁盘上。fclose 函数本身也包含了fflush 的操作。
#include
int fflush(FILE *stream);
用于设置下一次读写函数操作的位置
#include
int fseek(FILE *stream, long offset, int whence);
·offset 参数用于指定位置
·whence 参数则定义了offset 的意义
·whence 的可取值如下:
参数 | 功能 |
---|---|
SEEK_SET | offset是一个绝对位置。 |
SEEK_END | offset是以文件尾为参考点的相对位置。 |
SEEK_CUR | offset是以当前位置为参考点的相对位置。 |
⑦库函数 ftell()可用于获取文件当前的读写位置偏移量
long ftell(FILE *stream);
①feof()函数
int feof(FILE *stream);
库函数feof()用于测试参数 stream 所指文件的 end-of-file 标志,如果end-of-file标志被设置了,则调用feof()函数将返回一个非零值,如果end-of-file标志没有被设置,则返回0
②ferror()函数
int ferror(FILE *stream);
库函数 ferror()用于测试参数 stream 所指文件的错误标志,如果错误标志被设置了,则调用 ferror()函数将返回一个非零值,如果错误标志没有被设置,则返回 0。
③clearerr()函数
void clearerr(FILE *stream);
库函数 clearerr()用于清除 end-of-file 标志和错误标志,当调用 feof()或 ferror()校验这些标志后,通常需要清除这些标志,避免下次校验时使用到的是上一次设置的值,此时可以手动调用 clearerr()函数清除标志。
#include
int puts(const char *s); //输出字符串,只能输出到标准输出设备
int putchar(int c); //输出一个无符号字符,只能输出到标准输出设备
int fputc(int c, FILE *stream); //可把字符输出到指定的文件中
int fputs(const char *s, FILE *stream); //输出字符串
函数参数和返回值含义如下:
#include
char *gets(char *s);
int getchar(void); //从标准输入设备中读取一个字符
char *fgets(char *s, int size, FILE *stream);
int fgetc(FILE *stream);
#include
size_t strlen(const char *s);
sizeof 和 strlen 的区别:
strncat()与 strcat()的区别在于,strncat 可以指定源字符串追加到目标字符串的字符数量
#include
char *strcat(char *dest, const char *src);
char *strncat(char *dest, const char *src, size_t n);
函数参数和返回值含义如下:
strncpy()与 strcpy()的区别在于,strncpy()可以指定从源字符串 src 复制到目标字符串 dest 的字符数量
#include
char *strcpy(char *dest, const char *src);
char *strncpy(char *dest, const char *src, size_t n);
函数参数和返回值含义如下:
参数 c 虽然是以 int 类型传递,但 memset()函数在填充内存块时是使用该值的无符号字符形式,也就是函数内部会将该值转换为 unsigned char 类型的数据,以字节为单位进行数据填充
#include
void *memset(void *s, int c, size_t n);
函数参数和返回值含义如下:
对数组 str 进行初始化操作,将其存储的数据全部设置为 0。
#include
void bzero(void *s, size_t n);
函数参数和返回值含义如下:
#include
int strcmp(const char *s1, const char *s2);
函数参数和返回值含义如下:
#include
int strncmp(const char *s1, const char *s2, size_t n);
函数参数和返回值含义如下:
#include
char *strchr(const char *s, int c);
char *strrchr(const char *s, int c);
strrchr()函数在字符串中是从后到前(或者称为从右向左)查找字符,找到字符第一次出现的位置就返回,返回值指向这个位置,
函数参数和返回值含义如下:
#include
char *strstr(const char *haystack, const char *needle);
函数参数和返回值含义如下:
使用 atoi()、atol()、atoll()函数只能转换十进制表示的数字字符串,即 0~9。
#include
int atoi(const char *nptr);
long atol(const char *nptr);
long long atoll(const char *nptr);
函数参数和返回值含义如下:
strtol()、strtoll()两个函数可分别将字符串转为 long int 类型数据和 long long ing 类型数据,与 atol()、atoll()之间的区别在于,strtol()、strtoll()可以实现将多种不同进制数(譬如二进制表示的数字字符串、八进制表示的数字字符串、十六进制表示的数数字符串)表示的字符串转换为整形数据
#include
long int strtol(const char *nptr, char **endptr, int base);
long long int strtoll(const char *nptr, char **endptr, int base);
函数参数和返回值含义如下:
这两个函数使用方法与 strtol()、strtoll()一样,区别在于返回值的类型不同,strtoul()返回值类型是 unsigned long int,strtoull()返回值类型是 unsigned long long int
#include
unsigned long int strtoul(const char *nptr, char **endptr, int base);
unsigned long long int strtoull(const char *nptr, char **endptr, int base);
atof()用于将字符串转换为一个 double 类型的浮点数据
#include
double atof(const char *nptr);
函数参数和返回值含义如下:
strtof()、strtod()以及 strtold()三个库函数可分别将字符串转换为 float 类型数据、double 类型数据、long double 类型数据
#include
double strtod(const char *nptr, char **endptr);
float strtof(const char *nptr, char **endptr);
long double strtold(const char *nptr, char **endptr);
数字转换为字符串推荐大家使用前面介绍的格式化 IO 相关库函数,譬如使用 printf()将数字转字符串、并将其输出到标准输出设备或者使用 sprintf()或 snprintf()将数字转换为字符串并存储在缓冲区中
正则表达式描述了一种字符串的匹配模式(pattern),可以用来检查一个给定的字符串中是否含有某种子字符串、将匹配的字符串替换或者从某个字符串中取出符合某个条件的子字符串。
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
regmatch_t pmatch = {0};
regex_t reg;
char errbuf[64];
int ret;
char *sptr;
int length;
int nmatch; //最多匹配出的结果
if (4 != argc) {
/**********************************
* 执行程序时需要传入两个参数:
* arg1: 正则表达式
* arg2: 待测试的字符串
* arg3: 最多匹配出多少个结果
**********************************/
fprintf(stderr, "usage: %s \n" , argv[0]);
exit(0);
}
/* 编译正则表达式 */
if(ret = regcomp(®, argv[1], REG_EXTENDED)) {
regerror(ret, ®, errbuf, sizeof(errbuf));
fprintf(stderr, "regcomp error: %s\n", errbuf);
exit(0);
}
/* 赋值操作 */
sptr = argv[2]; //待测试的字符串
length = strlen(argv[2]);//获取字符串长度
nmatch = atoi(argv[3]); //获取最大匹配数
/* 匹配正则表达式 */
for (int j = 0; j < nmatch; j++) {
char temp_str[100];
/* 调用 regexec 匹配正则表达式 */
if(ret = regexec(®, sptr, 1, &pmatch, 0)) {
regerror(ret, ®, errbuf, sizeof(errbuf));
fprintf(stderr, "regexec error: %s\n", errbuf);
goto out;
}
if(-1 != pmatch.rm_so) {
if (pmatch.rm_so == pmatch.rm_eo) {//空字符串
sptr += 1;
length -= 1;
printf("\n"); //打印出空字符串
if (0 >= length)//如果已经移动到字符串末尾、则退出
break;
continue; //从 for 循环开始执行
}
memset(temp_str, 0x00, sizeof(temp_str));//清零缓冲区
memcpy(temp_str, sptr + pmatch.rm_so,
pmatch.rm_eo - pmatch.rm_so);//将匹配出来的子字符串拷贝到缓冲区
printf("%s\n", temp_str); //打印字符串
sptr += pmatch.rm_eo;
length -= pmatch.rm_eo;
if (0 >= length)
break;
}
}
/* 释放正则表达式 */
out:
regfree(®);
exit(0);
}
①进程概念
进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。Linux 系统下的每一个进程都有一个进程号(processID,简称 PID),进程号是一个正数,用于唯一标识系统中的某一个进程。
②环境变量的作用
环境变量常见的用途之一是在 shell 中,每一个环境变量都有它所表示的含义,譬如 HOME 环境变量表示用户的家目录,USER 环境变量表示当前用户名,SHELL 环境变量表示 shell 解析器名称,PWD 环境变量表示当前所在目录等,在我们自己的应用程序当中,也可以使用进程的环境变量。
①内存布局
②虚拟地址空间
在 Linux 系统中,每一个进程都在自己独立的地址空间中运行,在 32 位系统中,每个进程的逻辑地址空间均为 4GB,这 4GB 的内存空间按照 3:1 的比例进行分配,其中用户进程享有 3G 的空间,而内核独自享有剩下的 1G 空间
调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于 dup(),这也意味着父、子进程对应的文件描述符均指向相同的文件表,如下图所示:
Linux 内核提供了多种 IPC 机制,基本是从 UNIX 系统继承而来,而对 UNIX 发展做出重大贡献的两大主力 AT&T 的贝尔实验室及 BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对 UNIX 早期的进程间通信手段进行了系统的改进和扩充,形成了“System V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接字(Socket,也就是网络)的进程间通信机制。Linux 则把两者继承了下来,如下如所示:
其中,早期的 UNIX IPC 包括:管道、FIFO、信号;System V IPC 包括:System V 信号量、System V消息队列、System V 共享内存;上图中还出现了 POSIX IPC,事实上,较早的 System V IPC 存在着一些不足之处,而 POSIX IPC 则是在 System V IPC 的基础上进行改进所形成的,弥补了 System V IPC 的一些不足之处。POSIX IPC 包括:POSIX 信号量、POSIX 消息队列、POSIX 共享内存。总结如下:
线程是操作系统能够调度和执行的基本单位,在 Linux 中也被称之为轻量级进程。在 Linux 系统中,一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如 cpu、内存、文件等等),而将线程分配到某个 cpu 上执行。一个进程可以拥有多个线程,它还可以同时使用多个cpu 来执行各个线程,以达到最大程度的并行,提高工作的效率;同时,即使是在单 cpu 的机器上,也依然可以采用多线程模型来设计程序,使设计更简洁、功能更完备,程序的执行效率也更高
并行与串行则截然不同,并行指的是可以并排/并列执行多个任务,这样的系统,它通常有多个执行单元,所以可以实现并行运行,譬如并行运行 task1、task2、task3。并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行着
相比于串行和并行,并发强调的是一种时分复用,与串行的区别在于,它不必等待上一个任务完成之后在做下一个任务,可以打断当前执行的任务切换执行下一个任何,这就是时分复用。在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样轮训(交叉/交替执行),这就是并发运行。如下图所示:
对于一个单线程进程来说,它不需要处理线程同步的问题,所以线程
同步是在多线程环境下可能需要注意的一个问题。线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一致的问题。
当一个线程修改变量时,其它的线程在读取这个变量时可能会看到不一致的值,图 12.1.1 描述了两个线程读写相同变量(共享变量、共享资源)的假设例子。在这个例子当中,线程 A 读取变量的值,然后再给这个变量赋予一个新的值,但写操作需要 2 个时钟周期(这里只是假设);当线程 B 在这两个写周期中间读取了这个变量,它就会得到不一致的值,这就出现了数据不一致的问题。
一般有以下同步方式:
信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以信号提供了一种处理异步事件的方法。
信号名称 | 编号 | 描述 | 系统默认操作 |
---|---|---|---|
SIGHUP | 1 | Hangup (POSIX) | |
SIGINT | 2 | 终端中断符 | term |
SIGQUIT | 3 | 终端退出符 | term+core |
SIGILL | 4 | 非法硬件指令 | term+core |
SIGTRAP | 5 | Trace trap (POSIX) | |
SIGABRT | 6 | 异常终止(abort) | term+core |
SIGBUS | 7 | 内存访问错误 | term+core |
SIGFPE | 8 | 算术异常 | term+core |
SIGKILL | 9 | 终极终止信号 | term |
SIGUSR1 | 10 | 用户自定义信号 1 | term |
SIGSEGV | 11 | 无效的内存引用 | term+core |
SIGUSR2 | 12 | 用户自定义信号 2 | term |
SIGPIPE | 13 | 管道关闭 | term |
SIGALRM | 14 | 定时器超时(alarm) | term |
SIGTERM | 15 | 终止进程 | term |
SIGSTKFLT | 16 | Stack fault. | |
SIGCHLD/SIGCLD | 17 | 子进程终止或停止 | ignore |
SIGCONT | 18 | 使停止状态的进程继续运行 | cont |
SIGSTOP | 19 | 停止进程 | stop |
SIGTSTP | 20 | 终端停止符 | stop |
SIGXCPU | 24 | 超过 CPU 限制 | term+core |
SIGVTALRM | 26 | 虚拟定时器超时 | term |
SIGWINCH | 28 | 终端窗口尺寸发生变化 | ignore |
SIGPOLL/SIGIO | 29 | 异步 I/O | term/ignore |
SIGSYS | 31 | 无效系统调用 | term+core |
信号的分类:
从可靠性方面将信号分为可靠信号与不可靠信号
从实时性方面将信号分为实时信号与非实时信号
实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收,实时信号是 POSIX 标准的一部分,可用于应用进程。一般我们也把非实时信号(不可靠信号)称为标准信号
signal()函数是 Linux 系统下设置信号处理方式最简单的接口,可将信号的
处理方式设置为捕获信号、忽略信号以及系统默认操作
#include
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
函数参数和返回值含义如下:
/* Fake signal functions. */
#define SIG_ERR ((sig_t) -1) /* Error return. */
#define SIG_DFL ((sig_t) 0) /* Default action. */
#define SIG_IGN ((sig_t) 1) /* Ignore signal. */
gaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
与 kill 命令相类似,Linux 系统提供了 kill()系统调用,一个进程可通过 kill()向另一个进程发送信号;除了 kill()系统调用之外,Linux 系统还提供了系统调用 killpg()以及库函数 raise(),也可用于实现发送信号的功能,
kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程
#include
#include
int kill(pid_t pid, int sig);
函数参数和返回值含义如下:
参数 pid 不同取值含义:
有时进程需要向自身发送信号,raise()函数可用于实现这一要求
#include
int raise(int sig);
函数参数和返回值含义如下:
使用 alarm()函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM信号
#include
unsigned int alarm(unsigned int seconds);
函数参数和返回值:
pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause()才返回,在这种情况下,pause()返回-1,并且将 errno 设置为 EINTR
如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中,为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()函数获取。
#include
int sigpending(sigset_t *set);
函数参数和返回值含义如下:
判断 SIGINT 信号当前是否处于等待状态:
/* 定义信号集 */
sigset_t sig_set;
/* 将信号集初始化为空 */
sigemptyset(&sig_set);
/* 获取当前处于等待状态的信号 */
sigpending(&sig_set);
/* 判断 SIGINT 信号是否处于等待状态 */
if (1 == sigismember(&sig_set, SIGINT))
puts("SIGINT 信号处于等待状态");
else if (!sigismember(&sig_set, SIGINT))
puts("SIGINT 信号未处于等待状态");
#include
int sigqueue(pid_t pid, int sig, const union sigval value);
函数参数和返回值含义如下:
使用 exit()、_exit()或_Exit()这些函数来终止进程,然后这些方法使用于正常退出应用程序,而对于异常退出程序,则一般使用 abort()库函数,使用 abort()终止进程运行,会生成核心转储文件,可用于判断程序调用 abort()时的程序状态。
#include
void abort(void);
函数 abort()通常产生 SIGABRT 信号来终止调用该函数的进程,SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;当调用 abort()函数之后,内核会向进程发送 SIGABRT 信号。
一个信号从进程中产生,发送给另一个进程,这其实也是信号类型的通信,不过由于只传递信号值,没有数据传递,在很多时候无法满足我们的需求,因此管道传输数据的功能在某些场合会很有优势。
什么是管道呢?当数据从一个进程连接流到另一个进程时,这之间的连接就是一个管道(pipe)。我们通常是把一个进程的输出通过管道连接到另一个进程的输入。对于 shell 命令来说,命令的连接是通过管道字符来完成的
匿名管道(PIPE)是一种特殊的文件,但虽然它是一种文件,却没有名字,因此一般进程无法使用 open() 来获取他的描述符,它只能在一个进程中被创建出来,然后通过继承的方式将他的文件描述符传递给子进程,这就是为什么匿名管道只能用于亲缘关系进程间通信的原因。另外,匿名管道不同于一般文件的显著之处是:它有两个文件描述符,而不是一个,一个只能用来读,另一个只能用来写,这就是所谓的“半双工”通信方式。而且它对写操作不做任何保护,即:假如有多个进程或线程同时对匿名管道进行写操作,那么这些数据很有可能会相互践踏,因此一个简单的结论是:匿名管道只能用于一对一的亲缘进程通信。最后,匿名管道不能使用 lseek() 来进行所谓的定位,因为他们的数据不像普通文件那样按块的方式存放在诸如硬盘、flash 等块设备上。
命名管道(FIFO)与匿名管道(PIPE)是不同的,命名管道可以在多个无关的进程中交换数据(通信)。我们知道,匿名管道的通信方式通常都由一个共同的祖先进程启动,只能在”有血缘关系”的进程中交互数据,这给我们在不相关的的进程之间交换数据带来了不方便,因此产生了命名管道,来解决不相关进程间的通信问题。
命名管道不同于无名管道之处在于它提供了一个路径名与之关联,以一个文件形式存在于文件系统中,这样,即使与命名管道的创建进程不存在“血缘关系”的进程,只要可以访问该命名管道文件的路径,就能够彼此通过命名管道相互通信,因为可以通过文件的形式,那么就可以调用系统中对文件的操作,如打开(open)、读(read)、写(write)、关闭(close)等函数,虽然命名管道文件存储在文件系统中,但数据却是存在于内存中的,这点要区分开。
pipe() 函数用于创建一个匿名管道,一个可用于进程间通信的单向数据通道
#include
int pipe(int pipefd[2]);
**注意:**数组 pipefd 是用于返回两个引用管道末端的文
件描述符,它是一个由两个文件描述符组成的数组的指针。pipefd[0] 指管道的读取端,pipefd[1]指向管道的写端,向管道的写入端写入数据将会由内核缓冲,即写入内存中,直到从管道的读取端读取数据为止,而且数据遵循先进先出原则。pipe() 函数还会返回一个 int 类型的变量,如果为0 则表示创建匿名管道成功,如果为-1 则表示创建失败,并且设置 errno。
至此,我们还只能在有“血缘关系”的程序之间传递数据,即这些程序是由一个共同的祖先进程启动的。但如果想在不相关的进程之间交换数据,我们可以用 FIFO 文件来完成这项工作,或者称之为命名管道。命名管道是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但它的的数据却是存储在内存中的。我们可以在终端(命令行)上创建命名管道,也可以在程序中创建它。
int mkfifo(const char * pathname,mode_t mode);
mkfifo() 会根据参数 pathname 建立特殊的 FIFO 文件,而参数 mode 为该文件的模式与权限。
mkfifo() 创建的 FIFO 文件其他进程都可以进行读写操作,可以使用读写一般文件的方式操作它,如 open,read,write,close 等。
mode 模式及权限参数说明:
函数返回值说明如下:
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。
消息队列与信号的对比:
信号承载的信息量少,而消息队列可以承载大量自定义的数据。
消息队列与管道的对比:
• 消息队列跟命名管道有不少的相同之处,它与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write(),接收数据用 read(),则在消息队列中,发送数据用msgsnd(),接收数据用msgrcv(),消息队列对每个数据都有一个最大长度的限制。
• 消息队列也可以独立于发送和接收进程而存在,在进程终止时,消息队列及其内容并不会被删除。
• 管道只能承载无格式字节流,消息队列提供有格式的字节流,可以减少了开发人员的工作量。
• 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级,接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。
• 消息队列可以实现消息的随机查询,消息不一定要以先进先出的顺序接收,也可以按消息的类型接收。
Linux 内核提供了一系列函数来使用消息队列:
收发消息前需要具体的消息队列对象,msgget() 函数的作用是创建或获取一个消息队列对象,并返回消息队列标识符。函数原型如下:
int msgget(key_t key, int msgflg);
若执行成功返回队列 ID,失败返回-1。它的两个输入参数说明如下:
这个函数的主要作用就是将消息写入到消息队列,俗称发送一个消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数说明:
msgrcv() 函数是从标识符为 msqid 的消息队列读取消息并将消息存储到 msgp 中,读取后把此消息从消息队列中删除,也就是俗话说的接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数说明:
– 0: 阻塞式接收消息,没有该类型的消息 msgrcv 函数一直阻塞等待
– IPC_NOWAIT:若在消息队列中并没有相应类型的消息可以接收,则函数立即返回,此时错误码为 ENOMSG
– IPC_EXCEPT:与 msgtype 配合使用返回队列中第一个类型不为 msgtype 的消息
– IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的 size 字节,则把该消息截断,截断部分将被丢弃
消息队列是可以被用户操作的,比如设置或者获取消息队列的相关属性
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数说明:
什么是共享内存?顾名思义,共享内存就是将内存进行共享,它允许多个不相关的进程访问同一个逻辑内存,直接将一块裸露的内存放在需要数据传输的进程面前,让它们自己使用。因此,共享内存是效率最高的一种 IPC 通信机制,它可以在多个进程之间共享和传递数据,进程间需要共享的数据被放在共享内存区域,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去,因此所有进程都可以访问共享内存中的地址,就好像它们是由用 C 语言函数 malloc 分配的内存一样。
int shmget(key_t key, size_t size, int shmflg);
参数说明:
– 0 或 IPC_PRIVATE。当 key 的取值为 IPC_PRIVATE,则函数 shmget() 创建一块新的共享内存;如果 key 的取值为 0,而参数 shmflg 中设置了 IPC_PRIVATE 这个标志,则同样将创建一块新的共享内存。
– 大于 0 的 32 位整数:视参数 shmflg 来确定操作。
– IPC_CREAT:如果内核中不存在关键字与 key 相等的共享内存,则新建一个共享内存;如果存在这样的共享内存,返回此共享内存的标识符。
– IPC_EXCL:如果内核中不存在键值与 key 相等的共享内存,则新建一个共享内存;如
果存在这样的共享内存则报错。
– SHM_HUGETLB:使用“大页面”来分配共享内存,所谓的“大页面”指的是内核为了提高程序性能,对内存实行分页管理时,采用比默认尺寸(4KB)更大的分页,以减少缺页中断。Linux 内核支持以 2MB 作为物理页面分页的基本单位。
– SHM_NORESERVE:不在交换分区中为这块共享内存保留空间。
如果一个进程想要访问这个共享内存,那么需要将其映射到进程的虚拟空间中,然后再去访问它,那么系统提供的 shmat() 函数就是把共享内存区对象映射到调用进程的地址空间。
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
– SHM_RDONLY:以只读方式映射共享内存。
– SHM_REMAP:重新映射,此时 shmaddr 不能为 NULL。
– NULLSHM:自动选择比 shmaddr 小的最大页对齐地址。
int shmdt(const void *shmaddr);
参数说明:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
信号量与已经介绍过的信号、管道、FIFO 以及消息列队不同,它本质上是一个计数器,用于协调多进程间对共享数据对象的读取,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得该临界资源在一个时刻只有一个进程独享。可能会有同学问了,为什么不使用全局变量呢?那是因为全局变量并不能在进程间共同使用,因为进程间是相互独立的,而且也无法保证引用计数的原子操作,因此使用系统提供的信号量即可。
由于信号量只能进行两种操作:等待和发送信号,即 P 操作和 V 操作,锁行为就是 P 操作,解锁就是 V 操作,可以直接理解为 P 操作是申请资源,V 操作是释放资源。PV 操作是计算机操作系统需要提供的基本功能之一,它们的行为是这样的:
semget 函数的功能是创建或者获取一个已经创建的信号量,如果成功则返回对应的信号量标识符,失败则返回-1
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
参数说明:
int semop(int semid, struct sembuf *sops, size_t nsops);
参数说明:
semctl 函数主要是对信号量集的一系列控制操作,根据操作命令 cmd 的不同,执行不同的操作
int semctl(int semid, int semnum, int cmd, ...);
信号量(Semaphore)是一种实现进程/线程间通信的机制,可以实现进程/线程之间同步或临界资源的互斥访问,常用于协助一组相互竞争的进程/线程来访问临界资源。在多进程/线程系统中,各进程/线程之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。
在 POSIX 标准中,信号量分两种,一种是无名信号量,一种是有名信号量。无名信号量一般用于进程/线程间同步或互斥,而有名信号量一般用于进程间同步或互斥。有名信号量和无名信号量的差异在于创建和销毁的形式上,但是其他工作一样,无名信号量则直接保存在内存中,而有名信号量则要求创建一个文件。
有名信号量其实是一个文件,它的名字由类似 “sem.[信号量名字]”这样的字符串组成,注意看文件名前面有 “sem.”,它是一个特殊的信号量文件,在创建成功之后,系统会将其放置在/dev/shm 路径下,不同的进程间只要约定好一个相同的信号量文件名字,就可以访问到对应的有名信号量,并且借助信号量来进行同步或者互斥操作,需要注意的是,有名信号量是一个文件,在进程退出之后它们并不会自动消失,而需要手工删除并释放资源。
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。
# define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
#include
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
函数参数和返回值含义如下:
#include
int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
调用 pthread_mutex_lock()函数对互斥锁进行上锁,如果互斥锁处于未锁定状态,则此次调用会上锁成功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用 pthread_mutex_lock()会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。
调用 pthread_mutex_unlock()函数将已经处于锁定状态的互斥锁进行解锁
当互斥锁已经被其它线程锁住时,调用 pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用 pthread_mutex_trylock()函数;调用 pthread_mutex_trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用 pthread_mutex_trylock()将会锁住互斥锁并立马返回,如果互斥锁已经被其它线程锁住,调用 pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码 EBUSY。
#include
int pthread_mutex_trylock(pthread_mutex_t *mutex);
#include
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数 mutex 指向目标互斥锁;同样在调用成功情况下返回 0,失败返回一个非 0 值的错误码。
条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。使用条件变量主要包括两个动作:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
#include
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
条件变量的主要操作便是发送信号(signal)和等待。发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。
函数 pthread_cond_signal()和 pthread_cond_broadcast()均可向指定的条件变量发送信号,通知一个或多个处于等待状态的线程。调用 pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。
#include
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
在 I/O 多路复用中,进程通过系统调用 select()或 poll()来主动查询文件描述符上是否可以执行 I/O 操作。而在异步 I/O 中,当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务直到文件描述符可以执行 I/O 操作为止,此时内核会发送信号给进程。所以要使用异步 I/O,还得结合前面所学习的信号相关的内容,所以异步 I/O 通常也称为信号驱动 I/O。
要使用异步 I/O,程序需要按照如下步骤来执行:
关于“阻塞”一词前面已经给大家多次提到,阻塞其实就是进入了休眠状态,交出了 CPU 控制权。前面所学习过的函数,譬如 wait()、pause()、sleep()等函数都会进入阻塞,本小节来聊一聊关于阻塞式 I/O 与非阻塞式 I/O。
阻塞式 I/O 顾名思义就是对文件的 I/O 操作(读写操作)是阻塞式的,非阻塞式 I/O 同理就是对文件的I/O 操作是非阻塞的。这样说大家可能不太明白,这里举个例子,譬如对于某些文件类型(读管道文件、网络设备文件和字符设备文件),当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这就是阻塞式 I/O 常见的一种表现;如果是非阻
塞式 I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误!
普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的;但是对于某些文件类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O
进行操作
I/O 多路复用(IO multiplexing)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行 I/O 操作时,能够通知应用程序进行相应的读写操作。I/O 多路复用技术是为了解决:在并发式 I/O 场景中进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的I/O 系统调用。
由此可知,I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞 I/O,譬如程序中既要读取鼠标、又要读取键盘,多路读取。
我们可以采用两个功能几乎相同的系统调用来执行 I/O 多路复用操作,分别是系统调用 select()和 poll()。这两个函数基本是一样的,细节特征上存在些许差别!
I/O 多路复用存在一个非常明显的特征:外部阻塞式,内部监视多路 I/O。
系统调用 select()可用于执行 I/O 多路复用操作,调用 select()会一直阻塞,直到某一个或多个文件描述符成为就绪态(可以读或写)。
#include
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
这些参数按照如下方式使用:
- 返回-1 表示有错误发生,并且会设置 errno。可能的错误码包括 EBADF、EINTR、EINVAL、EINVAL以及 ENOMEM,EBADF 表示 readfds、writefds 或 exceptfds 中有一个文件描述符是非法的;EINTR表示该函数被信号处理函数中断了,其它错误大家可以自己去看,在 man 手册都有相信的记录。
- 返回 0 表示在任何文件描述符成为就绪态之前 select()调用已经超时,在这种情况下,readfds,writefds 以及 exceptfds 所指向的文件描述符集合都会被清空。
- 返回一个正整数表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符的个数,在这种情况下,每个返回的文件描述符集合都需要检查,通过 FD_ISSET()宏进行检查,以此找出发生的 I/O 事件是什么。如果同一个文件描述符在 readfds,writefds 以及 exceptfds 中同时被指定,且它多于多个 I/O 事件都处于就绪态的话,那么就会被统计多次,换句话说,select()返回三个集合中被标记为就绪态的文件描述符的总数
在 poll()函数中,则需要构造一个 struct pollfd 类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数参数含义如下:
存储映射 I/O(memory-mapped I/O)是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write 操作)。这样就可以在不使用基本 I/O 操作函数 read()和 write()的情况下执行 I/O 操作。
为了实现存储映射 I/O 这一功能,我们需要告诉内核将一个给定的文件映射到进程地址空间中的一块内存区域中,这由系统调用 mmap()来实现。
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
函数参数和返回值含义如下:
#include
int munmap(void *addr, size_t length);
使用系统调用 mprotect()可以更改一个现有映射区的保护要求
#include
int mprotect(void *addr, size_t len, int prot);
对于存储 I/O 来说亦是如此,写入到文件映射区中的数据也不会立马刷新至磁盘设备中,而是会在我们将数据写入到映射区之后的某个时刻将映射区中的数据写入磁盘中。所以会导致映射区中的内容与磁盘文件中的内容不同步。我们可以调用 msync()函数将映射区中的数据刷写、更新至磁盘文件中(同步操作),系统调用 msync()类似于 fsync()函数,不过 msync()作用于映射区
#include
int msync(void *addr, size_t length, int flags);