linux应用基础

io操作

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
 char buff[1024];
 int fd1, fd2;
 int ret;
 /* 打开源文件 src_file(只读方式) */
 fd1 = open("./src_file", O_RDONLY);
 if (-1 == fd1)
 return fd1;
 /* 打开目标文件 dest_file(只写方式) */
 fd2 = open("./dest_file", O_WRONLY);
 if (-1 == fd2) {
 ret = fd2;
 goto out1;
 }
 /* 读取源文件 1KB 数据到 buff 中 */
 ret = read(fd1, buff, sizeof(buff));
 if (-1 == ret)
 goto out2;
 /* 将 buff 中的数据写入目标文件 */
 ret = write(fd2, buff, sizeof(buff));
 if (-1 == ret)
 goto out2;
 ret = 0;
out2:
 /* 关闭目标文件 */
  close(fd2);
out1:
 /* 关闭源文件 */
 close(fd1);
 return ret; }

fd是一个 int 类型的数据,在 open函数执行成功的情况下,会返回一个非负整数,该返回值就是一个文件描述符(file descriptor)。一般一个进程打开的fd是有限的,默认是1024。当我们在程序中,调用 open 函数打开文件的时候,分配的文件描述符一般都是从 3 开始,这里大家可能要问了,上面不是说从 0 开始的吗,确实是如此,但是 0、1、2 这三个文件描述符已经默认被系统占用
了,分别分配给了系统标准输入(0)、标准输出(1)以及标准错误(2)。
在 Linux 系统下,可以通过 man 命令(也叫 man 手册)来查看某一个 Linux 系统调用的帮助信息,man命令可以将该系统调用的详细信息显示出来,譬如函数功能介绍、函数原型、参数、返回值以及使用该函数所需包含的头文件等信息;

man 2 open #查看 open 函数的帮助信息

Tips:man 命令后面跟着两个参数,数字 2 表示系统调用,man 命令除了可以查看系统调用的帮助信息外,还可以查看 Linux 命令(对应数字 1)以及标准 C 库函数(对应数字 3)所对应的帮助信息;最后一个参数 open 表示需要查看的系统调用函数名。
先用open函数打开文件得到fd,之后才能用read,write,close进行操作,其中open需要用库<include sys/types.h>,<include sys/stat.h>

off_t off = lseek(fd, 0, SEEK_SET);//将读写位置移动到文件开头处
off_t off = lseek(fd, 0, SEEK_END);//将读写位置移动到文件末尾:
off_t off = lseek(fd, 100, SEEK_SET);//将读写位置移动到偏移文件开头 100 个字节处:
off_t off = lseek(fd, 0, SEEK_CUR);//获取当前读写位置偏移量

linux文件管理:
静态文件:文件存放在磁盘文件系统中,并且以一种固定的形式进行存放
硬盘的最小存储单位叫做“扇区”(Sector),每个扇区储存 512 字节(相当于 0.5KB),操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是 4KB,即连续八个 sector 组成一个 block。
硬盘分为两个区,一个存储数据,一个存储inode table (类似设备树),
inode 实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、文件类型、文件数据存储的 block(块)位置等等信息。在这里插入图片描述
动态文件:当我们调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。
同时我们操作的时候,其实是对内存的(动态文件)进行操作,但是静态文件和动态文件是不同步的,这些最后需要内核进行同步更新到磁盘设备。
具体事件如,打开大文件比较慢,或者写文件突然关机,文件丢失等。
linux下的进程控制块(Process control block,缩写PCB):设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块。
PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文件状态标志、引用计数、当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表,其示意图如下所示:
通俗理解就是这个进程(应用程序),需要打开的文件记录,inode指针,使用次数,读写偏移量等。一个文件表代表一个文件的记录,inode指向其在磁盘位置。在这里插入图片描述
ps:一次进程中多次打开一个文件,但是内存中也只有一份动态文件。
同时一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的。即同一个文件被多次打开,会得到多个不同的文件描述符,也就意味着会有多个不同的文件表,而文件读写偏移量信息就记录在文件表数据结构中。
linux下erro;
errno是一个全局变量,操作系统会将这个错误所对应的编号赋值给 errno 变量,本质上是一个 int 类型的变量,用于存储错误编号,需要#include <errno.h>。
strerror(),该函数可以将对应的 errno 转换成适合我们查看的字符串信息,调用时需要#include <string.h>

printf("Error: %s\n", strerror(errno));
	除了 strerror 函数之外,我们还可以使用 perror 函数来查看错误信息,#include <stdio.h>
perror("open error");

