第一章 系统调用
内核提供了一系列的服务、资源、支持一系列功能,应用程序通过调用系统调用 API 函数来使用内核提供的服务、资源以及各种各样的功能
库函数也就是 C 语言库函数
在 Linux 下,通常以动态(.so) 库文件的形式提供,存放在根文件系统/lib 目录下
- 库函数是属于应用层,而系统调用是内核提供给应用层的编程接口,属于系统内核的一部分
库函数运行在用户空间,调用系统调用会由用户空间(用户态)陷入到内核空间(内核态)
库函数通常是有缓存的,而系统调用是无缓存的,所以在性能、效率上,库函数通常要优于系统调用
int main(int argc, char **argv) {
}
argc 形参表示传入参数的个数,包括应用程序自身路径和程序名
可能很难理解,举个例子
./hello 112233
那么此时参数个数为 2
argv[0]等于"./hello"
argv[1]等于"112233"
文件 I/O(Input、Outout)输入/输出操作
1.文件描述符int 类型
一个进程最多可以打开 1024 个文件,可以改
文件描述符是从 0 开始分配的
如果文件被关闭后,它对应的文件描述符将会被释放
一般是从3开始的。系统默认开始就分配系统标准输入(0)、标准输出(1)以及标准错误(2)。后面的可能是vscode开始文件
2. oepn在 Linux 系统中要操作一个文件,需要先打开该文件,得到文件描述符,然后再对文件进行相应的读写操作(或其他操作),最后在关闭该文件
write read close lseek
当打开文件时,会将读写偏移量设置为指向文件开始位置处,以后每次调用 read()、write()将自动对其进行调整,以指向已读或已写数据后的下一字节
注意!!!
⚫ 一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd
⚫ 一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。
⚫ 一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的。
Linux 系统下,可以使用 dup 或 dup2 这两个系统调用对文件描述符进行复制
复制得到的文件描述符与旧的文件描述符都指向了同一个文件表
dup 系统调用分配的文件描述符是由系统分配的,遵循文件描述符分配原则,并不能自己指定一个文件描述符,这是 dup 系统调用的一个缺陷;而 dup2 系统调用修复了这个缺陷,可以手动指定文件描述符
文件共享文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个 inode)被多个独立的读写体同时进行 IO 操作
读写体理解成文件描述符
常见的三种文件共享的实现方式
(1)同一个进程中多次调用 open 函数打开同一个文件
(2)不同进程中分别使用 open 函数打开同一个文件
(3)同一个进程中通过 dup(dup2)函数对文件描述符进行复制
文件在没有被打开的情况下一般都是存放在磁盘中的,譬如电脑硬盘、移动硬盘、U 盘等外部存储设备。静态文件
硬盘的最小存储单位叫做“扇区”(Sector),每个扇区储存 512 字节(相当于 0.5KB)
操作系统读取硬盘的时候,一次性连续读取多个扇区
这种由多个扇区组成的==“块”,是文件存取的最小单位==。“块”的大小,最常见的是 4KB,即连续八个 sector 组成一个 block。
调用 open 函数是如何找到对应文件的数据存储“块”的呢
我们的磁盘在进行分区、格式化的时候会将其分为两个区域,
一个是数据区,用于存储文件中的数据;
另一个是 inode 区,用于存放 inode table(inode 表)
每一个文件都有唯一的一个 inode,每一个 inode 都有一个与之相对应的数字编号,通过这个数字编号就可以找到 inode table 中所对应的 inode。
U盘快速格式化只是删除了 inode table 表,真正存储文件数据的区域并没有动
文件打开时的状态调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区)
并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)
打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件。
我们再来说一下,为什么要这样设计?
- 磁盘、硬盘、U 盘等存储设备基本都是 Flash
块设备,因为块设备硬件本身有一块一块为单位读写限制等特征。一个字节的改动也需要将该字节所在的 block
全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的读写操作非常不灵活
而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活
在 Linux 系统中,内核会为每个进程设立进程控制块(PCB),用于记录进程的状态信息、运行特征等
PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文件状态标志、引用计数、当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表
Linux 系统下对常见的错误做了一个编号,每一个编号都代表着每一种不同的错误类型,
当函数执行发生错误的时候,操作系统会将这个错误所对应的编号赋值给 errno 变量,
每一个进程(程序)都维护了自己的 errno 变量,它是程序中的全局变量,该变量用于存储就近发生的函数执行错误编号,也就意味着下一次的错误码会覆盖上一次的错误码。
strerror 函数
perror 函数
查看错误信息,一般用的最多的还是这个函数,
调用此函数不需要传入 errno,函数内部会自己去获取 errno 变量的值
除此之外还可以在输出的错误提示字符串之前加入自己的打印信息
例子:perror(“open error”);
进程(程序)退出可以分为正常退出和异常退出
异常往往更多的是一种不可预料的系统异常,可能是执行了某个函数时发生的、也有可能是收到了某种信号等
进程正常退出除了可以使用 return 之外,还可以使用 exit()、_exit()以及_Exit()
1 main 函数中使用 return 后返回,return 执行后把控制权交给调用函数,结束该进程。
2 调用==_exit()或_Exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统。==
3 exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。
执行 exit()会执行一些清理工作,最后调用_exit()函数。推荐大家使用 exit()
lseek()系统调用还允许文件偏移量超出文件长度
文件的大小是 4K(也就是 4096 个字节),如果通过 lseek 系统调用将该文件的读写偏移量移动到偏移文件头部 6000 个字节处
意味着 4096~6000 字节之间出现了一个空洞,里面应该全是空字符‘ ’???
文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才分配
应用场景
⚫ 在使用迅雷下载文件时,还未下载完成,就发现该文件已经占据了全部文件大小的空间,这也是空洞文件;下载时如果没有空洞文件,多线程下载时文件就只能从一个地方写入,这就不能发挥多线程的作用了;如果有了空洞文件,可以从不同的地址同时写入,就达到了多线程的优势;
⚫ 在创建虚拟机时,你给虚拟机分配了 100G 的磁盘空间,但其实系统安装完成之后,开始也不过只用了 3、4G 的磁盘空间,如果一开始就把 100G 分配出去,资源是很大的浪费。
Linux 是一个多任务、多进程操作系统
多个不同的进程就有可能对同一个文件进行 IO 操作,此时该文件便是它们的共享资源,可能会导致的竞争冒险
假设有两个独立的进程 A 和进程 B 都对同一个文件进行追加写操作(也就是在文件末尾写入数据),容易出现由于进程 A 的时间片耗尽,然后内核切换到了进程 B,从而覆盖数据。其操作之后的所得到的结果往往是不可预期的
原子操作原子操作要么一步也不执行,一旦执行,必须要执行完所有步骤,不可能只执行所有步骤中的一个子集。
(1)O_APPEND 实现原子操作
(2)pread()和 pwrite() 调用 pread 函数或 pwrite 函数可传入一个位置偏移量 offset 参数
(3) O_CREAT| O_EXCL 创建一个文件 判断文件是否存在、存在返回错误
fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作
ioctl()可以认为是一个文件 IO 操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或硬件外设,此函数将会在进阶篇中使用到,譬如可以通过 ioctl 获取 LCD 相关信息等
使用系统调用 truncate()或 ftruncate()可将普通文件截断为指定字节长度
第四章 标准 I/O 库标准 I/O 虽然是对文 件 I/O 进行了封装,还会处理很多细节,譬如分配 stdio 缓冲区、以优化的块长度执行 I/O 等
FILE 指针FILE 指针的作用相当于文件描述符
标准输入、标准输出和标准错误用户通过标准输入设备与系统进行交互,进程将从标准输入(stdin)文件中得到输入数据,将正常输出数据(譬如程序中 printf 打印输出的字符串)输出到标准输出(stdout)文件,而将错误信息(譬如函数调用报错打印的信息)输出到标准错误(stderr)文件。
标准输出文件和标准错误文件都对应终端的屏幕,而标准输入文件则对应于键盘。
每个进程启动之后都会默认打开标准输入、标准输出以及标准错误,得到三个文件描述符
#include#define STDIN_FILENO 0 #define STDOUT_FILENO1 #define STDERR_FILENO2
打开文件 fopen()
fread()和 fwrite() 返回值是所写和读的大小
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fseek 定位
int fseek(FILE *stream, long offset, int whence);
库函数 ftell()可用于获取文件当前的读写位置偏移量
如果返回值小于参数 nmemb 所指定的值,表示发生了错误或者已经到了文件末尾(文件结束 end-of-file),但 fread()无法具体确定是哪一种情况
库函数 feof()用于测试参数 stream 所指文件的 end-of-file 标志,如果 end-of-file 标志被设置了,则调用feof()函数将返回一个非零值,如果 end-of-file 标志没有被设置,则返回 0。
库函数 ferror()用于测试参数 stream 所指文件的错误标志,如果错误标志被设置了,则调用 ferror()函数将返回一个非零值,如果错误标志没有被设置,则返回 0。
库函数 clearerr()用于清除 end-of-file 标志和错误标志,当调用 feof()或 ferror()校验这些标志后,通常需要清除这些标志,避免下次校验时使用到的是上一次设置的值,此时可以手动调用 clearerr()函数清除标志。
格式化 I/OC 库函数提供了 5 个格式化输出函数,包括:printf()、fprintf()、dprintf()、sprintf()、snprintf()
#includeint printf(const char *format, ...); int fprintf(FILE *stream, const char *format, ...); int dprintf(int fd, const char *format, ...); int sprintf(char *buf, const char *format, ...); int snprintf(char *buf, size_t size, const char *format, ...);
sprintf()函数可能会发生缓冲区溢出的问题,存在安全隐患
snprintf()如果写入到缓冲区的字节数大于参数 size 指定的大小,超出的部分将会被丢弃!如果缓冲区空间足够大,snprintf()函数就会返回写入到缓冲区的字符数
格式控制字符串 format
用于控制对应的参数如何进行转换
printf(“转换说明 1 转换说明 2 转换说明 3”, arg1, arg2, arg3);
C 库函数提供了 3 个格式化输入函数,包括:scanf()、fscanf()、sscanf()
#includeI/O 缓冲int scanf(const char *format, ...); int fscanf(FILE *stream, const char *format, ...); //类似fprintf int sscanf(const char *str, const char *format, ...); //类似sprintf
出于速度和效率的考虑,系统 I/O 调用(即文件 I/O,open、read、write 等)和==标准 C 语言库 I/O 函数(即标准 I/O 函数)==在操作磁盘文件时会对数据进行缓冲
1.文件 I/O 的内核缓冲read()和 write()系统调用在进行文件读写操作的时候并不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区(kernel buffer cache)之间复制数据。
在后面的某个时刻,内核会将其缓冲区中的数据写入(刷新)到磁盘设备中
强制将文件 I/O 内核缓冲区中缓存的数据写入(刷新)到磁盘设备中
系统调用 fsync()将参数 fd 所指文件的内容数据和元数据写入磁盘,只有在对磁盘设备的写入操作完成之后,fsync()函数才会返回
#includeint fsync(int fd);
系统调用 fdatasync()与 fsync()类似。刷新所有文件 I/O 内核缓冲区
第五章 文件属性与目录 Linux 系统中的文件类型普通文件
最常见的,譬如文本文件、二进制文件,我们编写的源代码文件这些都是普通文件
普通文件可以分为两大类:文本文件和二进制文件。
⚫ 文本文件:文件中的内容是由文本构成的,所谓文本指的是 ASCII 码字符。譬如常见的.c、.h、.sh、.txt 等
⚫ 二进制文件:二进制文件中存储的本质上也是数字,真正的数字。譬如 Linux 系统下的可执行文件、C 代码编译之后得到的.o 文件、.bin 文件等都是二进制文件。
在 Linux 系统下,可以通过 stat 命令或者 ls 命令来查看文件类型
ls中
⚫ ’ - ':普通文件
⚫ ’ d ':目录文件
⚫ ’ c ':字符设备文件
⚫ ’ b ':块设备文件
⚫ ’ l ':符号链接文件
⚫ ’ s ':套接字文件
⚫ ’ p ':管道文件
关于普通文件就给大家介绍这么多。
目录文件
录(directory)就是文件夹,文件夹在 Linux 系统中也是一种文件,是一种特殊文件
文件夹中记录了该文件夹本省的路径以及该文件夹下所存放的文件
字符设备文件和块设备文件
Linux 系统中,可将硬件设备分为字符设备和块设备
硬件设备会对应到一个设备文件,应用程序通过对设备文件的读写来操控、使用硬件设备
但是设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护
字符设备文件一般存放在 Linux 系统/dev/目录下,所以/dev 也称为虚拟文件系统 devfs
**符号链接文件(link)**类似于 Windows 系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另一个文件路径
stat 函数
Linux 下可以使用 stat 命令查看文件的属性
#include#include #include int stat(const char *pathname, struct stat *buf);
struct stat 是内核定义的一个结构体。这个结构体中的所有元素加起来构成了文件的属性信息
struct stat
{
dev_t st_dev;
ino_t st_ino;
mode_t st_mode;
nlink_t st_nlink;
uid_t st_uid;
gid_t st_gid;
dev_t st_rdev;
off_t st_size;
blksize_t st_blksize;
blkcnt_t st_blocks;
struct timespec st_atim;
struct timespec st_mtim;
struct timespec st_ctim;
};
判断文件所有者对该文件是否具有可执行权限
if (st.st_mode & S_IXUSR) {
//有权限
} else {
//无权限
}
判断是不是普通文件
// S_IFMT 宏是文件类型字段位掩码: S_IFMT 0170000
if ((st.st_mode & S_IFMT) == S_IFREG) {
}
if ((st.st_mode & S_IFMT) == S_IFLNK) {
}
fstat 和 lstat 函数
stat 是从文件名出发得到文件属性信息,不需要先打开文件
使用 fstat 函数之前需要先打开文件得到文件描述符
对于符号链接文件,stat、fstat 查阅的是符号链接文件所指向的文件对应的文件属性信息,而 lstat 查阅的是符号链接文件本身的属性信息。
- 实际用户 ID 和实际组 ID 标识我们究竟是谁,也就是执行该进程的用户是谁、以及该用户对应的所属组;实际用户 ID 和实际组
ID 确定了进程所属的用户和组。 进程的有效用户 ID、有效组 ID 以及附属组 ID 用于文件访问权限检查
首先对于有效用户 ID 和有效组 ID 来说,这是进程所持有的概念,对于文件来说,并无此属性!
通常,绝大部分情况下,进程的有效用户等于实际用户(有效用户 ID 等于实际用户 ID),有效组等于实际组(有效组 ID 等于实际组 ID)。
chown fchown 和 lchown 函数
只有超级用户进程能更改文件的用户 ID;
文件的权限可以分为两个大类,分别是普通权限和特殊权限(也可称为附加权限)
r 表示具有读权限;
w 表示具有写权限;
x 表示具有执行权限;
-表示无此权限。
特殊权限
S 字段三个 bit 位中,从高位到低位依次表示文件的 set-user-ID 位权限、set-group-ID 位权限以及 sticky 位权限
S_ISUID 04000 set-user-ID bit S_ISGID 02000 set-group-ID bit (see below) S_ISVTX 01000 sticky bit (see below)
以上数字使用的是八进制方式表示。对应的 bit 位数字为 1,则表示设置了该权限
if (st.st_mode & S_ISUID) {
//设置了 set-user-ID 位权限
} else {
//没有设置 set-user-ID 位权限
}
当进程对文件进行操作的时候、将进行权限检查,如果文件的 set-user-ID 位权限被设置,内核会将进程的有效 ID 设置为该文件的用户 ID(文件所有者 ID)。意味着该进程直接获取了文件所有者
的权限、以文件所有者的身份操作该文件。
目录权限
创建文件、删除文件
- 目录的读权限:可列出(譬如:通过 ls 命令)目录之下的内容(即目录下有哪些文件)。 目录的写权限:可以在目录下创建文件、删除文件。
目录的执行权限:可访问目录下的文件,譬如对目录下的文件进行读、写、执行等操作。
检查文件权限 access
#includeint access(const char *pathname, int mode);
mode:该参数可以取以下值:
⚫ F_OK:检查文件是否存在
⚫ R_OK:检查是否拥有读权限
⚫ W_OK:检查是否拥有写权限
⚫ X_OK:检查是否拥有执行权限
chmod fchomd
umask 函数
umask 不能对特殊权限位进行屏蔽
umask 权限掩码是进程的一种属性,用于指明该进程新建文件或目录时,应屏蔽哪些权限位。进程的umask 通常继承至其父进程
#include#include mode_t umask(mode_t mask);
返回值:返回设置之前的 umask 值,也就是旧的 umask。
文件的时间属性
time()查看当前系统时间
utime()和 utimes()函数来修改文件的时间属性
futimens()和 utimensat()功能与 utime()和 utimes()函数功能一样,用于显式修改文件时间戳
⚫ 可按纳秒级精度设置时间戳。相对于提供微秒级精度的 utimes(),这是重大改进!
⚫ 可单独设置某一时间戳。譬如,只设置访问时间、而修改时间保持不变
软链接文件也就是前面给大家的 Linux 系统下的七种文件类型之一
使用 ln 命令创建的两个硬链接文件与源文件都拥有相同的 inode 号,
既然inode 相同,也就意味着它们指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已,创建出来的硬链接文件与源文件对文件系统来说是完全平等的关系
当为文件每创建一个硬链接,inode 节点上的链接数就会加一,每删除一个硬链接,inode 节点上的链接数就会减一,直到为 0,inode 节点和对应的数据块才会被文件系统所回收
软链接文件与源文件有着不同的 inode 号。软链接文件的数据块中存储的是源文件的路径名,链接文件可以通过这个路径找到被链接的源文件,它们之间类似于一种“主从”关系
对于硬链接来说,存在一些限制情况
⚫ 不能对目录创建硬链接(超级用户可以创建,但必须在底层文件系统支持的情况下)
⚫ 硬链接通常要求链接文件和源文件位于同一文件系统中。
⚫ 不可以对不存在的文件创建软链接。
link()系统调用用于创建硬链接文件
ret = link("./test_file", "./hard");
创建软链接 symlink()
读取软链接文件
调用 open 打开一个链接文件本身是不会成功的,因为打开的并不是链接文件本身、而是其指向的文件
ret = readlink("./soft", buf, sizeof(buf));
目录
目录作为一种特殊文件,并不适合使用前面介绍的文件 I/O 方式进行读写等操作
在 Linux 系统下,会有一些专门的系统调用或 C 库函数用于对文件夹进行操作
目录存储形式
目录在文件系统中的存储方式与常规文件类似
1.mkdir
2.rmdir
3.opendir
4.readdir
#includestruct dirent *readdir(DIR *dirp);
struct dirent {
ino_t d_ino;
off_t d_off;
unsigned short d_reclen;
unsigned char d_type;
char d_name[256];
};
对于 struct dirent 结构体,我们只需要关注 d_ino 和 d_name 两个字段即可,分别记录了文件的 inode 编号和文件名
每调用一次 readdir(),就会从 drip 所指向的目录流中读取下一条目录项(目录条目)
5.rewindir
可将目录流重置为目录起点,以便对 readdir()的下一次调用将从目录列表中的第一个文件开始6.
6.closedir()
关闭处于打开状态的目录,同时释放它所使用的资源
进程的当前工作目录
Linux 下的每一个进程都有自己的当前工作目录(current working directory),当前工作目录是该进程解析、搜索相对路径名的起点(不是以" / "斜杆开头的绝对路径)
getcwd 函数来获取进程的当前工作目录
#includechar *getcwd(char *buf, size_t size);
改变当前工作目录
系统调用 chdir()和 fchdir()可以用于更改进程的当前工作目录
#includeint chdir(const char *path); int fchdir(int fd);
删除普通文件
系统调用 unlink()或使用 C 库函数 remove()
unlink()系统调用用于移除/删除一个硬链接
unlink()系统调用并不会对软链接进行解引用操作,若 pathname 指定的文件为软链接文件,则删除软链接文件本身,而非软链接所指定的文件。
remove()是一个 C 库函数,用于移除一个文件或空目录
remove()不对软链接进行解引用操作
文件重命名rename
ret = rename("./test_file", "./new_file");
第六章 字符串处理
字符串输入/输出
put 输出字符串并自行换行。把字符串输出到标准输出设备,将’ ‘转换为换行符’ n ’
putchar() 函数可以把参数 c 指定的字符==(一个无符号字符)==输出到标准输出设备
fputc 函数 与 putchar()区别在于,putchar()只能输出到标准输出设备,而 fputc()可把字符输出到指定的文件中,既可以是标准输出、标准错误设备,也可以是一个普通文件。
fputs()与 puts()类似,也用于输出一条字符串
gets()函数用于从标准输入设备(譬如键盘)中获取用户输入的字符串
用户从键盘输入的字符串数据首先会存放在一个输入缓冲区中,gets()函数会从输入缓冲区中读取字符串存储到字符指针变量 s 所指向的内存空间,当从输入缓冲区中读走字符后,相应的字符便不存在于缓冲区
⚫ gets()函数不仅比 scanf 简洁,而且,就算输入的字符串中有空格也可以,因为 gets()函数允许输入的字符串带有空格、制表符,输入的空格和制表符也是字符串的一部分,仅以回车换行符作为字符串的分割符。而对于 scanf以%s 格式输入的时候,空格、换行符、TAB 制表符等都是作为字符串分割符存在
⚫ gets()会将回车换行符从输入缓冲区中取出来,然后将其丢弃,缓冲区中将不会遗留下回车换行符。scanf相反,缓冲区中依然还存在用户输入的分隔符
#include#include int main(void) { char s1[100] = {0}; char s2[100] = {0}; scanf("%s", s1); printf("s1: %sn", s1); scanf("%s", s2); printf("s2: %sn", s2); exit(0); }
getchar()只从标准输入设备文件输入缓冲区中读取一个字符,与 scanf 以%c 格式读取一样,空格、TAB 制表符、回车符都将是正常的字符。即使输入了多个字符,但 getchar()仅读取一个字符。
fgets()可以设置获取字符串的最大字符数。 使用 fgets()读取文件中输入的字符串,文件指针会随着读取的字节数向前移动。
fgetc
字符串长度
strlen() 返回字符串长度(以字节为单位),字符串结束字符’ '不计算在内。
编译器在编译时就计算出了 sizeof 的结果,而 strlen 必须在运行时才能计算出来;
字符串拼接
C 语言函数库中提供了 strcat()函数或 strncat()函数用于将两个字符串连接(拼接)起来
char *strcat(char *dest, const char *src); char *strncat(char *dest, const char *src, size_t n);
strcat()函数会把 src 所指向的字符串追加到 dest 所指向的字符串末尾,所以必须要保证== dest 有足够的存储空间==来容纳两个字符串,否则会导致溢出错误;dest 末尾的’ '结束字符会被覆盖
strncat()与 strcat()的区别在于,strncat 可以指定源字符串追加到目标字符串的字符数量
字符串拷贝
C 语言函数库中提供了 strcpy()函数和 strncpy()函数用于实现字符串拷贝
当 n 小于或等于 src 字符串长度(不包括结束字符的长度)时,则复制过去的字符串中没有包含结束字符’ ‘;
当 n 大于 src 字符串长度时,则会将 src 字符串的结束字符’ '也一并拷贝过去
除了 strcpy()和 strncpy()之外,其实还可以使用 memcpy()、 memmove()以及 bcopy()这些库函数实现拷贝操作,字符串拷贝本质上也只是内存数据的拷贝
memset
C 语言函数库提供了用于字符串比较的函数 strcmp()和 strncmp()
返回值:
⚫ 如果返回值小于 0,则表示 str1 小于 str2
⚫ 如果返回值大于 0,则表示 str1 大于 str2
⚫ 如果返回值等于 0,则表示字符串 str1 等于字符串 str2
主要是通过比较字符串中的字符对应的 ASCII 码值,直到出现了不同的字符
字符串查找
C 语言函数库中也提供了一些用于字符串查找的函数,包括 strchr()、strrchr()、strstr()、strpbrk()、index()以及 rindex()等
字符串与数字互转
一个字符串转为整形数据,主要包括 atoi()、atol()、atoll()以及
strtol()、strtoll()、strtoul()、strtoull()等,它们之间的区别主要包括以下两个方面:
⚫ 数据类型(int、long int、unsigned long 等)。
⚫ 不同进制方式表示的数字字符串(八进制、十六进制、十进制)。
**atoi()、atol()、atoll()**三个函数可用于将字符串分别转换为 int、long int 以及 long long 类型的数据(十进制)
**strtol()、strtoll()**可以实现将多种不同进制数(譬如二进制表示的数字字符串、八进制表示的数字字符串、十六进制表示的数数字符串)表示的字符串转换为整形数据
#includelong int strtol(const char *nptr, char **endptr, int base); long long int strtoll(const char *nptr, char **endptr, int base); printf("strtol: %ldn", strtol("0x500", NULL, 16));
base:数字基数,参数 base 必须介于 2 和 36(包含)之间,或者是特殊值 0。参数 base 决定了字符串转换为整数时合法字符的取值范围,譬如,当 base=2 时,合法字符为’ 0 ‘、’ 1 ‘(表示是一个二进制表示的数字字符串);当 base=8 时,合法字符为’ 0 ‘、’ 1 ‘、’ 2 ‘、’ 3 ‘……’ 7 ‘(表示是一个八进制表示的数字字符串);当 base=16 时,合法字符为’ 0 ’ 、’ 1 ‘、’ 2 ‘、’ 3 ‘……’ 9 ‘、’ a ‘……’ f '(表示是一个十六进制表示的数字字符串);
在 base=0 的情况下,如果字符串包含一个了“0x”前缀,表示该数字将以 16 为基数;
如果包含的是“0”前缀,表示该数字将以 8 为基数。
当 base=16 时,字符串可以使用“0x”前缀。
strtoul、strtoull 函数
使用方法与 strtol()、strtoll()一样
strtoul()返回值类型是 unsignedlong int,strtoull()返回值类型是 unsigned long long int
字符串转浮点型数据
C 函数库中用于字符串转浮点型数据的函数有 atof()、strtod()、strtof()、strtold()。
strtof()、strtod()以及 strtold()三个库函数可分别将字符串转换为 float 类型数据、double 类型数据、long double 类型数据
printf("atof: %lfn", atof("0.123"));
printf("strtof: %fn", strtof("0.123", NULL));
数字转字符串
sprintf()或 snprintf()
一个能够接受外部传参的应用程序往往使用上会比较灵活,根据参入不同的参数实现不同的功能
int main(int argc, char *argv[])
{
}
传递进来的参数以字符串的形式存在,字符串的起始地址存储在 argv 数组中,参数 argc 表示传递进来的参数个数,包括应用程序自身路径名
多个不同的参数之间使用空格分隔开来,如果参数本身带有空格、则可以使用双引号" "或者单引号’ '的形式来表示
正则表达式
通常会有这样的需要:给定一个字符串,检查该字符串是否符合某种条件或规则、或者从给定的字符串中找出符合某种条件或规则的子字符串,将匹配到的字符串提取出来。
譬如给定一个字符串,在程序当中判断该字符串是否是一个 IP 地址,对于实现这个功能,大家可能首先想到的是,使用万能的 for 循环,当然,笔者首先肯定的是,使用 for 循环自然是可以解决这个问题,但是在程序代码处理上会比较麻烦
正则表达式,又称为规则表达式(英语: Regular Expression)
检索、替换那些符合某个模式(规则)的字符串
描述了一种字符串的匹配模式(pattern)
可以用来检查一个给定的字符串中是否含有某种子字符串、将匹配的字符串替换或者从某个字符串中取出符合某个条件的子字符串。
通配符
?通配符匹配 0 个或 1 个字符,而*通配符匹配 0 个或多个字符
譬如"data?.txt"这样的匹配模式可以将下列文件查找出来:
data.dat
data1.dat
data2.dat
datax.dat
dataN.dat
尽管使用通配符的方法很有用,但它还是很有限,正则表达式则更加强大、更加灵活。
正则表达式其实也是一个字符串
该字符串由普通字符(譬如,数字 0~9、大小写字母以及其它字符)和特殊字符(称为“元字符”)所组成
由这些字符组成一个“规则字符串”。这个“规则字符串”用来表达对给定字符串的一种查找、匹配逻辑。
C 语言中使用正则表达式
C 语言中使用正则表达式
1)编译正则表达式 regcomp()
2)匹配正则表达式 regexec()
3)释放正则表达式 regfree()
匹配 URL 的正则表达式:
^((ht|f)tps?)://[-A-Za-z0-9_]+(.[-A-Za-z0-9_]+)+([-A-Za-z0-9_.,@?^=%&:/~+#]*[-A-Za-z0-9_@?^=%&/~+#])?$
1.int regcomp (regex_t *compiled, const char *pattern, int cflags)
regcomp()函数把指定的正则表达式pattern编译成一种特定的数据格式(参数regex_t *compiled),这样可以使匹配更有效。
③cflags 有如下4个值或者是它们或运算(|)后的值:
REG_EXTENDED 以功能更加强大的扩展正则表达式的方式进行匹配。
REG_ICASE 匹配字母时忽略大小写。
REG_NOSUB 不用存储匹配后的结果。
REG_NEWLINE 识别换行符,这样’$‘就可以从行尾开始匹配,’^'就可以从行的开头开始匹配。
2.int regexec (regex_t *compiled, char *string, size_t nmatch, regmatch_t matchptr [], int eflags)
当我们编译好正则表达式后,就可以用regexec 匹配我们的目标文本串了,如果在编译正则表达式的时候没有指定cflags的参数为REG_NEWLINE,则默认情况下是忽略换行符的,也就是把整个文本串当作一个字符串处理
3.void regfree (regex_t *compiled)
当我们使用完编译好的正则表达式后,或者要重新编译其他正则表达式的时候,我们可以用这个函数清空compiled指向的regex_t结构体的内容,请记住,如果是重新编译的话,一定要先清空regex_t结构体。
4.size_t regerror (int errcode, regex_t *compiled, char *buffer, size_t length)
当执行regcomp 或者regexec 产生错误的时候,就可以调用这个函数而返回一个包含错误信息的字符串。
#include#include #include int main(int argc,char** argv) { int status ,i; int cflags = REG_EXTENDED; regmatch_t pmatch[1]; const size_t nmatch = 1; //最多匹配出的结果 regex_t reg; const char * pattern = "^\w+([-+.]\w+)*@\w+([-.]\w+)*.\w+([-.]\w+)*$"; char * buf = "chenjiayi@126.com"; char errbuf[64]; if(regcomp(®,pattern,cflags)){ regerror(ret, ®, errbuf, sizeof(errbuf)); fprintf(stderr, "regcomp error: %sn", errbuf); exit(0); } status = regexec(®,buf,nmatch,0);//执行正则表达式和缓存的比较 if(status == REG_NOMATCH) printf("No matchn"); else if (0 == status) { printf("比较成功:"); for(i = pmatch[0].rm_so;i 基础语法 “^([]{})([]{})([]{})$”
第七章 系统信息与系统资源
算了太难了,用到再学在应用程序当中,有时往往需要去获取到一些系统相关的信息,譬如时间、日期、以及其它一些系统相关信息
除此之外,还会向大家介绍 Linux 系统下的/proc 虚拟文件系统uname()用于获取有关当前操作系统内核的名称和信息
时间、日期
sysinfo 系统调用可用于获取一些系统统计信息
gethostname 函数可用于单独获取 Linux 系统主机名
sysconf()函数可在运行时获取系统的一些配置信息,譬如页大小(page size)、主机名的最大长度、进程可以打开的最大文件数、每个用户 ID 的最大并发进程数等GMT 时间就是英国格林威治当地时间,也就是零时区(中时区)所在时间,与我国的标准时间北京时间(东八区)相差 早8 个小时
GMT 与 UTC 这两者几乎是同一概念,它们都是指格林威治标准时间,也就是国际标准时间,只不过UTC 时间比 GMT 时间更加精准,所以在我们的编程当中不用刻意去区分它们之间的区别。
CST 在这里其实指的是 China Standard Time(中国标准时间)的缩写,表示当前查看到的时间是中国标准时间,也就是我国所使用的标准时间–北京时间
在 Ubuntu 系统下,时区信息通常以标准格式保存在一些文件当中,这些文件通常位于/usr/share/zoneinfo目录下,该目录下的每一个文件(包括子目录下的文件)都包含了一个特定国家或地区内时区制度的相关信息系统的本地时间由时区配置文件/etc/localtime 定义,通常链接到/usr/share/zoneinfo 目录下的某一个文件
如果我们要修改 Ubuntu 系统本地时间的时区信息,可以直接将/etc/localtime 链接到/usr/share/zoneinfo目录下的任意一个时区配置文件,譬如 EST(美国东部标准时间),首先进入到/etc 目录下,执行下面的命令:sudo rm -rf localtime #删除原有链接文件 sudo ln -s /usr/share/zoneinfo/EST localtime #重新建立链接文件Linux 系统中的时间
点时间和段时间:时间点,时间段
实时时钟 RTC:操作系统中一般会有两个时钟,一个系统时钟(system clock),一个实时时钟(Real time clock)
系统时钟由系统启动之后由内核来维护,譬如使用 date 命令查看到的就是系统时钟,所以在系统关机情况下是不存在的;而实时时钟一般由 RTC 时钟芯片提供,RTC 芯片有相应的电池为其供电,以保证系统在关机情况下 RTC 能够继续工作、继续计时。Linux 系统在开机启动之后首先会读取 RTC 硬件获取实时时钟作为系统时钟的初始值,之后内核便开始维护自己的系统时钟。
RTC 硬件只有在系统开机启动时会读取一次。系统关机时,内核会将系统时钟写入到 RTC 硬件、已进行同步操作。jiffies 的引入
jiffies 是内核中定义的一个全局变量,内核使用 jiffies 来记录系统从启动以来的系统节拍数
Linux 内核在编译配置时定义了一个节拍时间,使用节拍率(一秒钟多少个节拍数)来表示
配置的节拍率越高,每一个系统节拍的时间就越短,也就意味着 jiffies 记录的时间精度越高,
当然,高节拍率会导致系统中断的产生更加频繁,频繁的中断会加剧系统的负担,一般默认情况下都是采用 100Hz 作为系统节拍率。获得时间段
通过 time()或 gettimeofday()函数可以获取到当前时间点相对于 1970-01-01 00:00:00 +0000 (UTC)这个时间点所经过时间(日历时间),所以获取得到的是一个时间段的长度
time 函数获取得到的是一个时间段,也就是从 1970-01-01 00:00:00 +0000 (UTC)到现在这段时间所经过的秒数
gettimeofday()函数微秒级时间转换函数
#includechar *ctime(const time_t *timep); char *ctime_r(const time_t *timep, char *buf); 打印出来的时间为"Mon Feb 22 17:10:46 2021"
#includestruct tm *localtime(const time_t *timep); struct tm *localtime_r(const time_t *timep, struct tm *result); 从 struct tm 结构体内容可知,该结构体中包含了年月日时分秒星期等信息
使用 localtime/localtime_r()便可以将 time_t 时间总秒数分解成了各个独立的时间信息#includestruct tm *gmtime(const time_t *timep); struct tm *gmtime_r(const time_t *timep, struct tm *result); gmtime()函数所得到的是 UTC 国际标准时间
#includetime_t mktime(struct tm *tm); mktime()函数与 localtime()函数相反, mktime()可以将使用 struct tm 结构体表示的分解时间转换为 time_t时间
asctime()函数与 ctime()函数的作用一样.ctime()是将 time_t 时间转换为固定格式字符串、而 asctime()则是将 struct tm 表示的分解时间转换为固定格式的字符串
strftime 函数功能上比 asctime()和 ctime()更加强大,它可以根据自己的喜好自定义时间的显示格式
#includeint settimeofday(const struct timeval *tv, const struct timezone *tz); 使用 settimeofday()函数可以设置时间,也就是设置系统的本地时间
总结 进程时间进程时间指的是进程从创建后(也就是程序运行后)到目前为止这段时间内使用 CPU 资源的时间总数
内核把 CPU 时间(进程时间)分为以下两个部分:
⚫ 用户 CPU 时间:进程在用户空间(用户态)下运行所花费的 CPU 时间。有时也成为虚拟时间(virtual time)。
⚫ 系统 CPU 时间:进程在内核空间(内核态)下运行所花费的 CPU 时间。这是内核执行系统调用或代表进程执行的其它任务(譬如,服务页错误)所花费的时间。Tips:进程时间不等于程序的整个生命周期所消耗的时间,如果进程一直处于休眠状态(进程被挂起、不会得到系统调度),那么它并不会使用 CPU 资源,所以休眠的这段时间并不计算在进程时间中。
#includeclock_t times(struct tms *buf); struct tms { clock_t tms_utime; clock_t tms_stime; clock_t tms_cutime; clock_t tms_cstime; }; times()函数用于获取当前进程时间
注意tms里面的是系统节拍数!!!#includeclock_t clock(void); 库函数 clock()提供了一个更为简单的方式用于进程时间,它的返回值描述了进程使用的总的 CPU 时间
产生随机数
(也就是进程时间,包括用户 CPU 时间和系统 CPU 时间)
但并不能获取到单独的用户 CPU 时间和系统 CPU 时间C 语言函数库中提供了很多函数用于产生伪随机数,其中最常用的是通过 rand()和 srand()产生随机数
调用 rand()可以得到[0, RAND_MAX]之间的伪随机数,多次调用 rand()便可以生成一组伪随机树序列,但是就是每一次运行程序所得到的随机数序列都是相同的。如果没有调用 srand()设置随机数种子的情况下,rand()会将 1 作为随机数种子,如果随机数种子相同,那么每一次启动应用程序所得到的随机数序列就是一样的
void srand(unsigned int seed); srand(time(NULL));一般将当前时间作为随机数种子赋值给参数 seed。譬如 time(NULL)
休眠有时需要将进程暂停或休眠一段时间,进入休眠状态之后,程序将暂停运行
一旦执行 sleep(),进程便主动交出 CPU 使用权,暂时退出系统调度队列
秒级休眠: sleep
微秒级休眠: usleep
高精度休眠: nanosleep#includeint nanosleep(const struct timespec *req, struct timespec *rem); req:一个 struct timespec 结构体指针,指向一个 struct timespec 变量,用于设置休眠时间长度,可精确到纳秒级别。
rem:也是一个 struct timespec 结构体指针,指向一个 struct timespec 变量,也可设置 NULL。
返回值:在成功休眠达到请求的时间间隔后,nanosleep()返回 0;如果中途被信号中断或遇到错误,则返回-1,并将剩余时间记录在参数 rem 指向的 struct timespec 结构体变量中(参数 rem 不为 NULL 的情况下,如果为 NULL 表示不接收剩余时间),还会设置 errno 标识错误类型。struct timespec 结构体,该结构体包含了两个成员变量,秒(tv_sec)和纳秒(tv_nsec)
申请堆内存malloc 和 free
#includevoid *malloc(size_t size); 返回值:返回值为 void *类型,如果申请分配内存成功,将返回一个指向该段内存的指针 void *并不是说没有返回值或者返回空指针,而是返回的指针类型未知 所以在调用 malloc()时通常需要进行强制类型转换,将 void *指针类型转换成我们希望的类型; 如果分配内存失败(譬如系统堆内存不足)将返回 NULL, 如果参数 size 为 0,返回值也是 NULL。 malloc()在堆区分配一块指定大小的内存空间。它们的值是未知的,所以通常需要程序员对 malloc()分配的堆内存进行初始化操作。
调用 free()还是不调用 free()
Linux 系统中。基于内存的这一自动释放机制,很多应用程序通常会省略对 free()函数的调用。
大多数情况下,都是根据代码需求动态申请、释放的。如果持续占用,将会导致内存泄漏,也就是人们常说的“你的程序在吃内存”!#includevoid *calloc(size_t nmemb, size_t size); calloc()函数用来动态地分配内存空间并初始化为 0
个人感觉用calloc方便对齐内存在某些应用场合非常有必要,
常用于分配对其内存的库函数有:posix_memalign()、aligned_alloc()、memalign()、valloc()、pvalloc()#includeint posix_memalign(void **memptr, size_t alignment, size_t size); void *aligned_alloc(size_t alignment, size_t size); void *valloc(size_t size); #include void *memalign(size_t alignment, size_t size); void *pvalloc(size_t size); 前面介绍的 malloc()、calloc()分配内存返回的地址其实也是对齐的,但是它俩的对齐都是固定的,并且对其的字节边界比较小
譬如在 32 位系统中,通常是以 8 字节为边界进行对其,在 64 位系统中是以 16 字节进行对齐。什么是内存对齐?
尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度
现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。
假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作.
这都是前面存了一字节的数据导致的!!!
现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。
内存对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。
有效对其值:是给定值#pragma pack(n)和结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位。了解了上面的概念后,我们现在可以来看看内存对齐需要遵循的规则:
(1) 结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。(我也不理解,看图吧)(3)== 结构体的总大小为 有效对齐值 的整数倍==,如有需要编译器会在最末一个成员之后加上填充字节。
//32位系统 #includestruct { int i; char c1; char c2; }x1; struct{ char c1; int i; char c2; }x2; struct{ char c1; char c2; int i; }x3; int main() { printf("%dn",sizeof(x1)); // 输出8 printf("%dn",sizeof(x2)); // 输出12 printf("%dn",sizeof(x3)); // 输出8 return 0; } 回到这里来posix_memalign()函数
int posix_memalign(void **memptr, size_t alignment, size_t size); 注释:posix_memalign()函数用于在堆上分配 size 个字节大小的对齐内存空间, 将*memptr 指向分配的空间,分配的内存地址将是参数 alignment 的整数倍。 参数 alignment 表示对齐字节数,alignment 必须是 2 的幂次方(譬如 2^4、2^5、2^8 等),同时也要是 sizeof(void *)的整数倍,对于 32 位系统来说,sizeof(void *)等于4,如果是 64 位系统 sizeof(void *)等于 8。 ret = posix_memalign((void **)&base, 256, 1024);上面的东西很重要哦
memalign()与 aligned_alloc()参数是一样的,它们之间的区别在于:对于参数 size 必须是参数 alignment的整数倍这个限制条件,memalign()并没有这个限制条件。
Tips:memalign()函数已经过时了,并不提倡使用!valloc()分配 size 个字节大小的内存空间,返回指向该内存空间的指针,内存空间的地址是页大小(pagesize)的倍数。
操作系统 页式存储 页与块之间的关系详解
对程序进行分页存储
对内存进行分块存储
都是2k大小Tips:valloc()函数已经过时了,并不提倡使用!
proc 文件系统
proc 文件系统是一个虚拟文件系统,它以文件系统的方式为应用层访问系统内核数据提供了接口
用户和应用程序可以通过 proc 文件系统得到系统信息和进程相关信息,对 proc 文件系统的读写作为与内核进行通信的一种手段proc 文件系统是动态创建的,文件本身并不存在于磁盘当中、只存在于内存当中,与 devfs 一样,都被称为虚拟文件系统
proc 文件系统是为了提供有关系统中进程相关的信息。内核中的很多信息也开始使用它来报告,或启用动态运行时配置
它会将内核运行时的一些关键数据信息以文件的方式呈现在 proc 文件系统下的一些特定文件中,这样相当于将一些不可见的内核中的数据结构以可视化的方式呈现给应用层。proc 文件系统挂载在系统的/proc 目录下
对于内核开发者(譬如驱动开发工程师)来说,proc 文件系统给了开发者一种调试内核的方法:通过查看/proc/xxx 文件来获取到内核特定数据结构的值,在添加了新功能前后进行对比,就可以判断此功能所产生的影响是否合理。
有很多以数字命名的文件夹,譬如 100038、2299、98560,这些数字对应的其实就是一个一个的进程 PID 号
PID号–每一个进程在内核中都会存在一个编号,通过此编号来区分不同的进程/proc 目录下除了文件夹之外,还有很多的虚拟文件,譬如 buddyinfo、cgroups、cmdline、version 等等,
不同的文件记录了不同信息,关于这些文件记录的信息和意思如下:
⚫ cmdline:内核启动参数;
⚫ cpuinfo:CPU 相关信息;
⚫ iomem:IO 设备的内存使用情况;
⚫ interrupts:显示被占用的中断号和占用者相关的信息;
⚫ ioports:IO 端口的使用情况;
⚫ kcore:系统物理内存映像,不可读取;
⚫ loadavg:系统平均负载;
⚫ meminfo:物理内存和交换分区使用情况;
⚫ modules:加载的模块列表;
⚫ mounts:挂载的文件系统列表;
⚫ partitions:系统识别的分区表;
⚫ swaps:交换分区的利用情况;
⚫ version:内核版本信息;
⚫ uptime:系统运行时间;proc 文件系统的使用
第八章 信号:基础
proc 文件系统的使用就是去读取/proc 目录下的这些文件,获取文件中记录的信息,可以直接使用 cat 命令读取,也可以在应用程序中调用 open()打开、然后再使用 read()函数读取。事实上,在很多应用程序当中,都会存在处理异步事件这种需求,而信号提供了一种处理异步事件的方法
基本概念信号是事件发生时对进程的通知机制,也可以把它称为软件中断
信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟信号的目的是用来通信的
信号可以由“谁”发出呢?
⚫ 硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。
⚫ 用于在终端下输入了能够产生信号的特殊字符。譬如在终端上按下 CTRL + C 组合按键可以产生中断信号(SIGINT),通过这个方法可以终止在前台运行的进程;按下 CTRL + Z 组合按键可以产生暂停信号(SIGCONT),通过这个方法可以暂停当前前台运行的进程。
⚫ 进程调用 kill()系统调用可将任意信号发送给另一个进程或进程组。
⚫ 发生了软件事件,即当检测到某种软件条件已经发生。进程所设置的定时器已经超时、进程执行的 CPU 时间超限、进程的某个子进程退出等等情况)信号由谁处理、怎么处理
信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施
⚫ 忽略信号
⚫ 捕获信号。当信号到达进程后,执行预先绑定好的信号处理函数。Linux 系统提供了 signal()系统调用可用于注册信号的处理函数
⚫ 执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式信号是异步的
程序是无法得知中断事件产生的具体时间
只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式。信号本质上是 int 类型数字编号
信号的分类
这些信号在头文件中定义,每个信号都是以 SIGxxx 开头
不存在编号为 0 的信号
信号编号是从 1 开始的,事实上 kill()函数对信号编号 0 有着特殊的应用从可靠性方面将信号分为可靠信号与不可靠信号
从实时性方面将信号分为实时信号与非实时信号可靠信号与不可靠信号
期 UNIX 系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题
⚫ 进程每次处理信号后,就将对信号的响应设置为系统默认操作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用 signal(),重新为该信号绑定相应的处理函数。
⚫ 早期 UNIX 下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失(处理信号时又来了新的信号,则导致信号丢失)。Linux 支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用signal()。
因此,Linux 下的不可靠信号问题主要指的是信号可能丢失。在 Linux 系统下,信号值小于 SIGRTMIN(34)的信号都是不可靠信号,这就是"不可靠信号"的来源
编号 1-31 所对应的是不可靠信号,编号 34-64 对应的是可靠信号
可靠信号支持排队,不会丢失,同时,信号的发送和绑定也出现了新版本,信号发送函数 sigqueue()及信号绑定函数 sigaction()。实时信号与非实时信号
非实
时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。
实时信号保证了发送的多个信号都能被接收,实时信号是 POSIX 标准的一部分,可用于应用进程。
一般我们也把非实时信号(不可靠信号)称为标准信号常见信号与默认行为
进程对信号的处理
⚫ SIGINT
当用户在终端按下中断字符(通常是 CTRL + C)时,内核将发送 SIGINT 信号给前台进程组中的每一
个进程。该信号的系统默认操作是终止进程的运行。所以通常我们都会使用 CTRL + C 来终止一个占用前台
的进程,原因在于大部分的进程会将该信号交给系统去处理,从而执行该信号的系统默认操作。
⚫ SIGQUIT
当用户在终端按下退出字符(通常是 CTRL + )时,内核将发送 SIGQUIT 信号给前台进程组中的每一
个进程。该信号的系统默认操作是终止进程的运行、并生成可用于调试的核心转储文件。进程如果陷入无限
循环、或不再响应时,使用 SIGQUIT 信号就很合适。所以对于一个前台进程,既可以在终端按下中断字符
CTRL + C、也可以按下退出字符 CTRL + 来终止,当然前提条件是,此进程会将 SIGINT 信号或 SIGQUIT
信号交给系统处理(也就是没有将信号忽略或捕获),进入执行该信号所对应的系统默认操作。
⚫ SIGILL
如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。该信号的系统默
认操作是终止进程的运行。
⚫ SIGABRT
当进程调用 abort()系统调用时(进程异常终止),系统会向该进程发送 SIGABRT 信号。该信号的系统
默认操作是终止进程、并生成核心转储文件。
⚫ SIGBUS
产生该信号(总线错误, bus error)表示发生了某种内存访问错误。该信号的系统默认操作是终止进程。
⚫ SIGFPE
该信号因特定类型的算术错误而产生,譬如除以 0。该信号的系统默认操作是终止进程。
⚫ SIGKILL
此信号为“必杀(sure kill)”信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕
获,故而“一击必杀”,总能终止进程。使用 SIGINT 信号和 SIGQUIT 信号虽然能终止进程,但是前提条
件是该进程并没有忽略或捕获这些信号,如果使用 SIGINT 或 SIGQUIT 无法终止进程,那就使用“必杀信
号”SIGKILL 吧。Linux 下有一个 kill 命令,kill 命令可用于向进程发送信号,我们会使用"kill -9 xxx"命令
来终止一个进程(xxx 表示进程的 pid),这里的-9 其实指的就是发送编号为 9 的信号,也就是 SIGKILL 信
号。
⚫ SIGUSR1
该信号和 SIGUSR2 信号供程序员自定义使用,内核绝不会为进程产生这些信号,在我们的程序中,可
以使用这些信号来互通通知事件的发生,或是进程彼此同步操作。该信号的系统默认操作是终止进程。
⚫ SIGSEGV
这一信号非常常见,当应用程序对内存的引用无效时,操作系统就会向该应用程序发送该信号。引起对
内存无效引用的原因很多,C 语言中引发这些事件往往是解引用的指针里包含了错误地址(譬如,未初始化
的指针),或者传递了一个无效参数供函数调用等。该信号的系统默认操作是终止进程。
⚫ SIGUSR2
与 SIGUSR1 信号相同。
⚫ SIGPIPE
涉及到管道和 socket,当进程向已经关闭的管道、FIFO 或套接字写入信息时,那么系统将发送该信号
给进程。该信号的系统默认操作是终止进程。
⚫ SIGALRM
与系统调用 alarm()或 setitimer()有关,
应用程序中可以调用 alarm()或 setitimer()函数来设置一个定时器,
当定时器定时时间到,那么内核将会发送 SIGALRM 信号给该应用程序,关于 alarm()或 setitimer()函数的使
用,后面将会进行讲解。该信号的系统默认操作是终止进程。
⚫ SIGTERM
这是用于终止进程的标准信号,也是 kill 命令所发送的默认信号(kill xxx,xxx 表示进程 pid),有时
我们会直接使用"kill -9 xxx"显式向进程发送 SIGKILL 信号来终止进程,然而这一做法通常是错误的,精心
设计的应用程序应该会捕获 SIGTERM 信号、并为其绑定一个处理函数,当该进程收到 SIGTERM 信号时,
会在处理函数中清除临时文件以及释放其它资源,再而退出程序。如果直接使用 SIGKILL 信号终止进程,
从而跳过了 SIGTERM 信号的处理函数,通常 SIGKILL 终止进程是不友好的方式、是暴力的方式,这种方
式应该作为最后手段,应首先尝试使用 SIGTERM,实在不行再使用最后手段 SIGKILL。
⚫ SIGCHLD
当父进程的某一个子进程终止时,内核会向父进程发送该信号。当父进程的某一个子进程因收到信号而
停止或恢复时,内核也可能向父进程发送该信号。注意这里说的停止并不是终止,你可以理解为暂停。该信
号的系统默认操作是忽略此信号,如果父进程希望被告知其子进程的这种状态改变,则应捕获此信号。
⚫ SIGCLD
与 SIGCHLD 信号同义。
⚫ SIGCONT
将该信号发送给已停止的进程,进程将会恢复运行。当进程接收到此信号时并不处于停止状态,系统默
认操作是忽略该信号,但如果进程处于停止状态,则系统默认操作是使该进程继续运行。
⚫ SIGSTOP
这是一个“必停”信号,用于停止进程(注意停止不是终止,停止只是暂停运行、进程并没有终止),
应用程序无法将该信号忽略或者捕获,故而总能停止进程。
⚫ SIGTSTP
这也是一个停止信号,当用户在终端按下停止字符(通常是 CTRL + Z),那么系统会将 SIGTSTP 信号
发送给前台进程组中的每一个进程,使其停止运行。
⚫ SIGXCPU
当进程的 CPU 时间超出对应的资源限制时,内核将发送此信号给该进程。
⚫ SIGVTALRM
应用程序调用 setitimer()函数设置一个虚拟定时器,当定时器定时时间到时,内核将会发送该信号给进
程。
⚫ SIGWINCH
在窗口环境中,当终端窗口尺寸发生变化时(譬如用户手动调整了大小,应用程序调用 ioctl()设置了大
小等),系统会向前台进程组中的每一个进程发送该信号。
⚫ SIGPOLL/SIGIO
这两个信号同义。这两个信号将会在高级 IO 章节内容中使用到,用于提示一个异步 IO 事件的发生,
譬如应用程序打开的文件描述符发生了 I/O 事件时,内核会向应用程序发送 SIGIO 信号。
⚫ SIGSYS
如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程。
Linux 系统提供了系统调用 signal()和 sigaction()两个函数用于设置信号的处理方式
#includetypedef void (*sig_t)(int); sig_t signal(int signum, sig_t handler); signal()函数是 Linux 系统下设置信号处理方式最简单的接口,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作
参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为 SIG_IGN 或 SIG_DFL, SIG_IGN 表示此进程需要忽略该信号, SIG_DFL 则表示设置为系统默认操作。
#include#include #include static void sig_handler(int sig) { printf("Received signal: %dn", sig); } nt main(int argc, char *argv[]) { sig_t ret = NULL; ret = signal(SIGINT, (sig_t)sig_handler); if (SIG_ERR == ret) { perror("signal error"); exit(-1); } for ( ; ; ) { } exit(0); } 当运行程序之后,程序会占用终端称为一个前台进程,此时按下中断符便会打印出信息(^C 表示按下了中断符)
平时大家使用 CTRL + C 可以终止一个进程,而这里却不能通过这种方式来终止这个测试程序,原因在于测试程序中捕获了该信号,而对应的处理方式仅仅只是打印一条语句、而并不终止进程于是想要终止进程就kill
Tips:普通用户只能杀死该用户自己的进程,无权限杀死其它用户的进程。
两种不同状态下信号的处理方式
如果程序中没有调用 signal()函数为信号设置相应的处理方式,亦或者程序刚启动起来并未运行到 signal()处,那么这时进程接收到一个信号后是如何处理的呢?
⚫ 程序启动
当一个应用程序刚启动的时候(或者程序中没有调用 signal()函数),通常情况下,进程对所有信号的处理方式都设置为系统默认操作。⚫ 进程创建
当一个进程调用 fork()创建子进程时,其子进程将会继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕获函数的地址在子进程中是有意义的。除了 signal()之外,sigaction()系统调用是设置信号处理方式的另一选择,
事实上,推荐大家使用 sigaction()函数。
sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制#includeint sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); signum:需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
act:act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式。如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
。如果参数
oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来struct sigaction 结构体
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };⚫ sa_handler:指定信号处理函数,与 signal()函数的 handler 参数相同。
⚫ sa_sigaction:也用于指定信号处理函数,他提供了更多的参数,可以通过该函数获取到更多信息
sa_handler 和sa_sigaction 是互斥的,不能同时设置,对于标准信号来说,使用 sa_handler 就可以了,可通过下面的sa_flags标志位来设置SA_SIGINFO 标志进行选择。
⚫sa_mask:参数 sa_mask 定义了一组信号,当进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除。如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除。
进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参数 sa_mask 来完成⚫ sa_restorer:该成员已过时,不要再使用了。
向进程发送信号
⚫ sa_flags:参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程
SA_NOCLDSTOP
如果 signum 为 SIGCHLD,则子进程停止时(即当它们接收到 SIGSTOP、 SIGTSTP、SIGTTIN 或 SIGTTOU中的一种时)或恢复(即它们接收到 SIGCONT)时不会收到 SIGCHLD 信号。
SA_NOCLDWAIT
如果 signum 是 SIGCHLD,则在子进程终止时不要将其转变为僵尸进程
SA_NODEFER
默认情况下,我们期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了 SA_NODEFER 标志,则表示不对它进行阻塞。
SA_RESETHAND
执行完信号处理函数之后,将信号的处理方式设置为系统默认操作。
SA_RESTART
被信号中断的系统调用,在信号处理完成之后将自动重新发起。
SA_SIGINFO
如果设置了该标志,则表示使用 sa_sigaction 作为信号处理函数,该标志为实际数字是0kill()函数
kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程#include#include int kill(pid_t pid, int sig); 返回值:成功返回 0;失败将返回-1,并设置 errno。
参数 pid 不同取值含义:
⚫ 如果 pid 为正,则信号 sig 将发送到 pid 指定的进程。
⚫== 如果 pid 等于 0,则将 sig 发送到当前进程的进程组中的每个进程。==
⚫ 如果 pid 等于-1,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外。
⚫ 如果 pid 小于-1,则将 sig 发送到 ID 为-pid 的进程组中的每个进程。基本规则是发送者进程的实际用户 ID 或有效用户 ID 必须等于接收者进程的实际用户 ID 或有效用户 ID。
超级用户root 进程可以将信号发送给任何进程从上面介绍可知,当 sig 为 0 时,仍可进行正常执行的错误检查,但不会发送信号,
这通常可用于确定一个特定的进程是否存在,如果向一个不存在的进程发送信号, kill()将会返回-1, errno 将被设置为ESRCH,表示进程不存在。raise()
有时进程需要向自身发送信号,raise()函数可用于实现这一要求#includealarm()和 pause()函数int raise(int sig); 等价于kill(getpid(), sig);