linux下exit
linux下退出,一般原则程序执行正常退出 return 0,而执行函数出错退出 return -1。而当遇到异常退出(不是出错退出!!!是系统异常)
常用的异常退出有exit()、_exit()以及_Exit()。
其中系统调用_exit()以及_Exit()会清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统。
exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。执行 exit()会执行一些清理工作,最后调用_exit()函数。
空洞文件(hole file):
lseek()系统调用,使用 lseek 可以修改文件的当前读写位置偏移量,此函数不但可以改变位置偏移量,并且还允许文件偏移量超出文件长度。
如果一个文件只有4k,通过lseek写了6000后面的内容,4k到6k之间就是空洞文件,中间这段并不会占用任何物理空间。
空洞文件对多线程共同操作文件是及其有用的,有时候我们创建一个很大的文件,如果单个线程从头开始依次构建该文件需要很长的时间,有一种思路就是将文件分为多段,然后使用多线程来操作,每个线程负责其中一段数据的写入。例如迅雷下载时,还没下载完先占用对于地址内存。
open操作位:O_TRUNC 这个标志的作用非常简单,如果使用了这个标志,调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0;fd = open("./test_file", O_WRONLY | O_TRUNC);
open 函数携带了 O_APPEND 标志,调用 open 函数打开文件,当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。fd = open("./test_file", O_RDWR | O_APPEND);
其中文件标识符也可以复制,使用函数dup复制,复制得到的文件描述符与旧的文件描述符指向的是同一个文件表,所以可知,这两个文件描述符的
属性是一样。
文件共享
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
竞争冒险:操作共享资源的两个进程(或线程),其操作之后的所得到的结果往往是不可预期的,因为每个进程(或线程)去操作文件的顺序是不可预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争状态。
大概意思就是,当两个进程同时操作一个文件,使用了open函数进行写操作,每个进程都有它自己的进程控制块 PCB,有自己的文件表(意味着有自己独立的读写位置偏移量),但是共享同一个 inode 节点(也就是对应同一个文件)。
进程 A 调用了 lseek 函数,它将进程 A 的该文件当前位置偏移量设置为 1500 字节处,准备进行写操作,但是时间耗尽,内核切换到了进程 B,进程 B 执行 lseek 函数,也将其对该文件的当前位置偏移量设置为 1500 个字节处,写了100个字节数据,那么此时在进程 B 中,该文件的当前位置偏移量已经移动到了 1600 字节处。B 进程时间片耗尽,内核又切换到了进程 A,使进程 A 恢复运行,当进程 A 调用 write 函数时,是从进程 A 的该文件当前位置偏移量(1500 字节处)开始写入,这样导致的后果就是a进程写的数据将覆盖原来b进程写入的数据。
解决办法——原子操作:
上述的问题出在逻辑操作“先定位到文件末尾,然后再写”,它使用了两个分开的函数调用,首先使用 lseek 函数将文件当前位置偏移量移动到文件末尾、然后在使用 write函数将数据写入到文件。
那么解决办法就是将这两个操作步骤合并成一个原子操作,所谓原子操作,是有多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有步骤,不可能只执行所有步骤中的一个子集。
pread()和 pwrite()都是系统调用,与 read()、write()函数的作用一样,用于读取和写入数据。区别在于,pread()和 pwrite()可用于实现原子操作,调用 pread 函数或 pwrite 函数可传入一个位置偏移量 offset 参数,用于指定文件当前读或写的位置偏移量,所以调用 pread 相当于调用 lseek 后再调用 read;
fcntl()和 ioctl():
fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作,譬如复制一个文件描述符(与 dup、dup2 作用相同)、获取/设置文件描述符标志、获取/设置文件状态标志等.
int fcntl(int fd, int cmd, ... /* arg */ ) 其中cmd为: ⚫ 复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC); ⚫ 获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD); ⚫ 获取/设置文件状态标志(cmd=F_GETFL 或 cmd=F_SETFL); ⚫ 获取/设置异步 IO 所有权(cmd=F_GETOWN 或 cmd=F_SETOWN); ⚫ 获取/设置记录锁(cmd=F_GETLK 或 cmd=F_SETLK);
eg:fd2 = fcntl(fd1, F_DUPFD, 0);
ioctl: ioctl()可以认为是一个文件 IO 操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或硬件外设,此函数将会在进阶篇中使用到,譬如可以通过 ioctl 获取 LCD 相关信息等。
标准io库:
系统调用(sys.h)与标准 C 语言函数库(std…eg:stdio.h),所谓标准 I/O 库则是标准 C 库中用于文件 I/O 操作(譬如读文件、写文件等)相关的一系列库函数的集合,通常标准 I/O 库函数相关的函数定义都在头文件<stdio.h>中。
标准 I/O 库函数是构建于文件 I/O(open()、read()、write()、lseek()、close()等)这些系统调用之上的,其中标准 I/O 库函数 fopen()就利用系统调用 open()来执行打开文件的操作、fread()利用系统调用 read()来执行读文件操作、fwrite()则利用系统调用 write()来执行写文件操作等等。
标准 I/O 和文件 I/O(标准c语言函数库和系统调用)区别:
1.都是c语言,但是前者是标准c语言函数库,而后者是linux系统调用
2.前者是通过调用后者实现的
3.标准xxx比系统调用更适合移植,不同操作系统的标准库的接口基本一致,所以前者有更好的移植性。
4.标准 I/O 库在用户空间维护了自己的 stdio 缓冲区,所以标准 I/O 是带有缓存的,而文件 I/O 在用户空间是不带有缓存的,所以在性能、效率上,标准 I/O 要优于文件 I/O。
其中使用上的区别,系统调用(open,write,read,close,lseek等使用的是fd文件标识符,是运行在linux内核的cmd中参考开发板),而标准库(fopen,fwrite,fread,fclose,fseek等是可以直接运行在windows中,例如海康sdk在vs上的写法)这里用标准库的 FILE 类型对象的指针(FILE ),使用该 FILE 指针与被打开或创建的文件相关联,然后该 FILE 指针就用于后续的标准 I/O 操作(fd==file
FILE 是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,包括用于实际I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等(有无缓冲区就是fd和file之间最大的区别)。FILE数据结构定义在标准 I/O 库函数头文件 stdio.h 中。
同时标准库是要优于系统调用的,其原因在于标准 I/O 实现维护了自己的缓冲区。如一般对文件进行多次写操作(循环写入1mb数据),直接调用write写入均是对磁盘进行操作,多次操作累计时间长,而用了标准缓冲区以后,不会写一次就操作磁盘,而是累计数据到缓冲区,最后一次性写入磁盘,前者直接操作io耗时长,后者只是将用户空间下的数据拷贝到了
文件 I/O 内核缓冲区
中,并没直接操作硬件,所以消耗的时间短,硬件操作占用的时间远比内存复制占用的时间大得多。(这里的缓冲区是文件内核缓冲区,标准库的缓冲区是用户空间的缓冲区)
ps:linux下输入time ./xxx就能计算运行xxx的时间。
文件 I/O 内核缓冲,这是由内核维护的缓冲区,而标准 I/O 所维护的 stdio 缓冲是用户空间的缓冲区,当应用程序中通过标准 I/O 操作磁盘文件时,为了减少调用系统调用的次数,标准 I/O 函数会将用户写入或读取文件的数据缓存在 stdio 缓冲区,然后再一次性将 stdio 缓冲区中缓存的数据通过调用系统调用 I/O(文件 I/O)写入到文件 I/O 内核缓冲区或者拷贝到应用程序的 buf 中。
函数:setbuf()、setbuffer()以及 setvbuf()。调用 setvbuf()库函数可以对文件的 stdio 缓冲区进行设置,譬如缓冲区的缓冲模式、缓冲区的大小、起始地址等。

int setvbuf(FILE *stream, char *buf, int mode, size_t size);
file即FILE 指针,buf:如果参数 buf 不为 NULL,那么 buf 指向 size 大小的内存区域将作为该文件的 stdio 缓冲区,mode:参数 mode 用于指定缓冲区的缓冲类型,_IONBF:不对 I/O 进行缓冲(无缓冲)。意味着每个标准 I/O 函数将立即调用 write()或者 read()。_IOLBF:采用行缓冲 I/O。在这种情况下,当在输入或输出中遇到换行符"\n"时,标准 I/O 才会执 _IOFBF:采用全缓冲 I/O。在这种情况下,在填满 stdio 缓冲区后才进行文件 I/O 操作(read、write)。行文件 I/O 操作。
 printf("Hello World!\n");
 printf("Hello World!");
 因为默认行缓冲模式,printf()输出的字符串写入到了标准输出的 stdio 缓冲区中,第一个 printf()包含了换行符,所以已经刷入了内核缓冲区,而第二个 printf 并没有包含换行符,所以第二个 printf 输出的"Hello World!"还缓存在 stdio 缓冲区中,需要等待一个换行符才可输出到终端。(这里的stio缓冲区即用户缓冲区,只有接收到换行符才能输入到内核缓冲区显示到命令界面)

刷新缓冲的三个方法(刷新函数fflush,关闭文件fclose(stdout); //关闭标准输出,结束程序:即main运行结束)
1.同时还有刷新缓冲的函数,即将stdio缓冲区刷新到内核缓冲区,在任何时候都可以使用库函数 fflush()来强制刷新(将输出到 stdio 缓冲区中的数据写入到内核缓冲区,通过 write()函数)stdio 缓冲区。

int fflush(FILE *stream);

2.同时在文件关闭时系统会自动刷新该文件的 stdio 缓冲区。
如在上面的函数后面加入fclose(stdout); //关闭标准输出,最后也会刷新两个hello world!
3.当程序退出时,确实会自动刷新 stdio 缓冲区
在这里插入代码片int main(void) { printf("Hello World!\n"); printf("Hello World!"); }

在这里插入图片描述 总结: 首先应用程序调用标准 I/O 库函数将用户数据写入到 stdio 缓冲区中,stdio 缓冲区是 由 stdio 库所维护的用户空间缓冲区。针对不同的缓冲模式,当满足条件时,stdio 库会调用文件 I/O(系统调用 I/O)将 stdio 缓冲区中缓存的数据写入到内核缓冲区中,内核缓冲区位于内核空间。最终由内核向磁盘设备发起读写操作,将内核缓冲区中的数据写入到磁盘(或者从磁盘设备读取数据到内核缓冲区)。 应用程序调用库函数可以对 stdio 缓冲区进行相应的设置,设置缓冲区缓冲模式、缓冲区大小以及由调用者指定一块空间作为 stdio 缓冲区,并且可以强制调用 fflush()函数刷新缓冲区;而对于内核缓冲区来说,应用程序可以调用相关系统调用对内核缓冲区进行控制,譬如调用 fsync()、fdatasync()或 sync()来刷新内核缓冲区(或通过 open 指定 O_SYNC 或 O_DSYNC 标志,类似于fflush函数),或者使用直接 I/O 绕过内核缓冲区(open 函数指定 O_DIRECT 标志)

 **文件描述符fd(内核内存中)与 FILE 指针(用户内存中)互转:**
 在应用程序中,在同一个文件上执行 I/O 操作时,还可以将文件 I/O(系统调用 I/O)与标准 I/O 混合使用,这个时候我们就需要将文件描述符和 FILE 指针对象之间进行转换,此时可以借助于库函数 fdopen()、fileno()来完成。
 库函数 fileno()可以将标准 I/O 中使用的 FILE 指针转换为文件 I/O 中所使用的文件描述符,而 fdopen()则进行着相反的操作。
	****标准输入输出和错误****
	stdin,stdout,stderr。
	fopen,fwrite,fread..
	库函数 fseek()的作用类似所学习的系统调用 lseek(),用于设置文件读写位置偏移量,lseek()用于文件 I/O,而库函数 fseek()则用于标准 I/O。
	区别一下前面的原子操作和标准库和系统调用,原子操作一般加p,eg:pread()和 pwrite(),而标准库操作加f,eg:fopen,fwrite等。
	库函数 printf()用于输出程序中的打印信息,printf()函数可将格式化数据写入到标准输出,所以通常称为格式化输出,同样的有格式化输入,就是将数据写入的标准输入中。,格式化输出还包括:fprintf()、dprintf()、sprintf()、snprintf()这 4 个库函数。从标准输入中获取格式化数据,格式化输入包括:scanf()、fscanf()、sscanf()这三个库函数。
	printf()函数用于将格式化数据写入到标准输出;dprintf()和 fprintf()函数用于将格式化数据写入到指定的文件中,两者不同之处在于,fprintf()使用 FILE 指针指定对应的文件、而 dprintf()则使用文件描述符 fd 指定对应的文件;sprintf()、snprintf()函数可将格式化的数据存储在用户指定的缓冲区 buf 中。
	scanf()函数可将用户输入(标准输入)的数据进行格式化转换;fscanf()函数从 FILE 指针指定文件中读取数据,并将数据进行格式化转换;sscanf()函数从参数 str 所指向的字符串中读取数据,并将数据进行格式化转换。

Linux下的文件类:

主要分为七类,使用stat命令能够查看文件类型,
⚫’ - ':普通文件
⚫ ’ d ':目录文件
⚫ ’ c ':字符设备文件
⚫ ’ b ':块设备文件
⚫ ’ l ':符号链接文件
⚫ ’ s ':套接字文件(socket)实际上就是网络通信
⚫ ’ p ':管道文件,用于进程间通信
普通文件可以分为两大类:文本文件(.c、.h、.sh、.txt)和二进制文(Linux 系统下的可执行文件、C 代码编译之后得到的.o 文件、.bin 文件等都是二进制文件)。普通文件中的数据存在系统磁盘中,可以访问文件中的内容,文件中的内容以字节为单位进行存储于访问。
Linux 系统中,可将硬件设备分为字符设备和块设备,所以就有了字符设备文件和块设备文件两种文件类型。虽然有设备文件,但是设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护,当系统关机时,设备文件都会消失;字符设备文件一般存放在 Linux 系统/dev/目录下,所以/dev 也称为虚拟文件系统 devfs。
符号链接文件(link)类似于 Windows 系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另一个文件路径,当对符号链接文件进行操作时,系统根据情况会对这个操作转移到它指向的文件上去,而不是对它本身进行操作(类似于windows下的快捷方式)
**总结:**普通文件是最常见的文件类型;目录也是一种文件类型;设备文件对应于硬件设备;符号链接文件类似于 Windows 的快捷方式;管道文件用于进程间通信;套接字文件用于网络通信。
文件权限:(777超级权限)在这里插入图片描述

O 对应的 3 个 bit 位用于描述其它用户的权限;
G 对应的 3 个 bit 位用于描述同组用户的权限;
U 对应的 3 个 bit 位用于描述文件所有者的权限;
S 对应的 3 个 bit 位用于描述文件的特殊权限。
S_IRWXU 00700 owner has read, write, and execute permission
S_IRUSR 00400 owner has read permission
S_IWUSR 00200 owner has write permission
S_IXUSR 00100 owner has execute permission//执行权限

S_IRWXG 00070 group has read, write, and execute permission
S_IRGRP 00040 group has read permission
S_IWGRP 00020 group has write permission
S_IXGRP 00010 group has execute permission

S_IRWXO 00007 others (not in group) have read, write, and execute permission
S_IROTH 00004 others have read permission
S_IWOTH 00002 others have write permission
S_IXOTH 00001 others have execute permission
eg:777代表超级权限
而首位的文件类型判断如下S_IFSOCK 0140000 socket(套接字文件)
S_IFLNK 0120000 symbolic link(链接文件)
S_IFREG 0100000 regular file(普通文件)
S_IFBLK 0060000 block device(块设备文件)
S_IFDIR 0040000 directory(目录)
S_IFCHR 0020000 character device(字符设备文件)
S_IFIFO 0010000 FIFO(管道文件)

记忆,7421,超级读写执行权限,在 Linux 系统下,可以使用 chmod 命令修改文件权限,同时还有通过掩码加密权限,umask命令用于查看/设置权限掩码,权限掩码主要用于对新建文件的权限进行屏蔽。当新建文件时,文件实际的权限并不等于我们所设置的权限,譬如:调用 open 函数新建文件时,文件实际的权限并不等于 mode 参数所描述的权限,而是通过如下关系得到实际权限:

mode & ~umask
譬如调用 open 函数新建文件时,mode 参数指定为 0777,假设 umask 为 0002,那么实际权限为:
0777 & (~0002) = 0775

umask 是进程自身的一种属性、A 进程的 umask 与 B 进程的 umask 无关(父子进程关系除外)
应用:

ret = stat("./test_file", &file_stat);//获取文件的 inode 节点编号以及文件大小

stat即查看文件属性,最后file_stat结构体里面包含多个类型,如file_stat.st_size,
file_stat.st_ino大小和编号等,同时可以(file_stat.st_mode & S_IFMT)来判断具有哪些权限。

 switch (file_stat.st_mode & S_IFMT) {
 case S_IFSOCK: printf("socket"); break;
 case S_IFLNK: printf("symbolic link"); break;
 case S_IFREG: printf("regular file"); break;
 case S_IFBLK: printf("block device"); break;
 case S_IFDIR: printf("directory"); break;
 case S_IFCHR: printf("character device"); break;
 case S_IFIFO: printf("FIFO"); break;
 }

在这里插入图片描述同时类似的函数还有fstat 和 lstat,操作类似。
文件属主,即文件属于哪一个用户,或者用户组(用户 ID 简称 UID、用户组 ID 简称 GID),chown 是一个系统调用,该系统调用可用于改变文件的所有者(用户 ID)和所属组(组 ID),同时也有fchown()、lchown()。
符号和链接
在 Linux 系统中有两种链接文件,分为软链接(也叫符号链接)文件和硬链接文件,软链接文件也就是前面给大家的 Linux 系统下的七种文件类型之一,其作用类似于 Windows 下的快捷方式。从使用角度来讲,两者没有任何区别,都与正常的文件访问方式一样,支持读、写以及执行。
创建命令:

硬链接:ln 源文件 链接文件
软链接:ln -s 源文件 链接文件

创建的硬链接和源文件inode是完全相同的,指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已,创建出来的硬链接文件与源文件对文件系统来说是完全平等的关系。但是输出硬链接不会影响到源文件,因为 inode 数据结结构中会记录文件的链接数,这个链接数指的就是硬链接数,当为文件每创建一个硬链接,inode 节点上的链接数就会加一,每删除一个硬链接,inode 节点上的链接数就会减一,直到为 0,inode 节点和对应的数据块才会被文件系统所回收,源文件也相当于一个硬链接,故起始硬链接数为1.
软链接文件与源文件有着不同的 inode 号,它们之间有着不同的数据块,但是软链接文件的数据块中存储的是源文件的路径名。
上面那个是命令,而在linux系统下可以使用系统调用link(), symlink()函数创建硬链接文件(link)和软链接文件(symlink)。

int link(const char *oldpath, const char *newpath);
int symlink(const char *target, const char *linkpath);

创建了软链接(硬链接可以直接读取,相当于直接连接起来了,而不是像软连接存储了路径信息),如果要读取文件的话,不能用open打开链接,因为没有fd,打开的并不是链接文件本身、而是其指向的文件,只能使用系统调用 readlink。
目录文件
linux系统下创建目录 mkdir()以及删除目录 rmdir函数,同时只能使用 opendir()、readdir()和 closedir()来打开、读取以及关闭目录。文件重命名rename()系统调用。
ps:删除普通文件:系统调用 unlink()或使用 C 库函数 remove(),其中unlink()的作用与 link()相反,unlink()系统调用用于移除/删除一个硬链接,unlink()系统调用并不会对软链接进行解引用操作,若 pathname 指定的文件为软链接文件,则删除软链接文件本身,而非软链接所指定的文件。而硬链接只是使其inode 链接计数减 1,如果该文件还有其它硬链接,则任可通过其它链接访问该
文件的数据。
putchar()、puts()、fputc()、fputs()这些函数只能输出字符串,不能进行格式转换。而printf()可以按照自己规定的格式输出字符串信息,一般称为格式化输出。
常用的字符串输入函数有 gets()、getchar()、fgetc()、fgets(),用法与下列类似,而gets与scanf类似,区别1就是gets允许输入带有空格等,仅以回车换行符作为字符串的分割符。而scanf将空格当为字符串分割符。区别2,gets()会将回车换行符从输入缓冲区中取出来,然后将其丢弃。scanf()读走缓冲区中的字符串数据时,并不会将分隔符(空格、TAB 制表符、回车换行符等)读走将其丢弃,缓冲区中依然还存在用户输入的分隔符。
putchar()函数可以把参数 c 指定的字符(一个无符号字符)输出到标准输出设备,fputc()与 putchar()类似,fputc()可把字符输出到指定的文件中,既可以是标准输出、标准错误
设备,也可以是一个普通文件。
fputs()与 puts()类似,也用于输出一条字符串,与 puts()区别在于,puts()只能输出到标准输出设备,而 fputs()可把字符串输出到指定的文件中,既可以是标准输出、标准错误设备,也可以是一个普通文件。
字符串长度函数strlen 和 sizeof区别:
⚫ sizeof 是 C 语言内置的操作符关键字,而 strlen 是 C 语言库函数;
⚫ sizeof 仅用于计算数据类型的大小或者变量的大小,而 strlen 只能以结尾为’ \0 '的字符串作为参数;
⚫ 编译器在编译时就计算出了 sizeof 的结果,而 strlen 必须在运行时才能计算出来;
⚫ sizeof 计算数据类型或变量会占用内存的大小,strlen 计算字符串实际长度。

 char str[50] = "Linux app strlen test!";
 char *ptr = str;
 printf("sizeof: %ld\n", sizeof(str));//50是str的数组长度,即占内存大小
 printf("strlen: %ld\n", strlen(str));//22始终是str长度
 printf("sizeof: %ld\n", sizeof(ptr));//8计算的是指针变量 ptr 的大小,这里等于 8 个字节,64位系统
 printf("strlen: %ld\n", strlen(ptr));//22

字符串拼接:strcat()函数或 strncat()函数(前者直接拼接,后者需要指定拼接字符串长度)
字符串比较:strcmp()和 strncmp()类似上列,后者可以只比较指定长度字符串。
字符串拷贝:strcpy()函数和 strncpy()函数,strncpy()可以指定从源字符串 src 复制到目标字符串 dest 的字符数量,效果同上当大于原字符串大小时直接将\n拷贝过去即可。
同时也可以使用内存拷贝,即memcpy()、memmove()以及 bcopy()这些库函数实现拷贝操作,字符串拷贝本质上也只是内存数据的拷贝。
字符串查找:strchr(从左到右查找)、strrchr(从右到左查找)、strstr()、strpbrk()、index()以及 rindex(),从字符串中查找字符或者字符串。
内存填充:
memset()函数用于将某一块内存的数据全部设置为指定的值,bzero()函数用于将一段内存空间中的数据全部设置为 0。
字符串转整数:
将一个字符串转为整形数据,主要包括 atoi()、atol()、atoll()以及strtol()、strtoll()、strtoul()、strtoull()等,atoi()、atol()、atoll()三个函数可用于将字符串分别转换为 int、long int 以及 long long 类型的数据,需要包含头文件<stdlib.h>。
而strtol()、strtoll()两个函数可分别将字符串转为 long int 类型数据和 long long ing 类型数据,与上面不同的是strtol()、strtoll()可以实现将多种不同进制数,而strtoul()、strtoull()返回的是无符号值。
字符串转浮点数
atof()、strtod()、strtof()、strtold(),atof()用于将字符串转换为一个 double 类型的浮点数据,strtof()、strtod()以及 strtold()三个库函数可分别将字符串转换为 float 类型数据、double 类型数据、longdouble 类型数据。
应用程序传递参数方法:

int main(int argc, char **argv)
int main(int argc, char *argv[])
传递进来的参数以字符串的形式存在,字符串的起始地址存储在 argv 数组中,参数 argc 表示传递进来
的参数个数,包括应用程序自身路径名

**

正则表达式:

**
系统信息和系统资源:
系统调用 uname()用于获取有关当前操作系统内核的名称和信息,sysinfo 系统调用可用于获取一些系统统计信息。
通过 rand()和 srand()产生随机数,休眠sleep()、usleep()以及 nanosleep(),一般做延迟使用,sleep是秒级延迟,usleep是us级延迟。

内存操作:
在linux中内存是由系统管理分配的,app想要内存需要向系统申请,不用的时候需要释放内存。
其中申请内存使用函数malloc(),释放内存free()
其他内存分配函数:
calloc()用来动态地分配内存空间并初始化为 0
内存对齐:eg:int占4byte,char占一个byte,那么将它们放到一个结构体中应该占4+1=5byte;但是实际上,通过运行程序得到的结果是8 byte,这就是内存对齐所导致的。
proc文件系统(process)
proc 文件系统是一个虚拟文件系统,它以文件系统的方式为应用层访问系统内核数据提供了接口,用户和应用程序可以通过 proc 文件系统得到系统信息和进程相关信息,对 proc 文件系统的读写作为与内核进行通信的一种手段。
总结就是内核与用户之间的接口,或者叫中间商,因为不存在与磁盘中,而是存在内存中是动态创建的所以叫做虚拟文件系统。
可以看到内核中proc中由许多数字命名的文件夹,这些数字对应的其实就是一个一个的进程 PID 号,每一个进程在内核中都会存在一个编号,通过此编号来区分不同的进程。其他的文件夹如iomem:IO 设备的内存使用情况;modules:加载的模块列表;mounts:挂载的文件系统列表;version:内核版本信息;uptime:系统运行时间;cpuinfo:CPU 相关信息;
信号(中断)
信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。

sig_t signal(int signum, sig_t handler);
signum:此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。
handler:sig_t 类型的函数指针,指向信号对应的信号处理函数
eg:ret = signal(SIGINT, (sig_t)sig_handler);//信号的调用,类比于中断,当程序内核接收到ctr+c(SIGINT类型)时,自动跳转到sig_handler(类似中断函数)中执行。
//SIGINT
当用户在终端按下中断字符(通常是 CTRL + C)时,该信号的系统默认操作是终止进程的运行,这种信号linux有64个,采用的有31个,linux下使用kill-l可以查看所有信号,不可靠信号问题主要指的是信号可能丢失,编号 1~31 所对应的是不可靠信号,编号 34~64 对应的是可靠信号。同时还分实时信号和非实时信号,非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

在这里插入图片描述
信号函数除了signal还有sigaction()int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);其中signum:需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。act:act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式。oldact:oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构。如果参数oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来;
这里面的act都是sigaction结构体,其中包含sa_handler/sa_sigaction信号处理函数(中断函数),sa_mask信号掩码(类似于中断嵌套,当运行当前中断时,不被其他中断(信号)打断),sa_flags信号标志用于控制信号的处理过程。

static void sig_handler(int sig){}
 struct sigaction sig = {0};
 int ret;
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 ret = sigaction(SIGINT, &sig, NULL);

信号的发送:
系统调用函数:kill()和killp(),raise()用于发送信号。

int kill(pid_t pid, int sig);
pid是进程的id类似于uid(user id,这个是process id)
int raise(int sig);//向自身进程发送信号

在一个程序中,上面的signal和sigaction将信号和中断绑定了,后续使用信号发送的时候,触发中断任然运行中断函数内的内容。

static void sig_handler(int sig) {
 printf("Received signal: %d\n", sig);
}
 main(){
 struct sigaction sig = {0};
 int ret;
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 ret = sigaction(SIGINT, &sig, NULL);
 if (-1 == ret) {
 perror("sigaction error");
 exit(-1);
 }
 for ( ; ; ) {
 /* 向自身发送 SIGINT 信号 */
 if (0 != raise(SIGINT)) {
 printf("raise error\n");
 exit(-1);
 }
 sleep(3); // 每隔 3 秒发送一次
 }
 }//结果就是每隔3秒打印一个Received signal 2(2是SIGINT的编号)

系统调用alarm()和 pause(),定时和暂停,前者到了设定的时间将会向内核会向进程发送 SIGALRM信号unsigned int alarm(unsigned int seconds)

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig) {
 puts("Alarm timeout");
 exit(0);
}
int main(int argc, char *argv[])
{
 struct sigaction sig = {0};
 int second;
 /* 检验传参个数 */
 if (2 > argc)
 exit(-1);
 /* 为 SIGALRM 信号绑定处理函数 */
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 if (-1 == sigaction(SIGALRM, &sig, NULL)) {
 perror("sigaction error");
 exit(-1);
 }
 /* 启动 alarm 定时器 */
 second = atoi(argv[1]);
 printf("定时时长: %d 秒\n", second);
 alarm(second);
 /* 循环 */
 for ( ; ; )
 sleep(1);
 exit(0);
//步骤先定义sig结构体,然后绑定信号和中断函数,最后调用信号机制,如信号发送或者定时暂停等,这里使用的是定时,到点运行信号,然后信号与中断绑定,再运行中断处理函数。
///* 进入休眠状态 */
// pause();
 //puts("休眠结束");//这里加入休眠函数,只有定时信号触发,才会结束休眠运行下面的代码,效果同上,只是最后定时结束会打印休眠结束。

pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause()才返回,在这种情况下,pause()返回-1,并且将 errno 设置为 EINTR。int pause(void);
信号集
有一个能表示多个信号(一组信号)的数据类型—信号集(signalset),sigemptyset()和sigfillset()用于初始化信号集。sigemptyset()初始化信号集,使其不包含任何信号;而sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号),sig empty set (信号空集合),sig fill set(信号满集合)。然后再分别使用 sigaddset()和 sigdelset()函数向信号集中添加或移除一个信号int sigaddset(sigset_t *set, int signum);int sigdelset(sigset_t *set, int signum);。sigismember()函数可以测试某一个信号是否在指定的信号集中int sigismember(const sigset_t *set, int signum);
信号掩码:
内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号,当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理(相当于黑名单,上了黑名单的不能处理)。
添加进入信号掩码**int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);**

/* 定义信号集 */
sigset_t sig_set;
/* 将信号集初始化为空 */
sigemptyset(&sig_set);
/* 向信号集中添加 SIGINT 信号 */
sigaddset(&sig_set, SIGINT);
/* 向进程的信号掩码中添加信号 */
ret = sigprocmask(SIG_BLOCK, &sig_set, NULL);//将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中,相对将SIGINT拉进黑名单(这个信号已经加入了sig—set)
/* 从信号掩码中移除信号 */
ret = sigprocmask(SIG_UNBLOCK, &sig_set, NULL);//解除黑名单
	阻塞等待信号 sigsuspend():添加信号掩码可以阻塞信号,该函数可以接触阻塞信号`nt sigsuspend(const sigset_t *mask);`sigsuspend()函数会将参数 mask 所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置为参数 mask 所指向的信号集。
	**实时信号**
	如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中,为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()函数获取。`int sigpending(sigset_t *set);`会将处于等待状态的信号会存放在参数 set 所指向的信号集中,使用方法如下.
/* 获取当前处于等待状态的信号 */
sigpending(&sig_set);//将等待的信号存入sig——set信号集中
/* 判断 SIGINT 信号是否处于等待状态 */
if (1 == sigismember(&sig_set, SIGINT))//判断是否在信号集中
puts("SIGINT 信号处于等待状态");

发送实时信号
使用 sigqueue()函数发送实时信号,int sigqueue(pid_t pid, int sig, const union sigval value);参数 value 指定了信号的伴随数据,union sigval 数据类型,可以是int也可以是指针,相当于顺带传一个参数进去。

进程:

main的调用,编译链接时链接器将代码链接到应用程序中,最终构成可执行文件,当执行应用程序时,在 Linux 下输入可执行文件的相对路径或绝对路径就可以运行该程序,譬如./a/home/dt/app,还可根据应用程序是否接受传参在执行命令时在后面添加传入的参数信息,譬如./app arg1 arg2 或/home/dt/app arg1 arg2。程序运行需要通过操作系统的加载器来实现,加载器是操作系统中的程序,当执行程序时,加载器负责将此应用程序加载内存中去执行。
进程就是一个可执行文件运行的过程,是一个动态过程而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。
进程号:Linux 系统下的每一个进程都有一个进程号(processID,简称 PID),进程号是一个正数,用于唯一标识系统中的某一个进程,linux下ps命令可以查看进程号,而getpid()来获取本进程的进程号。
进程的环境变量(指定系统运行的一些参数,例如运行路径,直接全局搜索太长,会优先搜索环境变量中的路径简化搜索,或者相当于一个系统中的全局变量,运行函数直接使用即可):每一个进程都有一组与其相关的环境变量,这些环境变量以字符串形式存储在一个字符串数组列表中,把这个数组称为环境列表。其中每个字符串都是以名称=值/name=value形式定义,所以环境变量是“名称-值”的成对集合。
env 命令查看到 shell 进程的所有环境变量,export 命令还可以添加一个新的环境变量或删除一个环境变量,获取某个指定的环境变量getenv(),这些都是系统调用,在命令终端可以使用。
C 语言函数库中提供了用于修改、添加、删除环境变量的函数,putenv(添加环境变量)、setenv(添加或者修改)、unsetenv(移除)、clearenv(移除全部环境变量)函数等。
环境变量的作用,环境变量常见的用途之一是在 shell 中,每一个环境变量都有它所表示的含义,譬如 HOME 环境变量表示用户的家目录,USER 环境变量表示当前用户名,SHELL 环境变量表示 shell 解析器名称,PWD 环境变量表示当前所在目录等,在我们自己的应用程序当中,也可以使用进程的环境变量。
C语言程序组成:
正文段:也可称为代码段,文本段具有只读属性,以防止程序由于意外而修改其指令;正文段是可以共享的,即使在多个进程间也可同时运行同一段程序。
初始化数据段:通常将此段称为数据段,包含了显式初始化的全局变量和静态变量,当程序加载到内存中时,从可执行文件中读取这些变量的值。
未初始化数据段:包含了未进行显式初始化的全局变量和静态变量,通常将此段称为 bss 段,
栈:函数内的局部变量以及每次函数调用时所需保存的信息都放在此段中,每次调用函数时,函数传递的实参以及函数返回值等也都存放在栈中。栈是一个动态增长和收缩的段,由栈帧组成,系统会为每个当前调用的函数分配一个栈帧,栈帧中存储了函数的局部变量(所谓自动变量)、实参和返回值fifo,先进先出。
堆:可在运行时动态进行内存分配的一块区域,譬如使用 malloc()分配的内存空间,就是从系统堆内存中申请分配的。在这里插入图片描述

进程的虚拟地址空间:

每一个进程都在自己独立的地址空间中运行,在 32 位系统中,每个进程的逻辑地址空间均为 4GB,这 4GB 的内存空间按照 3:1 的比例进行分配,其中用户进程享有 3G 的空间,而内核独自享有剩下的 1G 空间。
![在这里插入图片描述](https://img-blog.csdnimg.cn/8eb1d5d1931d487faaa60437806a56a1.png)
	虚拟地址会通过硬件 MMU(内存管理单元)映射到实际的物理地址空间中,建立虚拟地址到物理地址的映射关系后,对虚拟地址的读写操作实际上就是对物理地址的读写操作,MMU 会将物理地址“翻译”为对应的物理地址。
	虚拟地址的作用:因为物理内存(一般存放正在运行的代码,类似于手机的运存)的大小有限,一个系统可能同时运行多个线程,如果内存空间不足,需要将程序暂时拷贝到磁盘中,再将新的程序装入内存,进进出出内存使用效率底下。同时如果全部存入物理内存中,因为加载内存的地址是由系统随机分配的(这个地址是随机的,不确定的),而链接器的地址必须跟程序地址一致,否则无法链接,所以需要一个虚拟地址(这个是固定的,确定的)。
	好处:1.进程与进程、进程与内核相互隔离。一个进程不能读取或修改另一个进程或内核的内存数据,这是因为每一个进程的虚拟地址空间映射到了不同的物理地址空间。提高了系统的安全性与稳定性。
	2.在某些应用场合下,两个或者更多进程能够共享内存。因为每个进程都有自己的映射表,可以让不同进程的虚拟地址空间映射到相同的物理地址空间中内存!

在这里插入图片描述

映射详解:

虚拟地址和物理地址的映射是逻辑上的,实际中不存在,磁盘没有拷贝代码到内存中只是逻辑上被放入了内存,具体到代码,就是建立并初始化了相关的数据结构(struct address_space),这个过程有系统调用mmap()实现,所以建立内存映射的效率很高。既然没有拷贝到内存,那是如何通过内存访问磁盘文件?mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,这样以后,进程无需再调用read或write对文件进行读写,而只需要通过ptr就能够操作文件。但是ptr所指向的是一个逻辑地址,要操作其中的数据,必须通过MMU将逻辑地址转换成物理地址,如图1中过程2所示。
建立内存映射并没有实际拷贝数据,这时,MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会在swap中寻找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中,如图1中过程3所示。
如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘上,如图1中过程4所示。
总结:内存映射,相当于提前声明,我可能需要被用到(将被用到的程序在内存中建立相关数据结构,类似于占座,这就没拷贝程序进内存),当第一次被使用到时,发现没有数据,执行中断,如果没有在swap页中找到(意味着没有存入内存中,内存中可能已经存入了一些数据,相当于查找目录,发现没有在里面,后续就是添加进目录),就会利用之前mmp已经建立的内存映射,拷贝数据到内存中。
效率问题:原因是read()是系统调用,其中进行了数据拷贝,它首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,如图2中过程1,然后再将这些数据拷贝到用户空间,如图2中过程2,在这个过程中,实际上完成了 两次数据拷贝 ;而mmap()也是系统调用,如前所述,mmap()中没有进行数据拷贝,真正的数据拷贝是在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行了 一次数据拷贝 。因此,内存映射的效率要比read/write效率高。(就是正常read()需要先将数据从磁盘拷贝物理内存的内核缓冲段,然后再拷贝到物理内存的用户空间中,有两次拷贝。而通过内存映射直接将文件从硬盘拷贝到用户空间,只进行了 一次数据拷贝 。)
在这里插入图片描述
进程函数:
fork(英文复刻)函数创建一个新的进程,调用 fork()函数的进程称为父进程(parent process),由 fork()函数创建出来的进程被称为子进程(child process)。fork()调用成功后,将会在父进程中返回子进程的 PID,而在子进程中返回值是 0;子进程和父进程会继续执行 fork()调用之后的指令,子进程、父进程各自在自己的进程空间中运行。
事实上,子进程是父进程的一个副本,譬如子进程拷贝了父进程的数据段、堆、栈以及继承
了父进程打开的文件描述符,父进程与子进程并不共享这些存储空间,这是子进程对父进程相应部分存储空间的完全复制,执行 fork()之后,每个进程均可修改各自的栈数据以及堆段中的变量,而并不影响另一个进程。
因为调用 fork()之后,子进程成为了一个独立的进程,可被系统调度运行,而父进程也继续被系统调度运行无法确定父、子两个进程谁将率先访问 CPU,也就是说无法确认谁先被系统调用运行(在多核处理器中,它们可能会同时各自访问一个 CPU)。解决方法采用信号同步技术,如果要让子进程先运行,则可使父进程被阻塞,等到子进程来唤醒它。

int main(void) {
 struct sigaction sig = {0};
 sigset_t wait_mask;
 /* 初始化信号集 */
 sigemptyset(&wait_mask);
 /* 设置信号处理方式 */
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 if (-1 == sigaction(SIGUSR1, &sig, NULL)) {
 perror("sigaction error");
 exit(-1);
 }
 switch (fork()) {//调用成功子进程返回0,父进程返回pid
 case -1:
 perror("fork error");
 exit(-1);
 case 0:
 /* 子进程 */
 printf("子进程开始执行\n");
 printf("子进程打印信息\n");
 printf("~~~~~~~~~~~~~~~\n");
 sleep(2);
 kill(getppid(), SIGUSR1); //发送信号给父进程、唤醒它,getppid得到父进程的pid
 _exit(0);
 default:
 /* 父进程 */
 if (-1 != sigsuspend(&wait_mask))//挂起、阻塞
 exit(-1);
 printf("父进程开始执行\n");
 printf("父进程打印信息\n");
 exit(0);//在父进程
分支中,直接调用了 sigsuspend()使父进程进入挂起状态,由子进程通过 kill 命令发送信号唤醒
 } }

所有进程的父进程是init进程,pid是1,是 Linux 系统启动之后运行的第一个进程,它管理着系统上所有其它进程,init 进程是由内核启动,因此理论上说它没有父进程。
终止进程:
进程终止处理函数:需要提前注册,在进程终止之前进行一些操作,如注销等。在结束进程的时候,一般使用 exit()库函数而非_exit()系统调用,原因在于 exit()最终也会通过_exit()终止进程,但在此之前,它将会完成一些其它的工作,如调用终止处理函数,刷新 stdio 流缓冲区等。所以,由此可知,exit()函数会比_exit()会多做一些事情,包括执行终止处理函数、刷新 stdio 流缓冲以及调用_exit(),在前面曾提到过,在我们的程序当中,父、子进程不应都使用 exit()终止,只能有一个进程使用 exit()、而另一个则使用_exit()退出,当然一般推荐的是子进程使用_exit()退出、而父进程则使用 exit()退出。
总结:exit()函数包含_exit()函数,会先运行进程最终处理函数,在刷新stdio缓冲区,最后再调用_exit结束进程,推荐父进程使用exit。
监督子进程
系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息。
但是如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;同时子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。
这里我们就需要使用waitpid()函数,pid_t waitpid(pid_t pid, int *status, int options);参数pid是核心 如果 pid 大于 0,表示等待进程号为 pid 的子进程;如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程;如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。status 参数用于存储监督进程相关信息。
僵尸进程和孤儿进程
孤儿进程:父进程先于子进程结束。在linux中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程,换言之,某一子进程的父进程结束后,该子进程调用 getppid()将返回 1,init 进程变成了孤儿进程的“养父”。同时在图形化界面下,有一个守护进程,负责收养孤儿使其不变为孤儿进程,但是在字符界面下没有这个守护进程,其pid还是1.
僵尸进程:进程结束之后,通常需要其父进程为其“收尸”,回收子进程占用的一些内存资源,父进程通过调用wait()(或其变体 waitpid()、waitid()等)函数回收子进程资源,归还给系统。如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。如果系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。通过使用命令ps -aux查看进程状况,僵尸进程是Z(zombie),R是可执行进程(run),S 处于休眠状态,i多线程,克隆线程,T 停止或被追踪。
总结:孤儿进程即父进程先于子进程结束,其pid自动变为1(字符界面下),僵尸进程即子进程结束父进程未对其进行回收,导致堵塞内核进程表,可以通过ps-aux查看进程状况。
SIGCHLD 信号:当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;那既然子进程状态改变时(终止、暂停或恢复),父进程会收到 SIGCHLD 信号,SIGCHLD 信号的系统默认处理方式是将其忽略,所以我们要捕获它、绑定信号处理函数,在信号处理函数中调用 wait()收回子 进程,回收完毕之后再回到父进程自己的工作流程中(不能一直wait()等待子进程终止,这样父进程就没办法做其他的了,可以使用信号(即中断),然后在中断处理函数中调用wait()回收子进程即可),同时因为调用信号处理函数的时候,会自动将引发调用的信号加入信号掩码中,即一次只能处理一个子进程,但是如果同时多个子进程终止,可能会留下许多僵尸进程,解决方案就是:在 SIGCHLD 信号处理函数中循环以非阻塞方式来调用 waitpid(),直至再无其它终止的子进程需要处理为止(等效于在中断处理函数中并行执行wait)。
Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。同时一个新创建的进程会处于就绪态,只要得到 CPU 就能被执行。在这里插入图片描述

exec函数集(<unistd.h>):
在一个程序里面调用另一个程序:execve常用的方法是由 fork()生成的子进程对 execve()
的调用最为频繁,也就是子进程执行 exec 操作;因为如果把另一个程序的代码放进子进程这样会导致代码复杂,而调用execve就可以很好地避免这一现象。这些库函数都是基于系统调用 execve()而实现的,虽然参数各异、但功能相同都是在一个程序中调用一个新的程序,eg:execl()、execlp()、execle()、execv()、execvp()、execvpe()
system函数集(<stdlib.h>)
可以在程序中实现shell的代码,相当于在程序中运行命令行的指令,int system(const char *command);
进程组:
用于标识该进程属于哪一个进程组,进程组是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或者在功能上有联系。getpgid()可以获取进程对应的进程组 ID。
会话:
会话是一个或多个进程组的集合,其与进程组、进程之间的关系如下图所示:在这里插入图片描述
一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组;每个会话都有一个会话首领(leader),即创建会话的进程。
当用户准备退出会话时,系统向该会话发出 SIGHUP 信号,会话将 SIGHUP 信号发送给所有子进程,子进程接收到 SIGHUP 信号后,便会自动终止,当所有会话中的所有进程都退出时,会话也就终止了。
守护进程
也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生。特点是长期运行,与控制终端脱离。
单例模式:
即一个程序在系统中不能被同时执行多次,例如pc上登入多个qq,这种在单例模式下是不被允许的。在 Linux 系统中/var/run/目录下有很多以.pid 为后缀结尾的文件,这个实际上是为了保证程序以单例模式运行而设计的,作为程序实现单例模式运行所需的特定文件。
实现单例模式的方法:使用文件锁,通过系统调用flock()、fcntl()或库函数 lockf()均可实现对文件进行上锁。当程序启动之后,首先打开该文件,调用 open 时一般使用O_WRONLY | O_CREAT 标志,当文件不存在则创建该文件,然后尝试去获取文件锁,若是成功,则将程序的进程号(PID)写入到该文件中,写入后不要关闭文件或解锁(释放文件锁),保证进程一直持有该文件锁;若是程序获取锁失败,代表程序已经被运行、则退出本次启动,当程序退出或文件关闭之后,文件锁会自动解锁。
总结:就是在运行程序的时候打开一个文件锁,然后写入pid进文件中,保持打开文件,然后运行其他代码,当另一个程序打开文件锁时,获取失败了,则表示已经有另一个相同的程序正在运行。

线程

线程(Thread);与进程类似,线程是允许应用程序并发执行多个任务的一种机制,线程参与系统调度,事实上,系统调度的最小单元是线程、而并非进程。是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。
一个程序运行时,,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序一开始时就运行的线程。应用程序都是以 main()做为入口开始运行的,所以 main()函数就是主线程的入口函数,main()函数所执行的任务就是主线程需要执行的任务。
任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程,多线程进程:除了主线程以外,还包含其它的线程,其它线程通常由主线程来创建(调用pthread_create 创建一个新的线程),那么创建的新线程就是主线程的子线程。主线程通常会在最后结束运行,执行各种清理工作,譬如回收各个子线程,其它新的线程(也就是子线程)是由主线程创建的。
线程是程序最基本的运行单位,而进程不能运行,真正运行的是进程中的线程。当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。
同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack,我们称为线程栈),自己的寄存器环境(register context)、自己的线程本地存储(thread-local storage)。
特点:线程不单独存在、而是包含在进程中; 线程是参与系统调度的基本单位; 可并发执行。同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果;共享进程资源。
多线程和多进程,都可以实现任务并发,但是多进程其实是分时复用轮流切换运行的,而且进程间通信较为麻烦,各进程地址空间隔离。多线程在多核处理器上更有优势!线程创建的速度远大于进程创建的速度。所以一般使用线程进行并行任务操作。
并行串行并发,并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行。并发强调的是一种时分复用,与串行的区别在于,它不必等待上一个任务完成之后在做下一个任务,可以打断当前执行的任务切换执行下一个任何,这就是时分复用。
⚫你吃饭吃到一半,电话来了,你一直到吃完了以后才去接电话,这就说明你不支持并发也不支持并行,仅仅只是串行,一件事、一件事接着做
⚫ 你吃饭吃到一半,电话来了,你停下吃饭去接了电话,电话接完后继续吃饭,这说明你支持并发,交替做不同的事;
⚫ 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行,同时做不同的事
多核处理器和单核处理器,多核处理器有多个执行单元,可以并行执行多条指令。对于单核处理器系统来说,它只有一个执行单元(如imx6u),只能采用并发运行系统中的线程,而肯定不可能是串行。内核实现了调度算法,用于控制系统中所有线程的调度,简单点来说,系统中所有参与调度的线程会加入到系统的调度队列中,它们由内核控制,每一个线程执行一段时间后,由系统调度切换执行调度队列中下一个线程,依次进行。但是由于核心处理速度太快,交替运行一次的时间宏观上忽略不计,表现效果就是同时运行所有的线程。ps:单个核心一次只能运行一个线程的计算,但是可以存在多个线程,靠计算速度实现并发。
线程id:库函数 pthread_self()来获取自己的线程 ID(<pthread.h>),pthread_equal()函数来检查两个线程 ID 是否相等。
主线程可以使用库函数 pthread_create()负责创建一个新的线程,创建出来的新线程被称为主线程的子线程。
线程终止:start函数的return,pthread_exit(),pthread_cancel()函数退出或者取消线程,同时exit等直接退出进程也会导致线程关闭。
线程回收:调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源;调用 pthread_join()函数将会以阻塞的形式等待指定的线程终止,如果该线程已经终止,则 pthread_join()立刻返回。当其它线程没有调用 pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应用程序无法创建新的线程。
线程取消:调用 pthread_cancel()库函数向一个指定的线程发送取消请求,线程分离,当线程终止时,其它线程可以通过调用 pthread_join()获取其返回状态、回收线程资源,有时,程序员并不关心线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。
线程的退出处理函数:使用 atexit()函数注册进程终止处理函数,当进程调用 exit()退出时就会
执行进程终止处理函数。当线程退出时也可以这样做,当线程终止退出时,去执行这样的处理函数,我们把这个称为线程清理函数(thread cleanup handler)。线程通过函数 pthread_cleanup_push()和 pthread_cleanup_pop()分别负责向调用线程的清理函数栈中添加
和移除清理函数。
线程同步:
线程同步是为了对共享资源的访问进行保护,如果两个线程同时访问一个共享的数据,同时修改了共享数据,会导致不可预测的后果,其数据最终结果无法确定,所以需要进行线程同步,必须确保多个线程不会同时修改同一变量、或者某一线程不会读取正由其它线程修改的变量,也就是必须确保不会出现对共享资源的并发访问。Linux 系统提供了多种用于实现线程同步的机制,常见的方法有:互斥锁、条件变量、自旋锁以及读写锁等。
互斥锁(mutex)又叫互斥量:
在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁。跟前文的文件锁类似。
互斥锁初始化,pthread_mutex_init()函数初始化互斥锁,然后就是对其进行加锁和解锁操作,互斥锁初始化之后,处于一个未锁定状态,调用函数 pthread_mutex_lock()可以对互斥锁加锁、获取互斥锁,而调用函数 pthread_mutex_unlock()可以对互斥锁解锁、释放互斥锁。需要加入头文件<pthread.h>,同时当不再需要互斥锁时,应该将其销毁,通过调用 pthread_mutex_destroy()函数来销毁互斥锁。
互斥锁死锁,就是一个线程对一个互斥锁加锁两次。。当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁。
自旋锁:
跟互斥锁一样也是锁,在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。
区别:,但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者是否已经释放了锁。互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层。
缺点:自旋锁一直占用的 CPU,它在未获得锁的情况下,一直处于运行状态(自旋),所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低
使用:自旋锁使用 pthread_spinlock_t 数据类型表示,当定义自旋锁后,需要使用 pthread_spin_init()函数对其进行初始化,当不再使用自旋锁时,调用 pthread_spin_destroy()函数将其销毁。
读写锁:
不同于前面的锁只有加锁和解锁两个状态,读写锁有三个状态,读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。
读写锁非常适合于对共享数据读的次数远大于写的次数的情况。当读写锁处于写模式加锁状态
时,它所保护的数据可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁。

高级io

阻塞其实就是进入了休眠状态,交出了 CPU 控制权,阻塞式 I/O 顾名思义就是对文件的 I/O 操作(读写操作)是阻塞式的,当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这就是阻塞式 I/O 常见的一种表现;如果是非阻塞式 I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误!
在open函数中加入| O_NONBLOCK即可改为非阻塞的,如果没有加入则默认未阻塞模式。
阻塞io的缺点,没办法并行读取,如鼠标键盘,需要先读取鼠标再读取键盘,如果鼠标没数据就会一直堵塞。使用非阻塞io解决了阻塞式 I/O 情况下并发读取文件所出现的问题,但依然不够完
美,使得程序的 CPU 占用率特别高,可以使用io的多路复用解决。
io的多路复用
I/O 多路复用(IO multiplexing)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行 I/O 操作时,能够通知应用程序进行相应的读写操作。I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞 I/O,譬如程序中既要读取鼠标、又要读取键盘,多路读取。相当于同时监控,一旦有数据立刻调用。
使用系统调用 select()和 poll()实现io的多路复用,外部阻塞式,内部监视多路 I/O。
键盘是标准输入设备 stdin,进程会自动从父进程中继承标准输入、标准输出以及标准错误,标准输入设备对应的文件描述符为 0,所以在程序当中直接使用即可,不需要再调用 open 打开。

int main(void) {
 char buf[100];
 int fd, ret = 0, flag;
 fd_set rdfds;
 int loops = 5;
 /* 打开鼠标设备文件 */
 fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
 if (-1 == fd) {
 perror("open error");
 exit(-1);
 }
 /* 将键盘设置为非阻塞方式 */
 flag = fcntl(0, F_GETFL); //先获取原来的 flag
 flag |= O_NONBLOCK; //将 O_NONBLOCK 标准添加到 flag
 fcntl(0, F_SETFL, flag); //重新设置 flag
 /* 同时读取键盘和鼠标 */
 while (loops--) {
 FD_ZERO(&rdfds);
 FD_SET(0, &rdfds); //添加键盘
 FD_SET(fd, &rdfds); //添加鼠标,fd也是一个int整数相当于id
 ret = select(fd + 1, &rdfds, NULL, NULL, NULL);
 if (0 > ret) {
 perror("select error");
 goto out;
 }
 else if (0 == ret) {
 fprintf(stderr, "select timeout.\n");
 continue;
 }
 /* 检查键盘是否为就绪态 */
 if(FD_ISSET(0, &rdfds)) {
 ret = read(0, buf, sizeof(buf));
 if (0 < ret)
 printf("键盘: 成功读取<%d>个字节数据\n", ret);
 }
 /* 检查鼠标是否为就绪态 */
 if(FD_ISSET(fd, &rdfds)) {
 ret = read(fd, buf, sizeof(buf));
 if (0 < ret)
 printf("鼠标: 成功读取<%d>个字节数据\n", ret);
 }
 }
out:
 /* 关闭文件 */
 close(fd);
 exit(ret);
}

跟上面的select()提供函数对其进行操作相同,poll提供结构体进行设置,差别如下:

/* 同时读取键盘和鼠标 */
 fds[0].fd = 0;
 fds[0].events = POLLIN; //只关心数据可读
 fds[0].revents = 0;
 fds[1].fd = fd;
 fds[1].events = POLLIN; //只关心数据可读
 fds[1].revents = 0;
 ...
 ret = poll(fds, 2, -1);//2代表有两个数据要监督即fds[]有两个元素,1代表要一直轮询跟select相同。
 if(fds[0].revents & POLLIN) {
 ret = read(0, buf, sizeof(buf));
 if (0 < ret)
 printf("键盘: 成功读取<%d>个字节数据\n", ret);
 }

总结:在使用 select()或 poll()时需要注意一个问题,当监测到某一个或多个文件描述符成为就绪态(可以读或写)时,需要执行相应的 I/O 操作,以清除该状态,否则该状态将会一直存在;
异步io
上面那个是多路复用io,当使用异步io中,当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。因为是内核通过发送信号进行驱动,又称信号驱动 I/O。
操作步骤:1,指定O_NONBLOCK 标志使能非阻塞 I/O。2,指定 O_ASYNC 标志使能异步 I/O。3,设置异步 I/O 事件的接收进程。也就是当文件描述符上可执行 I/O 操作时会发送信号通知该进程,通常将调用进程设置为异步 I/O 事件的接收进程。4,为内核发送的通知信号注册一个信号处理函数。默认情况下,异步 I/O 的通知信号是 SIGIO。5,以上步骤完成之后,进程就可以执行其它任务了,当 I/O 操作就绪时,内核会向进程发送一个 SIGIO信号,当进程接收到信号时,会执行预先注册好的信号处理函数,我们就可以在信号处理函数中进行 I/O 操作。
这里也相当于在监控,但是运行监控的是内核,跟前面的系统调用也有一些区别。

int flag;
flag = fcntl(0, F_GETFL); //先获取原来的 flag
flag |= O_ASYNC; //将 O_ASYNC 标志添加到 flag
fcntl(fd, F_SETFL, flag); //重新设置 flag

fcntl(fd, F_SETOWN, getpid());//设置异步 I/O 事件的接收进程,第一个是文件描述符,第二个是操作命令cmd,第三个是传入接受进程的id
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#define MOUSE "/dev/input/event3"
static int fd;
static void sigio_handler(int sig) {
 static int loops = 5;
 char buf[100] = {0};
 int ret;
 if(SIGIO != sig)
 return;
 ret = read(fd, buf, sizeof(buf));
 if (0 < ret)
 printf("鼠标: 成功读取<%d>个字节数据\n", ret);
 loops--;
 if (0 >= loops) {
 close(fd);
 exit(0);
 } }
int main(void) {
 int flag;
 /* 打开鼠标设备文件<使能非阻塞 I/O> */
 fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
 if (-1 == fd) {
 perror("open error");
 exit(-1);
 }
 /* 使能异步 I/O */
 flag = fcntl(fd, F_GETFL);
 flag |= O_ASYNC;
 fcntl(fd, F_SETFL, flag);
 /* 设置异步 I/O 的所有者 */
 fcntl(fd, F_SETOWN, getpid());
 /* 为 SIGIO 信号注册信号处理函数 */
 signal(SIGIO, sigio_handler);//因为SIGIO是固定的,所以不需要绑定进程。
 for ( ; ; )
 sleep(1);
}

异步和poll,select的区别:对于异步 I/O,内核可以“记住”要检查的文件描述符,且仅当这些文件描述符上可执行 I/O 操作时,内核才会向应用程序发送信号。而对于 select()或 poll()函数来说,内部实现原理其实是通过轮训的方式来检查多个文件描述符是否可执行 I/O 操作。当需要检查的文件描述符数量较多时,随之也将会消耗大量的 CPU 资源来实现轮训检查操作。
存储映射
存储映射 I/O(memory-mapped I/O)是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write 操作,这样就可以在不使用基本 I/O 操作函数 read()和 write()的情况下执行 I/O 操作。
需要内核将一个给定的文件映射到进程地址空间中的一块内存区域中,这由系统调用 mmap()来实现。void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);addr指定映射到内存的初始地址,length是映射的长度,offset文件映射的偏移量,跟前面的length可以精确确定需要映射的文件的具体位置,fd需要映射的文件描述符。在这里插入图片描述
对于 mmap()函数,参数 addr 和 offset 在不为 NULL 和 0 的情况下,addr 和 offset 的值通常被要求是系统页大小的整数倍,可通过 sysconf()函数获取页大小。
当我不需要这个映射的时候需要解除映射关系,需要使用munmap()解除映射关系,int munmap(void *addr, size_t length);需要头文件件<sys/mman.h>
当进程终止时也会自动解除映射(如果程序中没有显式调用 munmap()),但调用 close()关闭文件时并不会解除映射。

int main(int argc, char *argv[])
{
 int srcfd, dstfd;
 void *srcaddr;
 void *dstaddr;
 int ret;
 struct stat sbuf;
 if (3 != argc) {
 fprintf(stderr, "usage: %s <srcfile> <dstfile>\n", argv[0]);
 exit(-1);
 }
 /* 打开源文件 */
 srcfd = open(argv[1], O_RDONLY);
 if (-1 == srcfd) {
 perror("open error");
 exit(-1);
 }
 /* 打开目标文件 */
 dstfd = open(argv[2], O_RDWR |
 O_CREAT | O_TRUNC, 0664);
 if (-1 == dstfd) {
 perror("open error");
 ret = -1;
 goto out1;
 }
 /* 获取源文件的大小 */
 fstat(srcfd, &sbuf);
 /* 设置目标文件的大小 */
 ftruncate(dstfd, sbuf.st_size);
 /* 将源文件映射到内存区域中 */
 srcaddr = mmap(NULL, sbuf.st_size,
 PROT_READ, MAP_SHARED, srcfd, 0);
 if (MAP_FAILED == srcaddr) {
 perror("mmap error");
 ret = -1;
 goto out2;
 }
 /* 将目标文件映射到内存区域中 */
 dstaddr = mmap(NULL, sbuf.st_size,
 PROT_WRITE, MAP_SHARED, dstfd, 0);
 if (MAP_FAILED == dstaddr) {
 perror("mmap error");
 ret = -1;
 goto out3;
 }
 /* 将源文件中的内容复制到目标文件中 */
 memcpy(dstaddr, srcaddr, sbuf.st_size);
 /* 程序退出前清理工作 */
out4:
 /* 解除目标文件映射 */
 munmap(dstaddr, sbuf.st_size);
out3:
 /* 解除源文件映射 */
 munmap(srcaddr, sbuf.st_size);
out2:
 /* 关闭目标文件 */
 close(dstfd);
out1:
 /* 关闭源文件并退出 */
 close(srcfd);
 exit(ret);
}

普通io和映射io的区别:普通 I/O 方式一般是通过调用 read()和 write()函数来实现对文件的读写,使用 read()和 write()读写文件时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,数据需要在不同的缓存间倒腾,效率会比较低。
存储映射 I/O 的实质其实是共享,与 IPC 之内存共享很相似。譬如执行一个文件复制操作来说,对于普通 I/O 方式,首先需要将源文件中的数据读取出来存放在一个应用层缓冲区中,接着再将缓冲区中的数据写入到目标文件中。
在这里插入图片描述
在这里插入图片描述
ps:fcntl()函数是一个多功能文件描述符管理工具箱,通过配合不同的 cmd 操作命令来实现不同的功能。O_TRUNC调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值