Linux_C_入门篇学习笔记

前言

应用开发与驱动开发本就是两个不同的方向,将来在工作当中也会负责不同的任务、解决不同的问题,应用程序负责处理应用层用户需求、逻辑,而驱动程序负责内核层硬件底层操作。

YOLOv5

学习 Linux 应用开发并不针对于嵌入式行业,也可从事 PC 端应用程序开发工作以及其它相关工作。

苹果证书

第一篇 入门篇

本篇作为 Linux 应用开发入门篇,主要为大家讲解 Linux 应用开发过程当中会用到的基础入门知识,譬 如 Linux 文件 IO 操作、高级 IO 操作、文件属性、系统信息获取与设置、进程、进程间通信、线程以及信号 等基础应用编程知识,为后面学习提高篇、进阶篇章节内容打下坚实的基础。

ParticleSystem

第一章 应用编程概念

1.1 系统调用

system call 其实是 Linux 内核提供给应用层的 API

分页失效

通过系统调用,Linux 应用程序可以请求内核以自己的名义执行某些事情,譬如打开磁盘中的文件、读 写文件、关闭文件以及控制其它硬件外设。

应用编程与裸机编程、驱动编程有什么区别?

Stream

裸机编程:编写直接在硬件上运行的程序,没有操作系统支持

mcu

Linux 驱动编程:基于内核驱动框架开发驱动程序,驱动开发工程师通过调用 Linux 内核提供的接口 完成设备驱动的注册,驱动程序负责底层硬件操作相关逻辑

juc

应用编程:应用程序运行于操作系统之上。通常在操作系统下有两种不同的状态:内核态和用户态,应用程序运行在用户态、而内核则运行在内核态。

vendor.js过大

第二章 文件 I/O 基础

2.1 简单IO实例与文件描述符

Linux 下一切皆文件,文件作为 Linux 系统设计思想的核心理念。

学生

#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;
}
标志 用途
O_RDONLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWR 以可读可写方式打开文件
O_CREAT 如果 pathname 参数指向的文件不存在则创建 此文件
O_DIRECTORY 如果 pathname 参数指向的不是一个目录,则 调用 open 失败
O_EXCL 此标志一般结合 O_CREAT 标志一起使用, 用于专门创建文件。 在 flags 参数同时使用到了 O_CREAT 和 O_EXCL 标志的情况下,如果 pathname 参数 指向的文件已经存在,则 open 函数返回错 误。
O_NOFOLLOW 如果 pathname 参数指向的是一个符号链接, 将不对其进行解引用,直接返回错误。

c#

2.2 lseek 读写偏移量

当调用 read() 或 write() 函数对文件进行读写操作时,会从当前读写位置偏移量开始进行数据读写。

7系列

读写偏移量用于指示 read()或 write()函数操作时文件的起始位置,会以相对于文件头部的位置偏移量来表示,文件第一个字节数据的位置偏移量为 0。

MSP432

实例:

gitlab服务器搭建

起始点向右偏移一格

TDesign

off_t off = lseek(fd1, 1, SEEK_SET);
if (-1 == off)
    return -1;

第三章 深入探究文件 I/O

3.1 Linux 系统如何管理文件

3.1.1 静态文件与inode

文件储存在硬盘上,硬盘的最小存储单位叫做“扇区”(Sector),每个扇区储存 512 字节(相当于 0.5KB),操作系统读取硬盘的时候,一次性连续读取多个扇区,即一次 性读取一个“块”(block)。“块”的大小,最常见的是 4KB,即连续八个 sector 组成一个 block。

英雄算法联盟

safari

3.1.2 文件打开时的状态

磁盘、硬盘、U 盘等存储设备基本都是 Flash 块设备,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的读写操作非常不灵活;而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活。所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件。

ERC721

在 Linux 系统中,内核会为每个进程设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写 PCB)。

技巧

算法伪代码

3.2 返回错误处理与errno

用文字返回具体原因

节日

3.2.1 strerror函数

/* 打开文件 */
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
    printf("Error: %s\n", strerror(errno));
    return -1;
}

3.2.2 perror函数

/* 打开文件 */
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
    perror("open error");
    return -1;
}

3.3 exit_exit_Exit

_exit()_Exit() 等价,都是系统调用

用Python实现简单人脸识别

执行 exit()会执行一些清理工作,最后调用_exit()函数。

业界资讯

9.9.2

mysql 索引优化

3.4 空洞文件

3.4.1 概念

譬如有一个 test_file,该文件的大小是 4K(也就是 4096 个字节),如果通过 lseek 系统调用将该文件的读写偏移量移动到偏移文件头部 6000 个字节处,接下来使用 write() 函数对文件进行写入操作。也就意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件。

aggregate

文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的。

程序设计

应用场景:迅雷下载文件,可以从不同的地址同时写入,就达到了多线程的优势;创建虚拟机时,你给虚拟机分配了 100G 的磁盘空间,开始也不过只 用了 3、4G 的磁盘空间。

fabric

3.4.2 实验测试

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
    int fd;
    int ret;
    char buffer[1024];
    int i;
    /* 打开文件 */
    fd = open("./hole_file", O_WRONLY,
        S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }
    /* 将文件读写位置移动到偏移文件头 4096 个字节(4K)处 */
    ret = lseek(fd, 4096, SEEK_SET);
    if (-1 == ret) {
        perror("lseek error");
        goto err;
    }
    /* 初始化 buffer 为 0xFF */
    memset(buffer, 0xFF, sizeof(buffer));
    /* 循环写入 4 次,每次写入 1K */
    for (i = 0; i < 4; i++) {
        ret = write(fd, buffer, sizeof(buffer));
        if (-1 == ret) {
            perror("write error");
            goto err;
        }
    }
    ret = 0;
err:
    /* 关闭文件 */
    close(fd);
    exit(ret);
}

SaaS

3.5 O_APPEND和O_TRUNC标志

3.5.1 O_TRUNC标志

调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0。

C语言深度解剖

fd = open("./test_file", O_WRONLY | O_TRUNC);

敏捷流程

3.5.2 O_APPEND标志

每次写入数据都是从文件末尾开始。

fd = open("./test_file", O_RDWR | O_APPEND);

3.6 多次打开同一个文件

多个副本。

在这里插入图片描述

3.7 复制文件描述符

3.7.1 dup函数

复制得到的文件描述符和旧的文件描述符拥有相同的权限

复制得到的文件描述符与旧的文件描述符指向的是同一个文件表

两个复制的文件描述符写入操作时将在各自的末尾写入;开两个写入时将覆盖另一个。

如fd1、fd2开两个,fd3复制于fd1。fd2将覆盖fd1的线程。

/* 循环写入数据 */
for (i = 0; i < 4; i++) {
    ret = write(fd1, buffer1, sizeof(buffer1));
    if (-1 == ret) {
        perror("write error");
        goto err2;
    }
    ret = write(fd3, buffer3, sizeof(buffer3));
    if (-1 == ret) {
        perror("write error");
        goto err2;
    }
    ret = write(fd2, buffer2, sizeof(buffer2));
    if (-1 == ret) {
        perror("write error");
        goto err2;
    }
}

结果为

eeff5566eeff5566eeff5566eeff556699883344aabbccdd99883344aabbcc

3.7.2 dup2函数

可以手动指定文件描述符

fd2 = dup2(fd1, 100);

3.8 文件共享

同一个文件被多个独立的读写体同时进行 IO 操作。

常见的三种文件共享的实现方式

(1)同一个进程中多次调用 open 函数打开同一个文件,各数据结构之间的关系如下图所示:

在这里插入图片描述

(2)不同进程中分别使用 open 函数打开同一个文件,其数据结构关系图如下所示:

在这里插入图片描述

(3)同一个进程中通过 dup(dup2)函数对文件描述符进行复制,其数据结构关系如下图所示:

在这里插入图片描述

3.9 原子操作与竞争冒险

多个不同的进程就 有可能对同一个文件进行 IO 操作,此时该文件便是它们的共享资源,它们共同操作着同一份文件。多进程环境下可能会导致竞争冒险。

3.9.1 竞争冒险简介

一大堆例子

3.9.2 原子操作

(1)O_APPEND 实现原子操作
(2)pread()和 pwrite()

都是系统调用

所以调用 pread 相当于调用 lseek 后再调用 read

(3)创建一个文件

3.10 fcntl和ioctl

3.10.1 fcntl函数

fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作,譬如复制一个文件描述符(与 dup、 dup2 作用相同)、获取/设置文件描述符标志、获取/设置文件状态标志等,类似于一个多功能文件描述符管理工具箱。

(1)复制文件描述符
/* 使用 fcntl 函数复制一个文件描述符 */
fd2 = fcntl(fd1, F_DUPFD, 0);
(2)获取/设置文件状态标志
/* 获取文件状态标志 */
flag = fcntl(fd, F_GETFL);
/* 设置文件状态标志,添加 O_APPEND 标志 */
ret = fcntl(fd, F_SETFL, flag | O_APPEND);

3.11 截断文件

使用系统调用 truncate()或 ftruncate()可将普通文件截断为指定字节长度。

这两个函数的区别在于:ftruncate()使用文件描述符 fd 来指定目标文件,而 truncate()则直接使用文件路径 path 来指定目标文件,其功能一样。

/* 使用 ftruncate 将 file1 文件截断为长度 0 字节 */
if (0 > ftruncate(fd, 0)) {
    perror("ftruncate error");
    exit(-1);
}
/* 使用 truncate 将 file2 文件截断为长度 1024 字节 */
if (0 > truncate("./file2", 1024)) {
    perror("truncate error");
    exit(-1);
}

第四章 标准I/O库

本章介绍标准 I/O 库

4.1 标准 I/O 库简介

标准 I/O 库函数是构建于文件 I/O(open()、read()、write()、lseek()、close()等)这些系统调用之上的

4.2 FILE指针

FILE 指针的作用相当于文件描述符,只不过 FILE 指针用于标准 I/O 库函数中、而文件描述符则用于文件 I/O 系统调用中。

FILE 是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,包括用于实际 I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等。FILE 数据结构定义在标准 I/O 库函数头文件 stdio.h 中。

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

标准输出文件和标准错误文件都对应终端的屏幕,而标准输入文件则对应于键盘。

文件描述符 0 代表标准输入、1 代表标准输出、2 代表标准错误

/* Standard file descriptors. */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO1 /* Standard output. */
#define STDERR_FILENO2 /* Standard error output. */

在标准 I/O 中,可以使用 stdin、stdout、stderr 来表示标准输入、标准输出和标准错误。

4.4 打开关闭文件

4.4.1 打开文件fopen()

#include <stdio.h>
FILE *fopen(const char *path, const char *mode);

没有指定文件的话会新建一个文件

mode 说明
r 以只读方式打开文件。
r+ 以可读、可写方式打开文件。
w 以只写方式打开文件,如果参数 path 指定的文件存在,将文件长度截断为 0;如果指定文件不存在 则创建该文件。
w+ 以可读、可写方式打开文件,如果参数 path 指定的文件存在,将文件长度截断为 0;如果指定文件 不存在则创建该文件。
a 以只写方式打开文件,打开以进行追加内容(在 文件末尾写入),如果文件不存在则创建该文件。
a+ 以可读、可写方式打开文件,以追加方式写入 (在文件末尾写入),如果文件不存在则创建该文件。
//使用只读方式打开文件
fopen(path, "r");

4.4.2 fclose()关闭文件

调用 fclose()库函数可以关闭一个由 fopen()打开的文件

#include <stdio.h>
int fclose(FILE *stream);

4.5 读文件和写文件

当使用 fopen()库函数打开文件之后,接着我们便可以使用 fread()和 fwrite()库函数对文件进行读、写操 作了,函数原型如下所示:

#include <stdio.h>
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);
库函数 fread()用于读取文件数据,其参数和返回值含义如下:
ptr:fread()将读取到的数据存放在参数 ptr 指向的缓冲区中;
size:fread()从文件读取 nmemb 个数据项,每一个数据项的大小为 size 个字节,所以总共读取的数据大
小为 nmemb * size 个字节。
nmemb:参数 nmemb 指定了读取数据项的个数。
stream:FILE 指针。
返回值:调用成功时返回读取到的数据项的数目(数据项数目并不等于实际读取的字节数,除非参数
size 等于 1);如果发生错误或到达文件末尾,则 fread()返回的值将小于参数 nmemb,那么到底发生了错误
还是到达了文件末尾,fread()不能区分文件结尾和错误,究竟是哪一种情况,此时可以使用 ferror()或 feof()
函数来判断,具体参考 4.7 小节内容的介绍。
库函数 fwrite()用于将数据写入到文件中,其参数和返回值含义如下:
ptr:将参数 ptr 指向的缓冲区中的数据写入到文件中。
size:参数 size 指定了每个数据项的字节大小,与 fread()函数的 size 参数意义相同。
nmemb:参数 nmemb 指定了写入的数据项个数,与 fread()函数的 nmemb 参数意义相同。
stream:FILE 指针。
返回值:调用成功时返回写入的数据项的数目(数据项数目并不等于实际写入的字节数,除非参数 size
等于 1);如果发生错误,则 fwrite()返回的值将小于参数 nmemb(或者等于 0)。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char buf[] = "Hello World!\n";
    FILE *fp = NULL;
    /* 打开文件 */
    if (NULL == (fp = fopen("./test_file", "w")))
    {
        perror("fopen error");
        exit(-1);
    }
    printf("文件打开成功!\n");
    /* 写入数据 */
    if (sizeof(buf) >
        fwrite(buf, 1, sizeof(buf), fp))
    {
        printf("fwrite error\n");
        fclose(fp);
        exit(-1);
    }
    printf("数据写入成功!\n");
    /* 关闭文件 */
    fclose(fp);
    exit(0);
}

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char buf[50] = {0};
    FILE *fp = NULL;
    int size;
    /* 打开文件 */
    if (NULL == (fp = fopen("./test_file", "r")))
    {
        perror("fopen error");
        exit(-1);
    }
    printf("文件打开成功!\n");
    /* 读取数据 */
    if (12 > (size = fread(buf, 1, 12, fp)))
    {
        if (ferror(fp))
        { //使用 ferror 判断是否是发生错误
            printf("fread error\n");
            fclose(fp);
            exit(-1);
        }
        /* 如果未发生错误则意味着已经到达了文件末尾 */
    }
    printf("成功读取%d 个字节数据: %s\n", size, buf);
    /* 关闭文件 */
    fclose(fp);
    exit(0);
}

4.6 fseek定位

4.6.1 fseek()

库函数 fseek()的作用类似于 2.7 小节所学习的系统调用 lseek(),用于设置文件读写位置偏移量,lseek() 用于文件 I/O,而库函数 fseek()则用于标准 I/O,其函数原型如下所示:

#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);

调用库函数 fread()、fwrite()读写文件时,文件的读写位置偏移量会自动递增,使用 fseek()可手动设置 文件当前的读写位置偏移量。

//譬如将文件的读写位置移动到文件开头处:
fseek(file, 0, SEEK_SET);
//将文件的读写位置移动到文件末尾:
fseek(file, 0, SEEK_END);
//将文件的读写位置移动到 100 个字节偏移量处:
fseek(file, 100, SEEK_SET);

4.6.2 ftell()

库函数 ftell()可用于获取文件当前的读写位置偏移量,其函数原型如下所示:

#include <stdio.h>
long ftell(FILE *stream);

参数 stream 指向对应的文件,函数调用成功将返回当前读写位置偏移量;调用失败将返回-1,并会设置 errno 以指示错误原因。

我们可以通过 fseek()和 ftell()来计算出文件的大小

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp = NULL;
    int ret;
    /* 打开文件 */
    if (NULL == (fp = fopen("./hello.c", "r")))
    {
        perror("fopen error");
        exit(-1);
    }
    printf("文件打开成功!\n");
    /* 将读写位置移动到文件末尾 */
    if (0 > fseek(fp, 0, SEEK_END))
    {
        perror("fseek error");
        fclose(fp);
        exit(-1);
    }
    /* 获取当前位置偏移量 */
    if (0 > (ret = ftell(fp)))
    {
        perror("ftell error");
        fclose(fp);
        exit(-1);
    }
    printf("文件大小: %d 个字节\n", ret);
    /* 关闭文件 */
    fclose(fp);
    exit(0);
}

在这里插入图片描述

4.7 检查或复位状态

调用 fread()读取数据时,如果返回值小于参数 nmemb 所指定的值,表示发生了错误或者已经到了文件 末尾(文件结束 end-of-file),但 fread()无法具体确定是哪一种情况;在这种情况下,可以通过判断错误标 志或 end-of-file 标志来确定具体的情况。

4.7.1 feof()函数

当文件的读写位置移动到了文件末尾时,end-of-file 标志将会被设置。

#include <stdio.h>
int feof(FILE *stream);
if (feof(file)) {
    /* 到达文件末尾 */
}
else {
    /* 未到达文件末尾 */
}

4.7.2 ferror()函数

库函数 ferror()用于测试参数 stream 所指文件的错误标志,如果错误标志被设置了,则调用 ferror()函数将返回一个非零值,如果错误标志没有被设置,则返回 0。

#include <stdio.h>
int ferror(FILE *stream);

当对文件的 I/O 操作发生错误时,错误标志将会被设置。

if (ferror(file)) {
/* 发生错误 */
}
else {
/* 未发生错误 */
}

4.7.3 clearerr()函数

库函数 clearerr()用于清除 end-of-file 标志和错误标志,当调用 feof()或 ferror()校验这些标志后,通常需要清除这些标志,避免下次校验时使用到的是上一次设置的值,此时可以手动调用 clearerr()函数清除标志。

#include <stdio.h>
void clearerr(FILE *stream);
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp = NULL;
    char buf[20] = {0};
    /* 打开文件 */
    if (NULL == (fp = fopen("./hello.c", "r")))
    {
        perror("fopen error");
        exit(-1);
    }
    printf("文件打开成功!\n");
    /* 将读写位置移动到文件末尾 */
    if (0 > fseek(fp, 0, SEEK_END))
    {
        perror("fseek error");
        fclose(fp);
        exit(-1);
    }
    /* 读文件 */
    if (0 == fread(buf, 1, 10, fp))
    {
        if (feof(fp))
            printf("end-of-file 标志被设置,已到文件末尾!\n");
        clearerr(fp); //清除标志
    }
    /* 关闭文件 */
    fclose(fp);
    exit(0);
}

在这里插入图片描述

4.8 格式化I/O

除了 printf()之外,格式化输出还包括:fprintf()、 dprintf()、sprintf()、snprintf()这 4 个库函数。

格式化输入包括:scanf()、 fscanf()、sscanf()这三个库函数

4.8.1 格式化输出

C 库函数提供了 5 个格式化输出函数,包括:printf()、fprintf()、dprintf()、sprintf()、snprintf(),其函数 定义如下所示:

#include <stdio.h>
int 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, ...);

fprintf()可将格式化数据写入到由 FILE 指针指定的文件中,譬如将字符串“Hello World”写入到标准错误

dprintf()可将格式化数据写入到由文件描述符 fd 指定的文件中,譬如将字符串“Hello World”写入到标 准错误

sprintf()函数将格式化数据存储在由参数 buf 所指定的缓冲区中,sprintf()函数可能会造成由参数 buf 指定的缓冲区溢出

snprintf()使用参数 size 显式的指定缓冲区的大小,如果写入到缓冲区的字节数大于参数 size 指定的大 小,超出的部分将会被丢弃


格式控制字符串 format

//[]表示可选
%[flags][width][.precision][length]type
flags:标志,可包含 0 个或多个标志;
width:输出最小宽度,表示转换后输出字符串的最小宽度;
precision:精度,前面有一个点号" . ";
length:长度修饰符;
type:转换类型,指定待转换数据的类型。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
    char buf[50] = {0};
    printf("%d (%s) %d (%s)\n", 520, "我爱你", 1314, "一生一世");
    fprintf(stdout, "%d (%s) %d (%s)\n", 520, "我爱你", 1314, "一生一世");
    dprintf(STDOUT_FILENO, "%d (%s) %d (%s)\n", 520, "我爱你", 1314, "一生一世");
    sprintf(buf, "%d (%s) %d (%s)\n", 520, "我爱你", 1314, "一生一世");
    printf("%s", buf);
    memset(buf, 0x00, sizeof(buf));
    snprintf(buf, sizeof(buf), "%d (%s) %d (%s)\n", 520, "我爱你", 1314, "一生一世");
    printf("%s", buf);
    exit(0);
}

在这里插入图片描述

4.8.2 格式化输入

C 库函数提供了 3 个格式化输入函数,包括:scanf()、fscanf()、sscanf(),其函数定义如下所示:

#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
scanf("%hd", var); //匹配 short int 类型数据
scanf("%hhd", var); //匹配 signed char 类型数据
scanf("%ld", var); //匹配 long int 类型数据
scanf("%f", var); //匹配 float 类型数据
scanf("%lf", var); //匹配 double 类型数据
scanf("%Lf", var); //匹配 long double 类型数据

输入字符串

//1 
char str[20];
scanf("%s", str);
//2
char *str;
scanf("%ms", &str);
free(str);

4.9 I/O缓冲

出于速度和效率的考虑,系统 I/O 调用(即文件 I/O,open、read、write 等)和标准 C 语言库 I/O 函数 (即标准 I/O 函数)在操作磁盘文件时会对数据进行缓冲,本小节将讨论文件 I/O 和标准 I/O 这两种 I/O 方式的数据缓冲问题,并讨论其对应用程序性能的影响。

除此之外,本小节还讨论了屏蔽或影响缓冲的一些技术手段,以及直接 I/O 技术—绕过内核缓冲直接访 问磁盘硬件。

4.9.1 文件I/O的内核缓冲

read()和 write()系统调用在进行文件读写操作的时候并不会直接访问磁盘设备,而是仅仅在用户空间缓 冲区和内核缓冲区(kernel buffer cache)之间复制数据。

如:系统调用 write() 与磁盘操作并不是同步的,write()函数并不会等待数据真正写入到磁盘之后再返回。

前面提到,当调用 write()之后,内核稍后会将数据写入到磁盘设备中,具体是什么时间点写入到磁盘, 这个其实是不确定的,由内核根据相应的存储算法自动判断。

4.9.2 刷新文件I/O的内核缓冲区

强制将文件 I/O 内核缓冲区中缓存的数据写入(刷新)到磁盘设备中。如:拔掉 U 盘之前,需要执行 sync 命令进行同步操作,这个同步操作其实就是将文件 I/O 内核缓冲区中的数据 更新到 U 盘硬件设备,所以如果在没有执行 sync 命令时拔掉 U 盘,很可能就会导致拷贝到 U 盘中的文件 遭到破坏。

控制文件 I/O 内核缓冲的系统调用

Linux 中提供了一些系统调用可用于控制文件 I/O 内核缓冲,包括系统调用 sync()、syncfs()、fsync()以 及 fdatasync()。

㈠、fsync()函数

系统调用 fsync()将参数 fd 所指文件的内容数据和元数据写入磁盘,只有在对磁盘设备的写入操作完成 之后,fsync()函数才会返回,其函数原型如下所示:

#include <unistd.h>
int fsync(int fd);

参数 fd 表示文件描述符,函数调用成功将返回 0,失败返回-1 并设置 errno 以指示错误原因

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 4096
#define READ_FILE "./rfile"
#define WRITE_FILE "./wfile"
static char buf[BUF_SIZE];
int main(void)
{
    int rfd, wfd;
    size_t size;
    /* 打开源文件 */
    rfd = open(READ_FILE, O_RDONLY);
    if (0 > rfd)
    {
        perror("open error");
        exit(-1);
    }
    /* 打开目标文件 */
    wfd = open(WRITE_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0664);
    if (0 > wfd)
    {
        perror("open error");
        exit(-1);
    }
    /* 拷贝数据 */
    while (0 < (size = read(rfd, buf, BUF_SIZE)))
        write(wfd, buf, size);
    /* 对目标文件执行 fsync 同步 */
    fsync(wfd);
    /* 关闭文件退出程序 */
    close(rfd);
    close(wfd);
    exit(0);
}

代码主要就是拷贝完成之后调用 fsync()函数,对目标文件的数据进行了同步操作,整 个操作完成之后 close 关闭源文件和目标文件、退出程序。

㈡、fdatasync()函数

系统调用 fdatasync()与 fsync()类似,不同之处在于 fdatasync()仅将参数 fd 所指文件的内容数据写入磁 盘,并不包括文件的元数据

#include <unistd.h>
int fdatasync(int fd);

㈢、sync()函数

系统调用 sync()会将所有文件 I/O 内核缓冲区中的文件内容数据和元数据全部更新到磁盘设备中

它不是对某一个指定的文件进行数据更新,而是刷新所有文件 I/O 内核缓 冲区。

#include <unistd.h>
void sync(void);

在 Linux实现中,调用 sync()函数仅在所有数据已经写入到磁盘设备之后才会返回


控制文件 I/O 内核缓冲的标志

㈠、O_DSYNC 标志

在调用 open()函数时,指定 O_DSYNC 标志,其效果类似于在每个 write()调用之后调用 fdatasync()函数 进行数据同步。譬如:

fd = open(filepath, O_WRONLY | O_DSYNC);

㈡、O_SYNC 标志

在调用 open()函数时,指定 O_SYNC 标志,使得每个 write()调用都会自动将文件内容数据和元数据刷 新到磁盘设备中,其效果类似于在每个 write()调用之后调用 fsync()函数进行数据同步,譬如:

fd = open(filepath, O_WRONLY | O_SYNC);

对性能的影响

在程序中频繁调用 fsync()、fdatasync()、sync()(或者调用 open 时指定 O_DSYNC 或 O_SYNC 标志) 对性能的影响极大,大部分的应用程序是没有这种需求的,所以在大部分应用程序当中基本不会使用到。

4.9.3 直接I/O:绕过内核缓冲

从 Linux 内核 2.4 版本开始,Linux 允许应用程序在执行文件 I/O 操作时绕过内核缓冲区,从用户空间 直接将数据传递到文件或磁盘设备,把这种操作也称为直接 I/O(direct I/O)或裸 I/O(raw I/O)。

直接 I/O 只在一些特定的需求场合,譬如磁盘速率测试工具、数据库系统等

我们可针对某一文件或块设备执行直接 I/O,要做到这一点,需要在调用 open()函数打开文件时,指定 O_DIRECT 标志,该标志至 Linux 内核 2.4.10 版本开始生效,譬如:

fd = open(filepath, O_WRONLY | O_DIRECT);

直接 I/O 的对齐限制

  • 应用程序中用于存放数据的缓冲区,其内存起始地址必须以块大小的整数倍进行对齐;
  • 写文件时,文件的位置偏移量必须是块大小的整数倍;
  • 写入到文件的数据大小必须是块大小的整数倍。

直接 I/O 测试与普通 I/O 对比测试

//直接I/O
/** 使用宏定义 O_DIRECT 需要在程序中定义宏_GNU_SOURCE
** 不然提示 O_DIRECT 找不到 **/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
/** 定义一个用于存放数据的 buf,起始地址以 4096 字节进行对其 **/
static char buf[8192] __attribute((aligned(4096)));
int main(void)
{
	int fd;
	int count;
	/* 打开文件 */
	fd = open("./test_file",
		O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT,
		0664);
	if (0 > fd) {
		perror("open error");
		exit(-1);
	}
	/* 写文件 */
	count = 10000;
	while (count--) {
		if (4096 != write(fd, buf, 4096)) {
			perror("write error");
			exit(-1);
		}
	}
	/* 关闭文件退出程序 */
	close(fd);
	exit(0);
}

在这里插入图片描述

4.9.4 stdio缓冲

标准 I/O(fopen、fread、fwrite、fclose、fseek 等)是 C 语言标准库函数,而文件 I/O(open、read、write、 close、lseek 等)是系统调用,虽然标准 I/O 是在文件 I/O 基础上进行封装而实现,但在效率、性能上标准 I/O 要优于文件 I/O,其原因在于标准 I/O 实 现维护了自己的缓冲区,我们把这个缓冲区称为 stdio 缓冲区

前面提到了文件 I/O 内核缓冲,这是由内核维护的缓冲区,而标准 I/O 所维护的 stdio 缓冲是用户空间 的缓冲区,当应用程序中通过标准 I/O 操作磁盘文件时,为了减少调用系统调用的次数,标准 I/O 函数会将 用户写入或读取文件的数据缓存在 stdio 缓冲区,然后再一次性将 stdio 缓冲区中缓存的数据通过调用系统 调用 I/O(文件 I/O)写入到文件 I/O 内核缓冲区或者拷贝到应用程序的 buf 中。

通过这样的优化操作,当操作磁盘文件时,在用户空间缓存大块数据以减少调用系统调用的次数,使得 效率、性能得到优化。使用标准 I/O 可以使编程者免于自行处理对数据的缓冲,无论是调用 write()写入数 据、还是调用 read()读取数据。


对 stdio 缓冲进行设置

C 语言提供了一些库函数可用于对标准 I/O 的 stdio 缓冲区进行相关的一些设置,包括 setbuf()、setbuffer() 以及 setvbuf()。

㈠、setvbuf()函数

调用 setvbuf()库函数可以对文件的 stdio 缓冲区进行设置,譬如缓冲区的缓冲模式、缓冲区的大小、起 始地址等。其函数原型如下所示:

#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

mode:参数 mode 用于指定缓冲区的缓冲类型,可取值如下:

  • _IONBF:不对 I/O 进行缓冲(无缓冲)。意味着每个标准 I/O 函数将立即调用 write()或者 read(), 并且忽略 buf 和 size 参数,可以分别指定两个参数为 NULL 和 0。标准错误 stderr 默认属于这一种 类型,从而保证错误信息能够立即输出。
  • _IOLBF:采用行缓冲 I/O。在这种情况下,当在输入或输出中遇到换行符"\n"时,标准 I/O 才会执 行文件 I/O 操作。
  • _IOFBF:采用全缓冲 I/O。在这种情况下,在填满 stdio 缓冲区后才进行文件 I/O 操作。

㈡、setbuf()函数

#include <stdio.h>
void setbuf(FILE *stream, char *buf);

㈢、setbuffer()函数

#include <stdio.h>
void setbuffer(FILE *stream, char *buf, size_t size);

标准输出 printf()的行缓冲模式测试

标准输出默认采用的是行缓冲模式

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("Hello World!\n");
    printf("Hello World!");
    for (;;)
        sleep(1);
}

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    /* 将标准输出设置为无缓冲模式 */
    if (setvbuf(stdout, NULL, _IONBF, 0))
    {
        perror("setvbuf error");
        exit(0);
    }
    printf("Hello World!\n");
    printf("Hello World!");
    for (;;)
        sleep(1);
}

在这里插入图片描述

刷新 stdio 缓冲区

无论我们采取何种缓冲模式,在任何时候都可以使用库函数 fflush()来强制刷新

#include <stdio.h>
int fflush(FILE *stream);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("Hello World!\n");
    printf("Hello World!");
    fflush(stdout); //刷新标准输出 stdio 缓冲区

    for (;;)
        sleep(1);
}

在这里插入图片描述

在一些其它的情况下,也会自动刷新 stdio 缓冲区,譬如当文件关闭时、程序退出时,

㈠、关闭文件时刷新 stdio 缓冲区

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("Hello World!\n");
    printf("Hello World!");
    fclose(stdout); //关闭标准输出
    for (;;)
        sleep(1);
}

在这里插入图片描述

㈡、程序退出时刷新 stdio 缓冲区

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("Hello World!\n");
    printf("Hello World!");
    exit(0);
    for (;;)
        sleep(1);
}

在这里插入图片描述

但是_exit()终止程序则不会刷新

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("Hello World!\n");
    printf("Hello World!");
    _exit(0);
    for (;;)
        sleep(1);
}

在这里插入图片描述

4.9.5 I/O缓冲小节

在这里插入图片描述

4.10 文件描述符与FILE指针互转

库函数 fileno()可以将标准 I/O 中使用的 FILE 指针转换为文件 I/O 中所使用的文件描述符,而 fdopen() 则进行着相反的操作,其函数原型如下所示:

#include <stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);

当混合使用文件 I/O 和标准 I/O 时,需要特别注意缓冲的问题,文件 I/O 会直接将数据写入到内核缓冲 区进行高速缓存,而标准 I/O 则会将数据写入到 stdio 缓冲区,之后再调用 write()将 stdio 缓冲区中的数据写 入到内核缓冲区。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("print");
    write(STDOUT_FILENO, "write\n", 6);
    exit(0);
}

在这里插入图片描述

第五章 文件属性与目录

本章将抛开文件 I/O 相关话题,来讨论 Linux 文件系统的其它特性以及文件相关属性

5.1 Linux系统中的文件类型

在 Windows 操作系统下打开文件,首先会识别文件名后 缀得到该文件的类型,然后再使用相应的调用相应的程序去打开它;但是在 Linux 系统下,并不会通过文件后缀名来识别一个文件的类型

5.1.1 普通文件

普通文件可以分为两大类:文本文件和二进制文件。

  • 文本文件:而文本文件中的数字应该被理解为这个数字所对应的 ASCII 字符码,譬如常见的.c、.h、.sh、.txt 等 这些都是文本文件,文本文件的好处就是方便人阅读、浏览以及编写。

  • 二进制文件:譬如 Linux 系统下的可执行文件、C 代码编译之后得到的.o 文 件、.bin 文件等都是二进制文件。

在 Linux 系统下,可以通过 stat 命令或者 ls -l 命令来查看文件类型

ls -l 结果的第一个字符含义如下:

字符 含义
普通文件
d 目录文件
c 字符设备文件
b 块设备文件
l 符号链接文件
s 套接字文件
p 管道文件

在这里插入图片描述

在这里插入图片描述

5.1.2 目录文件

目录(directory)就是文件夹,文件夹在 Linux 系统中也是一种文件,是一种特殊文件

可以用 vi 打开目录文件

在这里插入图片描述

5.1.3 字符设备文件和块设备文件

Linux 系统下,一切皆文件,也包括各种硬件设备。

设备文件(字符设备文件、块设备文件) 对应的是硬件设备,在 Linux 系统中,硬件设备会对应到一个设备文件,应用程序通过对设备文件的读写来操控、使用硬件设备,譬如 LCD 显示屏、串口、音频、按键等

Linux 系统中,可将硬件设备分为字符设备和块设备,所以就有了字符设备文件和块设备文件两种文件类型。

虽然有设备文件,但是设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中, 而是由文件系统虚拟出来的,一般是由内存来维护,当系统关机时,设备文件都会消失;字符设备文件一般存放在 Linux 系统/dev/目录下,所以/dev 也称为虚拟文件系统 devfs。

在这里插入图片描述

5.1.4 符号链接文件

符号链接文件(link)类似于 Windows 系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另 一个文件路径,当对符号链接文件进行操作时,系统根据情况会对这个操作转移到它指向的文件上去,而不 是对它本身进行操作,譬如,读取一个符号链接文件内容时,实际上读到的是它指向的文件的内容。

5.1.5 管道文件

管道文件(pipe)主要用于进程间通信

5.1.6 套接字文件

套接字文件(socket)也是一种进程间通信的方式,与管道文件不同的是,它们可以在不同主机上的进 程间通信,实际上就是网络通信

5.1.7 总结

普通文件是最常见的文件类型; 目录也是一种文件类型; 设备文件对应于硬件设备; 符号链接文件类似于 Windows 的快捷方式; 管道文件用于进程间通信; 套接字文件用于网络通信。

5.2 stat函数

Linux 下可以使用 stat 命令查看文件的属性,stat 函数是 Linux 中的系统调用,用于获取文件相关的信息

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *buf);

pathname:用于指定一个需要查看属性的文件路径。

struct stat 类型指针,用于指向一个 struct stat 结构体变量。

返回值:成功返回 0;失败返回-1,并设置 error。

5.2.1 struct stat结构体

structstat 是内核定义的一个结构体,在头文件中申明,所以可以在应用层使用,这个结构体 中的所有元素加起来构成了文件的属性信息,结构体内容如下所示:

struct stat
{
	dev_t st_dev; /* 文件所在设备的 ID */
	ino_t st_ino; /* 文件对应 inode 节点编号 */
	mode_t st_mode; /* 文件对应的模式 */
	nlink_t st_nlink; /* 文件的链接数 */
	uid_t st_uid; /* 文件所有者的用户 ID */
	gid_t st_gid; /* 文件所有者的组 ID */
	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; /* 文件状态最后被改变的时间 */
};

5.2.2 st_mode变量

st_mode 是 structstat 结构体中的一个成员变量,是一个 32 位无符号整形数据,该变量记录了文件的类 型、文件的权限这些信息

在这里插入图片描述

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

譬如,判断文件所有者对该文件是否具有可执行权限,可以通过以下方法测试(假设 st 是 structstat 类 型变量):

if (st.st_mode & S_IXUSR) {
//有权限
} else {
//无权限
}

这里我们重点来看看“文件类型”这 4 个 bit 位,这 4 个 bit 位用于描述该文件的类型,譬如该文件是 普通文件、还是链接文件、亦或者是一个目录等,那么就可以通过这 4 个 bit 位数据判断出来,如下所示:

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(管道文件)
/* 判断是不是普通文件 */
if ((st.st_mode & S_IFMT) == S_IFREG) {
/* 是 */
}
/* 判断是不是链接文件 */
if ((st.st_mode & S_IFMT) == S_IFLNK) {
/* 是 */
}

S_IFMT 宏是文件类型字段位掩码:

S_IFMT 0170000

除了这样判断之外,我们还可以使用 Linux 系统封装好的宏来进行判断,如下所示(m 是 st_mode 变 量):

S_ISREG(m) #判断是不是普通文件,如果是返回 true,否则返回 false
S_ISDIR(m) #判断是不是目录,如果是返回 true,否则返回 false
S_ISCHR(m) #判断是不是字符设备文件,如果是返回 true,否则返回 false
S_ISBLK(m) #判断是不是块设备文件,如果是返回 true,否则返回 false
S_ISFIFO(m) #判断是不是管道文件,如果是返回 true,否则返回 false
S_ISLNK(m) #判断是不是链接文件,如果是返回 true,否则返回 false
S_ISSOCK(m) #判断是不是套接字文件,如果是返回 true,否则返回 false
/* 判断是不是普通文件 */
if (S_ISREG(st.st_mode)) {
/* 是 */
}
/* 判断是不是目录 */
if (S_ISDIR(st.st_mode)) {
/* 是 */
}

样例:

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

int main()
{
    struct stat st;
    stat("./hello.c", &st);
    if (S_ISREG(st.st_mode))
        puts("Yes, hello.c 是普通文件");
    else
        puts("No");
    return 0;
}

在这里插入图片描述

5.2.3 struct timespec结构体

该结构体定义在头文件中,是 Linux 系统中时间相关的结构体。

struct timespec
{
 time_t tv_sec; /* 秒 */
 syscall_slong_t tv_nsec; /* 纳秒 */
};

time_t 其实指的 就是 long int 类型

在 Linux 系统中,time_t 时间指的是一个时间段,从某一个时间点到某一个时间点所经过的秒数

5.2.4 练习

(1)获取文件的 inode 节点编号以及文件大小,并将它们打印出来。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    struct stat file_stat;
    int ret;
    /* 获取文件属性 */
    ret = stat("./test_file", &file_stat);
    if (-1 == ret)
    {
        perror("stat error");
        exit(-1);
    }
    /* 打印文件大小和 inode 编号 */
    printf("file size: %ld bytes\n"
           "inode number: %ld\n",
           file_stat.st_size,
           file_stat.st_ino);
    exit(0);
}

在这里插入图片描述

(2)获取文件的类型,判断此文件对于其它用户(Other)是否具有可读可写权限。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    struct stat file_stat;
    int ret;
    /* 获取文件属性 */
    ret = stat("./test_file", &file_stat);
    if (-1 == ret)
    {
        perror("stat error");
        exit(-1);
    }
    /* 判读文件类型 */
    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;
    }
    printf("\n");
    /* 判断该文件对其它用户是否具有读权限 */
    if (file_stat.st_mode & S_IROTH)
        printf("Read: Yes\n");
    else
        printf("Read: No\n");
    /* 判断该文件对其它用户是否具有写权限 */
    if (file_stat.st_mode & S_IWOTH)
        printf("Write: Yes\n");
    else
        printf("Write: No\n");
    exit(0);
}

在这里插入图片描述

(3)获取文件的时间属性,包括文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后 被改变的时间,并使用字符串形式将其打印出来,包括时间和日期、表示形式自定。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    struct stat file_stat;
    struct tm file_tm;
    char time_str[100];
    int ret;
    /* 获取文件属性 */
    ret = stat("./test_file", &file_stat);
    if (-1 == ret)
    {
        perror("stat error");
        exit(-1);
    }
    /* 打印文件最后被访问的时间 */
    localtime_r(&file_stat.st_atim.tv_sec, &file_tm); //结构体的结构体
    strftime(time_str, sizeof(time_str),
             "%Y-%m-%d %H:%M:%S", &file_tm);
    printf("time of last access: %s\n", time_str);
    /* 打印文件内容最后被修改的时间 */
    localtime_r(&file_stat.st_mtim.tv_sec, &file_tm);
    strftime(time_str, sizeof(time_str),
             "%Y-%m-%d %H:%M:%S", &file_tm);
    printf("time of last modification: %s\n", time_str);
    /* 打印文件状态最后改变的时间 */
    localtime_r(&file_stat.st_ctim.tv_sec, &file_tm);
    strftime(time_str, sizeof(time_str),
             "%Y-%m-%d %H:%M:%S", &file_tm);
    printf("time of last status change: %s\n", time_str);
    exit(0);
}

在这里插入图片描述

5.3 fstat和lstat函数

前面给大家介绍了 stat 系统调用,起始除了 stat 函数之外,还可以使用 fstat 和 lstat 两个系统调用来获 取文件属性信息。fstat、lstat 与 stat 的作用一样,但是参数、细节方面有些许不同。

5.3.1 fstat函数

fstat 与 stat 区别在于,stat 是从文件名出发得到文件属性信息,不需要先打开文件;而 fstat 函数则是从 文件描述符出发得到文件属性信息,所以使用 fstat 函数之前需要先打开文件得到文件描述符。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int fstat(int fd, struct stat *buf);
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    struct stat file_stat;
    int fd;
    int ret;
    /* 打开文件 */
    fd = open("./test_file", O_RDONLY);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 获取文件属性 */
    ret = fstat(fd, &file_stat);
    if (-1 == ret)
        perror("fstat error");
    close(fd);
    exit(ret);
}

5.3.2 lstat函数

lstat()与 stat、fstat 的区别在于,对于符号链接文件,stat、fstat 查阅的是符号链接文件所指向的文件对 应的文件属性信息,而 lstat 查阅的是符号链接文件本身的属性信息。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int lstat(const char *pathname, struct stat *buf);

5.4 文件属主

Linux 是一个多用户操作系统,系统中一般存在着好几个不同的用户,而 Linux 系统中的每一个文件都 有一个与之相关联的用户和用户组,通过这个信息可以判断文件的所有者和所属组。

用户 ID 简称 UID、用户组 ID 简称 GID。

在这里插入图片描述

在这里插入图片描述

5.4.1 有效用户ID和有效组ID

首先对于有效用户 ID 和有效组 ID 来说,这是进程所持有的概念,对于文件来说,并无此属性。有效用户 ID 和有效组 ID 是站在操作系统的角度,用于给操作系统判断当前执行该进程的用户在当前环境下对某个文件是否拥有相应的权限。

当进行权限检查时,并不是通过进程的实际用户和实际组来参与权限检查的,是通过有效用户和有效组来参与文件权限检查。

5.4.2 chown函数

chown 是一个系统调用,该系统调用可用于改变文件的所有者(用户 ID)和所属组(组 ID)。

也可以用 sudo 改变,譬如将 a.cpp 文件的所有者和所属组修改为 root:

sudo chown root:root a.cpp

在这里插入图片描述

chown 函数原型如下所示

#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);

有以下两个限制条件:

  1. 只有超级用户进程能更改文件的用户 ID;
  2. 普通用户进程可以将文件的组 ID 修改为其所从属的任意附属组 ID,前提条件是该进程的有效用 户 ID 等于文件的用户 ID;而超级用户进程可以将文件的组 ID 修改为任意值。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    if (-1 == chown("./test_file", 0, 0)) // 0 就是 root
    {
        perror("chown error");
        exit(-1);
    }
    exit(0);
}

在这里插入图片描述

在 Linux 系统下,可以使用 getuid 和 getgid 两个系统调用分别用于获取当前进程的用户 ID 和用户组 ID,这里说的进程的用户 ID 和用户组 ID 指的就是进程的实际用户 ID 和实际组 ID,这两个系统调用函数 原型如下所示:

#include <unistd.h>
#include <sys/types.h>
uid_t getuid(void);
gid_t getgid(void);
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    printf("uid: %d\n", getuid());
    if (-1 == chown("./test_file", 0, 0))
    {
        perror("chown error");
        exit(-1);
    }
    exit(0);
}

加上 sudo 后成功运行:因为加上了 sudo 使得应用程序 的用户 ID 由原本的普通用户 ID 1000 变成了超级用户 ID 0,使得该进程变成了超级用户进程,所以调用 chown 函数就不会报错。

在这里插入图片描述

5.4.3 fchown和lchown函数

这两个同样也是系统调用,作用与 chown 函数相同,只是参数、细节方面有些许不同。

5.5 文件访问权限

struct stat 结构体中的 st_mode 字段记录了文件的访问权限位。当提及到文件时,指的是前面给大家介 绍的任何类型的文件,并不仅仅指的是普通文件;所有文件类型(目录、设备文件)都有访问权限(access permission)

5.5.1 普通权限和特殊权限

文件的权限可以分为两个大类,分别是普通权限和特殊权限(也可称为附加权限)。普通权限包括对文 件的读、写以及执行,而特殊权限则包括一些对文件的附加权限,譬如Set-User-ID、Set-Group-ID以及Sticky。

普通权限

st_mode 权限表示宏 含义
S_IRUSR 文件所有者读权限
S_IWUSR 文件所有者写权限
S_IXUSR 文件所有者执行权限
S_IRGRP 同组用户读权限
S_IWGRP 同组用户写权限
S_IXGRP 同组用户执行权限
S_IROTH 其它用户读权限
S_IWOTH 其它用户写权限
S_IXOTH 其它用户执行权限

特殊权限

特殊权限 含义
S_ISUID set-user-ID 位权限
S_ISGID set-group-ID 位权限
S_ISVTX Sticky 位权限

举例:通过 st_mode 变量判断文件是否设置了 set-user-ID 位权限,代码如下:

if (st.st_mode & S_ISUID) {
//设置了 set-user-ID 位权限
} else {
//没有设置 set-user-ID 位权限
}
  1. 当进程对文件进行操作的时候、将进行权限检查,如果文件的 set-user-ID 位权限被设置,内核会将 进程的有效 ID 设置为该文件的用户 ID(文件所有者 ID),意味着该进程直接获取了文件所有者 的权限、以文件所有者的身份操作该文件。
  2. 当进程对文件进行操作的时候、将进行权限检查,如果文件的 set-group-ID 位权限被设置,内核会 将进程的有效用户组 ID 设置为该文件的用户组 ID(文件所属组 ID),意味着该进程直接获取了 文件所属组成员的权限、以文件所属组成员的身份操作该文件。

5.5.2 目录权限

那说明删除文件、创建文件这些操作也是需要相应权限的,那这些权限又是从哪里获取的呢?答案就是目录。

  1. 目录的读权限:可列出(譬如:通过 ls 命令)目录之下的内容(即目录下有哪些文件)。
  2. 目录的写权限:可以在目录下创建文件、删除文件。
  3. 目录的执行权限:可访问目录下的文件,譬如对目录下的文件进行读、写、执行等操作。

5.5.3 检查文件权限access

文件的权限检查不单单只讨论文件本身的权限,还需要涉及到文件所在目录的权限,只有同时都满足了,才能通过操作系统的权限检查,进而才可以对文件进行相关操作。

#include <unistd.h>
int access(const char *pathname, int mode);

pathname:需要进行权限检查的文件路径。

mode:该参数可以取以下值:

  • F_OK:检查文件是否存在
  • R_OK:检查是否拥有读权限
  • W_OK:检查是否拥有写权限
  • X_OK:检查是否拥有执行权限

除了可以单独使用之外,还可以通过按位或运算符" | "组合在一起。

返回值:检查项通过则返回 0,表示拥有相应的权限并且文件存在;否则返回-1,如果多个检查项组合 在一起,只要其中任何一项不通过都会返回-1。

这里我在前面把ID换成了 root ,当前文件夹只有 “learn.c” 有 “w” 权限。

然后再切换到 sudu 把 ID 改回 1000。

chown("./test_file", 1000, 1000)

过几分钟就OK啦

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define MY_FILE "test_file"
int main(void)
{
    int ret;
    /* 检查文件是否存在 */
    ret = access(MY_FILE, F_OK);
    if (-1 == ret)
    {
        printf("%s: file does not exist.\n", MY_FILE);
        exit(-1);
    }
    /* 检查权限 */
    ret = access(MY_FILE, R_OK);
    if (!ret)
        printf("Read permission: Yes\n");
    else
        printf("Read permission: NO\n");
    ret = access(MY_FILE, W_OK);
    if (!ret)
        printf("Write permission: Yes\n");
    else
        printf("Write permission: NO\n");
    ret = access(MY_FILE, X_OK);
    if (!ret)
        printf("Execution permission: Yes\n");
    else
        printf("Execution permission: NO\n");
    exit(0);
}

5.5.4 修改文件权限chmod

#include <sys/stat.h>
int fchmod(int fd, mode_t mode);
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int ret;
    ret = chmod("./test_file", 0777);
    if (-1 == ret)
    {
        perror("chmod error");
        exit(-1);
    }
    exit(0);
}

5.5.5 umask函数

umask 命令用于查看/设置权限掩码,权限掩码主要用于对新建文件的权限进行屏蔽。

实际权限:

mode & ~umask
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);

这个例子没说清楚,具体的要用再查

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
     mode_t old_mask;
     old_mask = umask(0003);
     printf("old mask: %04o\n", old_mask);
     exit(0);
}

在这里插入图片描述

这里再次强调,umask 是进程自身的一种属性、A 进程的 umask 与 B 进程的 umask 无关(父子进程关 系除外)。在 shell 终端下可以使用 umask 命令设置 shell 终端的 umask 值,但是该 shell 终端关闭之后、再 次打开一个终端,新打开的终端将与之前关闭的终端并无任何瓜葛

5.6 文件的时间属性

字段 说明
st_atim 文件最后被访问的时间
st_mtim 文件内容最后被修改的时间
st_ctim 文件状态最后被改变的时间
  • 文件最后被访问的时间:访问指的是读取文件内容,文件内容最后一次被读取的时间,譬如使用 read()函数读取文件内容便会改变该时间属性;
  • 文件内容最后被修改的时间:文件内容发生改变,譬如使用 write()函数写入数据到文件中便会改变 该时间属性;
  • 文件状态最后被改变的时间:状态更改指的是该文件的 inode 节点最后一次被修改的时间,譬如更 改文件的访问权限、更改文件的用户 ID、用户组 ID、更改链接数等,但它们并没有更改文件的实 际内容,也没有访问(读取)文件内容。

5.6.1 utime()、utimes()修改时间属性

文件的时间属性虽然会在我们对文件进行相关操作(譬如:读、写)的时候发生改变,但这些改变都是 隐式、被动的发生改变,除此之外,还可以使用 Linux 系统提供的系统调用显式的修改文件的时间属性。

utime()函数

#include <sys/types.h>
#include <utime.h>
int utime(const char *filename, const struct utimbuf *times);

times:将时间属性修改为该参数所指定的时间值,times 是一个 struct utimbuf 结构体类型的指针,如果将 times 参数设置为 NULL,则会将文件的访问时间和修改时间设置为系统当前时间。

struct utimbuf {
    time_t actime; /* 访问时间 */
    time_t modtime; /* 内容修改时间 */
};

该结构体中包含了两个 time_t 类型的成员,分别用于表示访问时间和内容修改时间,time_t 类型其实就 是 long int 类型,所以这两个时间是以秒为单位的。

只有以下两种进程可对时间属性进行修改:

  • 超级用户进程(以 root 身份运行的进程)。
  • 有效用户 ID 与该文件用户 ID(文件所有者)相匹配的进程。
  • 在参数 times 等于 NULL 的情况下,对文件拥有写权限的进程。
#include <sys/types.h>
#include <utime.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MY_FILE "./test_file"
int main(void)
{
    struct utimbuf utm_buf;
    time_t cur_sec;
    int ret;
    /* 检查文件是否存在 */
    ret = access(MY_FILE, F_OK);
    if (-1 == ret)
    {
        printf("Error: %s file does not exist!\n", MY_FILE);
        exit(-1);
    }
    /* 获取当前时间 */
    time(&cur_sec);
    utm_buf.actime = cur_sec;
    utm_buf.modtime = cur_sec;
    /* 修改文件时间戳 */
    ret = utime(MY_FILE, &utm_buf);
    if (-1 == ret)
    {
        perror("utime error");
        exit(-1);
    }
    exit(0);
}

在这里插入图片描述

访问时间和内容修改时间和状态更改时间均被更改为当前时间了

utimes()函数

#include <sys/time.h>
int utimes(const char *filename, const struct timeval times[2]);

times:将时间属性修改为该参数所指定的时间值,times 是一个 struct timeval 结构体类型的数组,数组 共有两个元素,第一个元素用于指定访问时间,第二个元素用于指定内容修改时间

struct timeval {
    long tv_sec; /* 秒 */
    long tv_usec; /* 微秒 */
};
#include <unistd.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#define MY_FILE "./b.cpp"
int main(void)
{
    struct timeval tmval_arr[2];
    time_t cur_sec;
    int ret;
    int i;
    /* 检查文件是否存在 */
    ret = access(MY_FILE, F_OK);
    if (-1 == ret)
    {
        printf("Error: %s file does not exist!\n", MY_FILE);
        exit(-1);
    }
    /* 获取当前时间 */
    time(&cur_sec);
    for (i = 0; i < 2; i++)
    {
        tmval_arr[i].tv_sec = cur_sec;
        tmval_arr[i].tv_usec = 0;
    }

    /* 修改文件时间戳 */
    ret = utimes(MY_FILE, tmval_arr);
    if (-1 == ret)
    {
        perror("utimes error");
        exit(-1);
    }
    exit(0);
}

在这里插入图片描述

5.6.2 futimens()、utimensat()修改时间属性

仍然是两个系统调用

futimens

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#define MY_FILE "./test_file"
int main(void)
{
    struct timespec tmsp_arr[2];
    int ret;
    int fd;
    /* 检查文件是否存在 */
    ret = access(MY_FILE, F_OK);
    if (-1 == ret)
    {
        printf("Error: %s file does not exist!\n", MY_FILE);
        exit(-1);
    }
    /* 打开文件 */
    fd = open(MY_FILE, O_RDONLY);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 修改文件时间戳 */
#if 1
    ret = futimens(fd, NULL); //同时设置为当前时间
#endif
#if 0
 tmsp_arr[0].tv_nsec = UTIME_OMIT;//访问时间保持不变
 tmsp_arr[1].tv_nsec = UTIME_NOW;//内容修改时间设置为当期时间
 ret = futimens(fd, tmsp_arr);
#endif
#if 0
 tmsp_arr[0].tv_nsec = UTIME_NOW;//访问时间设置为当前时间
 tmsp_arr[1].tv_nsec = UTIME_OMIT;//内容修改时间保持不变
 ret = futimens(fd, tmsp_arr);
#endif
    if (-1 == ret)
    {
        perror("futimens error");
        goto err;
    }
err:
    close(fd);
    exit(ret);
}

utimensat

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define MY_FILE "/home/wsm/CS/C/test_file"
int main(void)
{
    struct timespec tmsp_arr[2];
    int ret;
    /* 检查文件是否存在 */
    ret = access(MY_FILE, F_OK);
    if (-1 == ret)
    {
        printf("Error: %s file does not exist!\n", MY_FILE);
        exit(-1);
    }
    /* 修改文件时间戳 */
#if 1
    ret = utimensat(-1, MY_FILE, NULL, AT_SYMLINK_NOFOLLOW); //同时设置为当前时间
#endif
#if 0
 tmsp_arr[0].tv_nsec = UTIME_OMIT;//访问时间保持不变
 tmsp_arr[1].tv_nsec = UTIME_NOW;//内容修改时间设置为当期时间
 ret = utimensat(-1, MY_FILE, tmsp_arr, AT_SYMLINK_NOFOLLOW);
#endif
#if 0
 tmsp_arr[0].tv_nsec = UTIME_NOW;//访问时间设置为当前时间
 tmsp_arr[1].tv_nsec = UTIME_OMIT;//内容修改时间保持不变
 ret = utimensat(-1, MY_FILE, tmsp_arr, AT_SYMLINK_NOFOLLOW);
#endif
    if (-1 == ret)
    {
        perror("futimens error");
        exit(-1);
    }
    exit(0);
}

5.7 符号链接(软链接)与硬链接

软链接其作用类似于 Windows 下的快捷方式

使用 ln 命令可以为一个文件创建软链接文件或硬链接文件,用法如下:
硬链接:ln 源文件 链接文件
软链接:ln -s 源文件 链接文件

源文件本身就是一个硬链接文件

在这里插入图片描述

hard12

软连接不占 inode 节点上的链接数

在这里插入图片描述

在这里插入图片描述

5.7.1 创建链接文件

创建硬链接 link()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    int ret;
    ret = link("./test_file", "./hard");
    if (-1 == ret)
    {
        perror("link error");
        exit(-1);
    }
    exit(0);
}

创建软链接 symlink()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    int ret;
    ret = symlink("./test_file", "./soft");
    if (-1 == ret)
    {
        perror("symlink error");
        exit(-1);
    }
    exit(0);
}

5.7.2 读取软链接文件

readlink()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
    char buf[50];
    int ret;
    memset(buf, 0x0, sizeof(buf));
    ret = readlink("./soft1", buf, sizeof(buf));
    if (-1 == ret)
    {
        perror("readlink error");
        exit(-1);
    }
    printf("%s\n", buf);
    exit(0);
}

在这里插入图片描述

5.8 目录

5.8.1 目录存储形式

在这里插入图片描述

5.8.2 创建和删除目录

mkdir()

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(void)
{
    int ret;
    ret = mkdir("./new_dir", S_IRWXU |
                                 S_IRGRP | S_IXGRP |
                                 S_IROTH | S_IXOTH);
    if (-1 == ret)
    {
        perror("mkdir error");
        exit(-1);
    }
    exit(0);
}

rmdir()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    int ret;
    ret = rmdir("./new_dir");
    if (-1 == ret)
    {
        perror("rmdir error");
        exit(-1);
    }
    exit(0);
}

5.8.3 打开、读取以及关闭目录

对于目录来说,可以使用 opendir()、 readdir()和 closedir()来打开、读取以及关闭目录

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <errno.h>
int main(void)
{
    struct dirent *dir;
    DIR *dirp;
    int ret = 0;
    /* 打开目录 */
    dirp = opendir("./my_dir");
    if (NULL == dirp)
    {
        perror("opendir error");
        exit(-1);
    }
    /* 循环读取目录流中的所有目录条目 */
    errno = 0;
    while (NULL != (dir = readdir(dirp)))
        printf("%s %ld\n", dir->d_name, dir->d_ino);
    if (0 != errno)
    {
        perror("readdir error");
        ret = -1;
        goto err;
    }
    else
        printf("End of directory!\n");
err:
    closedir(dirp);
    exit(ret);
}

在这里插入图片描述

5.8.4 进程的当前工作目录

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
    char buf[100];
    char *ptr;
    memset(buf, 0x0, sizeof(buf));
    ptr = getcwd(buf, sizeof(buf));
    if (NULL == ptr)
    {
        perror("getcwd error");
        exit(-1);
    }
    printf("Current working directory: %s\n", buf);
    exit(0);
}

在这里插入图片描述

改变当前工作目录

系统调用 chdir()和 fchdir()可以用于更改进程的当前工作目录

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
    char buf[100];
    char *ptr;
    int ret;
    /* 获取更改前的工作目录 */
    memset(buf, 0x0, sizeof(buf));
    ptr = getcwd(buf, sizeof(buf));
    if (NULL == ptr)
    {
        perror("getcwd error");
        exit(-1);
    }
    printf("Before the change: %s\n", buf);
    /* 更改进程的当前工作目录 */
    ret = chdir("./new_dir");
    if (-1 == ret)
    {
        perror("chdir error");
        exit(-1);
    }
    /* 获取更改后的工作目录 */
    memset(buf, 0x0, sizeof(buf));
    ptr = getcwd(buf, sizeof(buf));
    if (NULL == ptr)
    {
        perror("getcwd error");
        exit(-1);
    }
    printf("After the change: %s\n", buf);
    exit(0);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1AfgHvai-1662126244630)(C:\Users\lemer\AppData\Roaming\Typora\typora-user-images\image-20220824110356776.png)]

5.9 删除文件

使用 unlink 函数删除文件

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    int ret;
    ret = unlink("./test_file");
    if (-1 == ret)
    {
        perror("unlink error");
        exit(-1);
    }
    exit(0);
}

在这里插入图片描述

remove()

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int ret;
    ret = remove("./test_file");
    if (-1 == ret)
    {
        perror("remove error");
        exit(-1);
    }
    exit(0);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B6Sca9HR-1662126244630)(C:\Users\lemer\AppData\Roaming\Typora\typora-user-images\image-20220824110711984.png)]

5.10 文件重命名

rename()系统调用

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int ret;
    ret = rename("./test_file", "./new_file");
    if (-1 == ret)
    {
        perror("rename error");
        exit(-1);
    }
    exit(0);
}

在这里插入图片描述

其 inode 号并未改变

5.11 总结

本章所介绍的内容比较多,主要是围绕文件属性以及目录展开的一系列相关话题,本章开头先给大家介 绍 Linux 系统下的 7 种文件类型,包括普通文件、目录、设备文件(字符设备文件、块设备文件)、符号链 接文件(软链接文件)、管道文件以及套接字文件。

接着围绕 stat 系统调用,详细给大家介绍了 struct stat 结构体中的每一个成员,这使得我们对 Linux 下 文件的各个属性都有所了解。接着分别给大家详细介绍了文件属主、文件访问权限、文件时间戳、软链接与 硬链接以及目录等相关内容,让大家知道在应用编程中如何去修改文件的这些属性以及它们所需要满足的 条件。

至此,本章内容到这里就结束了,相信大家已经学习到了不少知识内容,大家加油!

第六章 字符串处理

6.1 字符串输入/输出

常用的字符串输出函数有 putchar()、puts()、fputc()、fputs()

printf()一般称为格式化输出

常用的字符串输入函数有 gets()、getchar()、fgetc()、fgets()。

6.2 字符串长度

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    char str[50] = "Linux app strlen test!";
    char *ptr = str;
    printf("sizeof: %ld\n", sizeof(str));
    printf("strlen: %ld\n", strlen(str));
    puts("~~~~~~~~~~");
    printf("sizeof: %ld\n", sizeof(ptr)); //指针变量 ptr 的大小,这里等于 8 个字节
    printf("strlen: %ld\n", strlen(ptr));
    exit(0);
}

在这里插入图片描述

6.3 字符串拼接

一种是 strcat ,这里不赘述

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    char str1[100] = "Linux app strcat test, ";
    char str2[] = "Hello World!";
    strncat(str1, str2, 5);
    puts(str1);
    exit(0);
}

在这里插入图片描述

6.4 字符串拷贝

strcpy 不再介绍

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    char str1[100] = "AAAAAAAAAAAAAAAAAAAAAAAA";
    char str2[] = "Hello World!";
    strncpy(str1, str2, 5);
    puts(str1);
    puts("~~~~~~~~~~~~~~~");
    strncpy(str1, str2, 20);
    puts(str1);
    exit(0);
}

在这里插入图片描述

除了 strcpy()和 strncpy()之外,其实还可以使用 memcpy()、memmove()以及 bcopy()这些库函数实现拷贝操作

6.5 内存填充

memset()

bzero()函数用于将一段内存空间中的数据全部设置为 0

char str[100];
bzero(str, sizeof(str));

6.6 字符串比较

#include <string.h>
int strcmp(const char *s1, const char *s2);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    printf("%d %d\n", 'A', 'a');
    printf("%d\n", strcmp("ABC", "ABC"));
    printf("%d\n", strcmp("ABC", "a"));
    printf("%d\n", strcmp("a", "ABC"));
    exit(0);
}

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    printf("%d\n", strncmp("ABC", "ABC", 3));
    printf("%d\n", strncmp("ABC", "ABCD", 3));
    printf("%d\n", strncmp("ABC", "ABCD", 4));
    exit(0);
}

在这里插入图片描述

6.7 字符串查找

strchr()、 strrchr()、strstr()、strpbrk()、index()以及 rindex()等。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    char *ptr = NULL;
    char str[] = "I love my home";
    ptr = strchr(str, 'o');  //从前往后
    if (NULL != ptr)
        printf("strchr: %ld\n", ptr - str);
    ptr = strrchr(str, 'o');  //从后往前
    if (NULL != ptr)
        printf("strrchr: %ld\n", ptr - str);
    exit(0);
}

strstr()

#include <string.h>
char *strstr(const char *haystack, const char *needle);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    char *ptr = NULL;
    char str[] = "I love my home";
    ptr = strstr(str, "home");
    if (NULL != ptr)
    {
        printf("String: %s\n", ptr);
        printf("Offset: %ld\n", ptr - str);
    }
    exit(0);
}

在这里插入图片描述

6.8 字符串与数字互转

6.8.1 字符串转整形数据

主要包括 atoi()、atol()、atoll()以及 strtol()、strtoll()、strtoul()、strtoull()等

#include <stdlib.h>
int atoi(const char *nptr);
long atol(const char *nptr);
long long atoll(const char *nptr);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    printf("atoi: %d\n", atoi("500"));
    printf("atol: %ld\n", atol("500"));
    printf("atoll: %lld\n", atoll("500"));
    exit(0);
}

在这里插入图片描述

strtol()、strtoll()

两个函数可分别将字符串转为 long int 类型数据和 long long ing 类型数据,与 atol()、 atoll()之间的区别在于,strtol()、strtoll()可以实现将多种不同进制数(譬如二进制表示的数字字符串、八进制 表示的数字字符串、十六进制表示的数数字符串)表示的字符串转换为整形数据

#include <stdlib.h>
long int strtol(const char *nptr, char **endptr, int base);
long long int strtoll(const char *nptr, char **endptr, int base);

函数参数和返回值含义如下:

nptr:需要进行转换的目标字符串。

endptr:char **类型的指针,如果 endptr 不为 NULL,则 strtol()或 strtoll()会将字符串中第一个无效字 符的地址存储在*endptr 中。如果根本没有数字,strtol()或 strtoll()会将 nptr 的原始值存储在*endptr 中(并返 回 0)。也可将参数 endptr 设置为 NULL,表示不接收相应信息。

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 大于 10 的时候,’ a ‘代表 10、’ b ‘代表 11、’ c ‘代表 12,依次类推,’ z '代表 35(不区 分大小写)。

在 base=0 的情况下,如果字符串包含一个了“0x”前缀,表示该数字将以 16 为基数;如果包含的是 “0”前缀,表示该数字将以 8 为基数。 当 base=16 时,字符串可以使用“0x”前缀。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    printf("strtol: %ld\n", strtol("0x500", NULL, 16));
    printf("strtol: %ld\n", strtol("0x500", NULL, 0));
    printf("strtol: %ld\n", strtol("500", NULL, 16));
    printf("strtol: %ld\n", strtol("0777", NULL, 8));
    printf("strtol: %ld\n", strtol("0777", NULL, 0));
    printf("strtol: %ld\n", strtol("1111", NULL, 2));
    printf("strtol: %ld\n", strtol("-1111", NULL, 2));
    exit(0);
}

在这里插入图片描述

strtoul()、strtoull()

#include <stdlib.h>
unsigned long int strtoul(const char *nptr, char **endptr, int base);
unsigned long long int strtoull(const char *nptr, char **endptr, int base);

6.8.2 字符串转浮点型数据

atof()、strtod()、strtof()、strtold()

atof ()

atof()用于将字符串转换为一个 double 类型的浮点数据

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    printf("atof: %lf\n", atof("0.123"));
    printf("atof: %lf\n", atof("-1.1185"));
    printf("atof: %lf\n", atof("100.0123"));
    exit(0);
}

在这里插入图片描述

strtof()、strtod()以及 strtold()

#include <stdlib.h>
double strtod(const char *nptr, char **endptr);
float strtof(const char *nptr, char **endptr);
long double strtold(const char *nptr, char **endptr);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    printf("strtof: %f\n", strtof("0.123", NULL));
    printf("strtod: %lf\n", strtod("-1.1185", NULL));
    printf("strtold: %Lf\n", strtold("100.0123", NULL));
    exit(0);
}

在这里插入图片描述

6.8.3 数字转字符串

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    char str[20] = {0};
    sprintf(str, "%d", 500);
    puts(str);
    memset(str, 0x0, sizeof(str));
    sprintf(str, "%.2f", 500.111);
    puts(str);
    memset(str, 0x0, sizeof(str));
    sprintf(str, "%u", 500);
    puts(str);
    exit(0);
}

在这里插入图片描述

6.9 给应用程序传参

int main(int argc, char *argv[])
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    int i = 0;
    printf("Number of parameters: %d\n", argc);
    for (i = 0; i < argc; i++)
        printf(" %s\n", argv[i]);
    exit(0);
}

在这里插入图片描述

6.10 正则表达式

正则表达式其实也是一个字符串,该字符串由普通字符(譬如,数字 0~9、大小写字母以及其它字符) 和特殊字符(称为“元字符”)所组成,由这些字符组成一个“规则字符串”,这个“规则字符串”用来表 达对给定字符串的一种查找、匹配逻辑。

6.10.1 初识正则表达式

?通配符匹配 0 个或 1 个字符,而*通配符匹配 0 个或多个字符

6.11 C语言中使用正则表达式

没看懂

第七章 系统信息与系统资源

7.1 系统信息

7.1.1 系统标识uname

系统调用 uname()用于获取有关当前操作系统内核的名称和信息,函数原型如下所示

#include <sys/utsname.h>
int uname(struct utsname *buf);

struct utsname 结构体

struct utsname
{
    char sysname[];  /* 当前操作系统的名称 */
    char nodename[]; /* 网络上的名称(主机名) */
    char release[];  /* 操作系统内核版本 */
    char version[];  /* 操作系统发行版本 */
    char machine[];  /* 硬件架构类型 */
#ifdef _GNU_SOURCE
    char domainname[]; /* 当前域名 */
#endif
};
#include <stdio.h>
#include <stdlib.h>
#include <sys/utsname.h>
int main(void)
{
    struct utsname os_info;
    int ret;
    /* 获取信息 */
    ret = uname(&os_info);
    if (-1 == ret)
    {
        perror("uname error");
        exit(-1);
    }
    /* 打印信息 */
    printf("操作系统名称: %s\n", os_info.sysname);
    printf("主机名: %s\n", os_info.nodename);
    printf("内核版本: %s\n", os_info.release);
    printf("发行版本: %s\n", os_info.version);
    printf("硬件架构: %s\n", os_info.machine);
    exit(0);
}

在这里插入图片描述

7.1.2 sysinfo函数

sysinfo 系统调用可用于获取一些系统统计信息,其函数原型如下所示:

#include <sys/sysinfo.h>
int sysinfo(struct sysinfo *info);

struct sysinfo 结构体

struct sysinfo
{
    long uptime;                                  /* 自系统启动之后所经过的时间(以秒为单位) */
    unsigned long loads[3];                       /* 1, 5, and 15 minute load averages */
    unsigned long totalram;                       /* 总的可用内存大小 */
    unsigned long freeram;                        /* 还未被使用的内存大小 */
    unsigned long sharedram;                      /* Amount of shared memory */
    unsigned long bufferram;                      /* Memory used by buffers */
    unsigned long totalswap;                      /* Total swap space size */
    unsigned long freeswap;                       /* swap space still available */
    unsigned short procs;                         /* 系统当前进程数量 */
    unsigned long totalhigh;                      /* Total high memory size */
    unsigned long freehigh;                       /* Available high memory size */
    unsigned int mem_unit;                        /* 内存单元大小(以字节为单位) */
    char _f[20 - 2 * sizeof(long) - sizeof(int)]; /* Padding to 64 bytes */
};
#include <stdio.h>
#include <stdlib.h>
#include <sys/sysinfo.h>
int main(void)
{
    struct sysinfo sys_info;
    int ret;
    /* 获取信息 */
    ret = sysinfo(&sys_info);
    if (-1 == ret)
    {
        perror("sysinfo error");
        exit(-1);
    }
    /* 打印信息 */
    printf("uptime: %ld\n", sys_info.uptime);
    printf("totalram: %lu\n", sys_info.totalram);
    printf("freeram: %lu\n", sys_info.freeram);
    printf("procs: %u\n", sys_info.procs);
    exit(0);
}

在这里插入图片描述

7.1.3 gethostname函数

此函数可用于单独获取 Linux 系统主机名,与 struct utsname 数据结构体中的 nodename 变量一样, gethostname 函数原型如下所示

#include <unistd.h>
int gethostname(char *name, size_t len);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
    char hostname[20];
    int ret;
    memset(hostname, 0x0, sizeof(hostname));
    ret = gethostname(hostname, sizeof(hostname));
    if (-1 == ret)
    {
        perror("gethostname error");
        exit(ret);
    }
    puts(hostname);
    exit(0);
}

在这里插入图片描述

7.1.4 sysconf()函数

sysconf()函数是一个库函数,可在运行时获取系统的一些配置信息,譬如页大小(page size)、主机名 的最大长度、进程可以打开的最大文件数、每个用户 ID 的最大并发进程数等。其函数原型如下所示:

#include <unistd.h>
long sysconf(int name);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("每个用户的最大并发进程数: %ld\n", sysconf(_SC_CHILD_MAX));
    printf("系统节拍率: %ld\n", sysconf(_SC_CLK_TCK));
    printf("系统页大小: %ld\n", sysconf(_SC_PAGESIZE));
    exit(0);
}

为什么这里每个用户的最大并发进程数是-1不清楚

在这里插入图片描述

7.2 时间、日期

7.2.1 时间的概念

GMT 时间

格林威治标准时间

UTC 时间

世界协调时间

在这里插入图片描述

7.2.2 Linux系统中的时间

实时时钟 RTC

操作系统中一般会有两个时钟,一个系统时钟(system clock),一个实时时钟(Real time clock),也叫 RTC

系统时钟由系统启动之后由内核来维护,所以在系统关机情况下是不存在的

实时时钟一般由 RTC 时钟芯片提供,RTC 芯片有相应的电池为其供电,以保证系统在关机情况下 RTC 能够继续工作、继续计时。

Linux 系统如何记录时间

Linux 系统在开机启动之后首先会读取 RTC 硬件获取实时时钟作为系统时钟的初始值,之后内核便开始维护自己的系统时钟。所以由此可知,RTC 硬件只有在系统开机启动时会读取一次,目的是用于对系统 时钟进行初始化操作,之后的运行过程中便不会再对其进行读取操作了。

而在系统关机时,内核会将系统时钟写入到 RTC 硬件、已进行同步操作。

jiffies 的引入

jiffies 是内核中定义的一个全局变量,内核使用 jiffies 来记录系统从启动以来的系统节拍数

一般默认情况 下都是采用 100Hz 作为系统节拍率。

内核其实通过 jiffies 来维护系统时钟,全局变量 jiffies 在系统开机启动时会设置一个初始值

7.2.3 获取时间time/gettimeofday

(1)time 函数

#include <time.h>
time_t time(time_t *tloc);

使用系统调用 time()获取自 1970-01-01 00:00:00 +0000 (UTC)以来的时间值:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    time_t t;
    t = time(NULL);
    if (-1 == t)
    {
        perror("time error");
        exit(-1);
    }
    printf("时间值: %ld\n", t);
    exit(0);
}

在这里插入图片描述

(2)gettimeofday 函数

#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
int main(void)
{
    struct timeval tval;
    int ret;
    ret = gettimeofday(&tval, NULL);
    if (-1 == ret)
    {
        perror("gettimeofday error");
        exit(-1);
    }
    printf("时间值: %ld 秒+%ld 微秒\n", tval.tv_sec, tval.tv_usec);
    exit(0);
}

在这里插入图片描述

7.2.4 时间转换函数

这些 API 可 以将 time()或 gettimeofday()函数获取到的秒数转换为利于查看和理解的形式。

(1)ctime 函数

#include <time.h>
char *ctime(const time_t *timep);
char *ctime_r(const time_t *timep, char *buf);
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    char tm_str[100] = {0};
    time_t tm;
    /* 获取时间 */
    tm = time(NULL);
    if (-1 == tm)
    {
        perror("time error");
        exit(-1);
    }
    /* 将时间转换为字符串形式 */
    ctime_r(&tm, tm_str);

    /* 打印输出 */
    printf("当前时间: %s", tm_str);
    exit(0);
}

在这里插入图片描述

(2)localtime 函数

localtime()函数可以把 time()或 gettimeofday()得到的秒数(time_t 时间或日历时间)变成一个 struct tm 结构体所表示的时间,该时间对应的是本地时间。localtime 函数原型如下:

#include <time.h>
struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);

struct tm 结构体

struct tm
{
    int tm_sec;   /* 秒(0-60) */
    int tm_min;   /* 分(0-59) */
    int tm_hour;  /* 时(0-23) */
    int tm_mday;  /* 日(1-31) */
    int tm_mon;   /* 月(0-11) */
    int tm_year;  /* 年(这个值表示的是自 1900 年到现在经过的年数) */
    int tm_wday;  /* 星期(0-6, 星期日 Sunday = 0、星期一=1…) */
    int tm_yday;  /* 一年里的第几天(0-365, 1 Jan = 0) */
    int tm_isdst; /* 夏令时 */
};
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    struct tm t;
    time_t sec;
    /* 获取时间 */
    sec = time(NULL);
    if (-1 == sec)
    {
        perror("time error");
        exit(-1);
    }
    /* 转换得到本地时间 */
    localtime_r(&sec, &t);
    /* 打印输出 */
    printf("当前时间: %d 年%d 月%d 日 %d:%d:%d\n",
           t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, //更改 这里的month要+1
           t.tm_hour, t.tm_min, t.tm_sec);
    exit(0);
}

在这里插入图片描述

(3)gmtime 函数

与 localtime()所不同的是, gmtime()函数所得到的是 UTC 国际标准时间,并不是计算机的本地时间,这是它们之间的唯一区别。

#include <time.h>
struct tm *gmtime(const time_t *timep);
struct tm *gmtime_r(const time_t *timep, struct tm *result);
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    struct tm local_t;
    struct tm utc_t;
    time_t sec;
    /* 获取时间 */
    sec = time(NULL);
    if (-1 == sec)
    {
        perror("time error");
        exit(-1);
    }
    /* 转换得到本地时间 */
    localtime_r(&sec, &local_t);
    /* 转换得到国际标准时间 */
    gmtime_r(&sec, &utc_t);
    /* 打印输出 */
    printf("本地时间: %d 年%d 月%d 日 %d:%d:%d\n",
           local_t.tm_year + 1900, local_t.tm_mon, local_t.tm_mday,
           local_t.tm_hour, local_t.tm_min, local_t.tm_sec);
    printf("UTC 时间: %d 年%d 月%d 日 %d:%d:%d\n",
           utc_t.tm_year + 1900, utc_t.tm_mon, utc_t.tm_mday,
           utc_t.tm_hour, utc_t.tm_min, utc_t.tm_sec);
    exit(0);
}

在这里插入图片描述

(4)mktime 函数

mktime()函数与 localtime()函数相反,mktime()可以将使用 struct tm 结构体表示的分解时间转换为 time_t 时间(日历时间),同样这也是一个 C 库函数,其函数原型如下所示:

#include <time.h>
time_t mktime(struct tm *tm);
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    struct tm local_t;
    time_t sec;
    /* 获取时间 */
    sec = time(NULL);
    if (-1 == sec)
    {
        perror("time error");
        exit(-1);
    }
    printf("获取得到的秒数: %ld\n", sec);
    localtime_r(&sec, &local_t);
    printf("转换得到的秒数: %ld\n", mktime(&local_t));
    exit(0);
}

在这里插入图片描述

(5)asctime 函数

asctime()函数与 ctime()函数的作用一样,也可将时间转换为可打印输出的字符串形式,与 ctime()函数 的区别在于,ctime()是将 time_t 时间转换为固定格式字符串、而 asctime()则是将 struct tm 表示的分解时间 转换为固定格式的字符串。asctime()函数原型如下所示:

#include <time.h>
char *asctime(const struct tm *tm);
char *asctime_r(const struct tm *tm, char *buf);
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    struct tm local_t;
    char tm_str[100] = {0};
    time_t sec;
    /* 获取时间 */
    sec = time(NULL);
    if (-1 == sec)
    {
        perror("time error");
        exit(-1);
    }
    localtime_r(&sec, &local_t);
    asctime_r(&local_t, tm_str);
    printf("本地时间: %s", tm_str);
    exit(0);
}

在这里插入图片描述

(6)strftime 函数

在功能上比 asctime()和 ctime()更加强大,它可以根据自己 的喜好自定义时间的显示格式

#include <time.h>
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

太多了,需要用的时候再去查

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    struct tm local_t;
    char tm_str[100] = {0};
    time_t sec;
    /* 获取时间 */
    sec = time(NULL);
    if (-1 == sec)
    {
        perror("time error");
        exit(-1);
    }
    localtime_r(&sec, &local_t);
    strftime(tm_str, sizeof(tm_str), "%Y-%m-%d %A %H:%M:%S", &local_t);
    printf("本地时间: %s\n", tm_str);
    exit(0);
}

7.2.5 设置时间settimeofday

使用 settimeofday()函数可以设置时间,也就是设置系统的本地时间,函数原型如下所示:

#include <sys/time.h>
int settimeofday(const struct timeval *tv, const struct timezone *tz);

7.2.6 总结

在这里插入图片描述

7.3 进程时间

进程时间指的是进程从创建后(也就是程序运行后)到目前为止这段时间内使用 CPU 资源的时间总数, 出于记录的目的,内核把 CPU 时间(进程时间)分为以下两个部分:

  1. 用户 CPU 时间:进程在用户空间(用户态)下运行所花费的 CPU 时间。有时也成为虚拟时间(virtual time)。
  2. 系统 CPU 时间:进程在内核空间(内核态)下运行所花费的 CPU 时间。这是内核执行系统调用或 代表进程执行的其它任务(譬如,服务页错误)所花费的时间。

一般来说,进程时间指的是用户 CPU 时间和系统 CPU 时间的总和,也就是总的 CPU 时间。

休眠的这段时间并不计算在进程时间中

7.3.1 times函数

times()函数用于获取当前进程时间,其函数原型如下所示:

#include <sys/times.h>
clock_t times(struct tms *buf);

struct tms 结构体

struct tms
{
    clock_t tms_utime;  /* user time, 进程的用户 CPU 时间, tms_utime 个系统节拍数 */
    clock_t tms_stime;  /* system time, 进程的系统 CPU 时间, tms_stime 个系统节拍数 */
    clock_t tms_cutime; /* user time of children, 已死掉子进程的 tms_utime + tms_cutime 时间总和 */
    clock_t tms_cstime; /* system time of children, 已死掉子进程的 tms_stime + tms_cstime 时间总和 */
};
#include <stdio.h>
#include <stdlib.h>
#include <sys/times.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
    struct tms t_buf_start;
    struct tms t_buf_end;
    clock_t t_start;
    clock_t t_end;
    long tck;
    int i, j;
    /* 获取系统的节拍率 */
    tck = sysconf(_SC_CLK_TCK);
    /* 开始时间 */
    t_start = times(&t_buf_start);
    if (-1 == t_start)
    {
        perror("times error");
        exit(-1);
    }
    /* *****需要进行测试的代码段***** */
    for (i = 0; i < 20000; i++)
        for (j = 0; j < 20000; j++)
            printf("i");
    sleep(1); //休眠挂起

    /* *************end************** */
    /* 结束时间 */
    t_end = times(&t_buf_end);
    if (-1 == t_end)
    {
        perror("times error");
        exit(-1);
    }
    /* 打印时间 */
    printf("时间总和: %f 秒\n", (t_end - t_start) / (double)tck);
    printf("用户 CPU 时间: %f 秒\n", (t_buf_end.tms_utime - t_buf_start.tms_utime) / (double)tck);
    printf("系统 CPU 时间: %f 秒\n", (t_buf_end.tms_stime - t_buf_start.tms_stime) / (double)tck);
    exit(0);
}

在这里插入图片描述

7.3.2 clock函数

库函数 clock()提供了一个更为简单的方式用于进程时间,它的返回值描述了进程使用的总的 CPU 时间 (也就是进程时间,包括用户 CPU 时间和系统 CPU 时间),其函数原型如下所示:

#include <time.h>
clock_t clock(void);
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(int argc, char *argv[])
{
    clock_t t_start;
    clock_t t_end;
    int i, j;
    /* 开始时间 */
    t_start = clock();
    if (-1 == t_start)
        exit(-1);
    /* *****需要进行测试的代码段***** */
    for (i = 0; i < 20000; i++)
        for (j = 0; j < 20000; j++)
            ;
    /* *************end************** */
    /* 结束时间 */
    t_end = clock();
    if (-1 == t_end)
        exit(-1);
    /* 打印时间 */
    printf("总的 CPU 时间: %f\n", (t_end - t_start) / (double)CLOCKS_PER_SEC);
    exit(0);
}

a

7.4 产生随机数

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(int argc, char *argv[])
{
    int random_number_arr[8];
    int count;
    /* 设置随机数种子 */
    srand(time(NULL));
    /* 生成伪随机数 */
    for (count = 0; count < 8; count++)
        random_number_arr[count] = rand() % 100;
    /* 打印随机数数组 */
    printf("[");
    for (count = 0; count < 8; count++)
    {
        printf("%d", random_number_arr[count]);
        if (count != 8 - 1)
            printf(", ");
    }
    printf("]\n");
    exit(0);
}

在这里插入图片描述

7.5 休眠

7.5.1 秒级休眠: sleep

#include <unistd.h>
unsigned int sleep(unsigned int seconds);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    puts("Sleep Start!");
    /* 让程序休眠 3 秒钟 */
    sleep(3);
    puts("Sleep End!");
    exit(0);
}

7.5.2 微秒级休眠: usleep

#include <unistd.h>
int usleep(useconds_t usec);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    puts("Sleep Start!");
    /* 让程序休眠 3 秒钟(3*1000*1000 微秒) */
    usleep(3 * 1000 * 1000);
    puts("Sleep End!");
    exit(0);
}

7.5.3 高精度休眠: nanosleep

#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    struct timespec request_t;
    puts("Sleep Start!");
    /* 让程序休眠 3 秒钟 */
    request_t.tv_sec = 3;
    request_t.tv_nsec = 0;
    nanosleep(&request_t, NULL);
    puts("Sleep End!");
    exit(0);
}

7.6 申请堆内存

7.6.1 在堆上分配内存:malloc和free

#include <stdlib.h>
void *malloc(size_t size);
#include <stdlib.h>
void free(void *ptr);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MALLOC_MEM_SIZE (1 * 1024 * 1024)
int main(int argc, char *argv[])
{
    char *base = NULL;
    /* 申请堆内存 */
    base = (char *)malloc(MALLOC_MEM_SIZE);
    if (NULL == base)
    {
        printf("malloc error\n");
        exit(-1);
    }
    /* 初始化申请到的堆内存 */
    memset(base, 0x0, MALLOC_MEM_SIZE);
    /* 使用内存 */
    /* ...... */
    /* 释放内存 */
    free(base);
    exit(0);
}

当一个进程终止时,内核会自动关闭它没有关闭的所有文件

7.6.2 在堆上分配内存的其它方法

用 calloc()分配内存

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    int *base = NULL;
    int i;
    /* 校验传参 */
    if (2 > argc)
        exit(-1);
    /* 使用 calloc 申请内存 */
    base = (int *)calloc(argc - 1, sizeof(int));
    if (NULL == base)
    {
        printf("calloc error\n");
        exit(-1);
    }
    /* 将字符串转为 int 型数据存放在 base 指向的内存中 */
    for (i = 0; i < argc - 1; i++)
        base[i] = atoi(argv[i + 1]);
    /* 打印 base 数组中的数据 */
    printf("你输入的数据是: ");
    for (i = 0; i < argc - 1; i++)
        printf("%d ", base[i]);
    putchar('\n');
    /* 释放内存 */
    free(base);
    exit(0);
}

在这里插入图片描述

7.6.3 分配对其内存

C 函数库中还提供了一系列在堆上分配对齐内存的函数,对齐内存在某些应用场合非常有必要,常用于 分配对其内存的库函数有:posix_memalign()、aligned_alloc()、memalign()、valloc()、pvalloc(),它们的函数 原型如下所示:

#include <stdlib.h>
int 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 <malloc.h>
void *memalign(size_t alignment, size_t size);
void *pvalloc(size_t size);
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    int *base = NULL;
    int ret;
    /* 申请内存: 256 字节对齐 */
    ret = posix_memalign((void **)&base, 256, 1024);
    if (0 != ret)
    {
        printf("posix_memalign error\n");
        exit(-1);
    }
    /* 使用内存 */
    // base[0] = 0;
    // base[1] = 1;
    // base[2] = 2;
    // base[3] = 3;
    /* 释放内存 */
    free(base);
    exit(0);
}

aligned_alloc()函数

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    int *base = NULL;
    /* 申请内存: 256 字节对齐 */
    base = (int *)aligned_alloc(256, 256 * 4);
    if (base == NULL)
    {
        printf("aligned_alloc error\n");
        exit(-1);
    }
    /* 使用内存 */
    // base[0] = 0;
    // base[1] = 1;
    // base[2] = 2;
    // base[3] = 3;
    /* 释放内存 */
    free(base);
    exit(0);
}

memalign()函数

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
int main(int argc, char *argv[])
{
    int *base = NULL;
    /* 申请内存: 256 字节对齐 */
    base = (int *)memalign(256, 1024);
    if (base == NULL)
    {
        printf("memalign error\n");
        exit(-1);
    }
    /* 使用内存 */
    // base[0] = 0;
    // base[1] = 1;
    // base[2] = 2;
    // base[3] = 3;
    /* 释放内存 */
    free(base);
    exit(0);
}

valloc()函数

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    int *base = NULL;
    /* 申请内存: 1024 个字节 */
    base = (int *)valloc(1024);
    if (base == NULL)
    {
        printf("valloc error\n");
        exit(-1);
    }
    /* 使用内存 */
    // base[0] = 0;
    // base[1] = 1;
    // base[2] = 2;
    // base[3] = 3;
    /* 释放内存 */
    free(base);
    exit(0);
}

7.7 proc文件系统

proc 文件系统是一个虚拟文件系统,它以文件系统的方式为应用层访问系统内核数据提供了接口。

proc 文件系统是动态创建的,文件本身并不存在于磁盘 当中、只存在于内存当中,与 devfs 一样,都被称为虚拟文件系统。

7.7.1 proc文件系统的使用

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
    char buf[512] = {0};
    int fd;
    int ret;
    /* 打开文件 */
    fd = open("/proc/version", O_RDONLY);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 读取文件 */
    ret = read(fd, buf, sizeof(buf));
    if (-1 == ret)
    {
        perror("read error");
        exit(-1);
    }
    /* 打印信息 */
    puts(buf);
    /* 关闭文件 */
    close(fd);
    exit(0);
}

在这里插入图片描述

第八章 信号:基础

事实上,在很多应用程序当中,都会存在处理异步事件这种需求,而信号提供了一种处理异步事件的方法,所以信号机制在 Linux 早期版本中就已经提供了支持,随着 Linux 内核版本的更新迭代,其 对信号机制的支持更加完善。

8.1 基本概念

信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。

信号的目的是用来通信的

一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进 程间通信(IPC)的原始形式。信号可以由“谁”发出呢?以下列举的很多情况均可以产生信号:

  1. 硬件发生异常。如,除数为 0、数组访问越界导致引用 了无法访问的内存区域等
  2. 用于在终端下输入了能够产生信号的特殊字符。CTRL + C 组合按键可以产生中断信号;CTRL + Z 组合按键可以产生暂停信号
  3. 进程调用 kill() 。当然对此是有所限制的,接收 信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是 root 超级用 户。
  4. 用户可以通过 kill 命令将信号发送给其它进程。
  5. 发生了软件事件。进程所设置的定时器 已经超时、进程执行的 CPU 时间超限、进程的某个子进程退出等等情况。

信号由谁处理、怎么处理

信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:

  1. 忽略信号。但有两种信号却决不能被忽略,它们是 SIGKILL 和 SIGSTOP。
  2. 捕获信号。
  3. 执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理。对大多数信号来说,系统默认的处理方式就是终止该进程。

信号是异步的

产生信号的事件对进程而言是随机出现的

信号本质上是 int 类型数字编号

内核针对每个信号,都给其定 义了一个唯一的整数编号,从数字 1 开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏), 信号名字与信号编号乃是一一对应关系

8.2 信号的分类

Linux 系统下可对信号从两个不同的角度进行分类,从可靠性方面将信号分为可靠信号与不可靠信号; 而从实时性方面将信号分为实时信号与非实时信号

8.2.1 可靠信号与不可靠信号

Linux 下的不可靠信号问题主要指的是信号可能丢失。

在 Linux 系统下,信号值小于 SIGRTMIN (34)的信号都是不可靠信号

在这里插入图片描述

可靠信号支持排队,不会丢失,同时,信号的发送和绑定也出现了新版本,信号发送函数 sigqueue()及 信号绑定函数 sigaction()。

8.2.2 实时信号与非实时信号

实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的

非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

一般我们也把非实时信号(不可靠信号)称为标准信号

8.3 常见信号与默认行为

需要时再查。

8.4 进程对信号的处理

当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作。

8.4.1 signal()函数

signal()函数是 Linux 系统下设置信号处理方式最简单的接口,可将信号的 处理方式设置为捕获信号、忽略信号以及系统默认操作

#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
    printf("Received signal: %d\n", sig);
}
int 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);
}

在这里插入图片描述

8.4.2 sigaction()函数

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *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);
};
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
    printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
    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 (;;)
    {
    }
    exit(0);
}

在这里插入图片描述

8.5 向进程发送信号

一个进程可通过 kill()向另一个进程发送信号; 除了 kill()系统调用之外,Linux 系统还提供了系统调用 killpg()以及库函数 raise(),也可用于实现发送信号的功能

8.5.1 kill()函数

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

返回值:成功返回 0;失败将返回-1,并设置 errno。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    int pid;
    /* 判断传参个数 */
    if (2 > argc)
        exit(-1);
    /* 将传入的字符串转为整形数字 */
    pid = atoi(argv[1]);
    printf("pid: %d\n", pid);
    /* 向 pid 指定的进程发送信号 */
    if (-1 == kill(pid, SIGINT))
    {
        perror("kill error");
        exit(-1);
    }
    exit(0);
}

实验时并没有出现想要的结果

8.5.2 raise()

进程向自身发送信号

#include <signal.h>
int raise(int sig);
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
    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 秒发送一次
    }
    exit(0);
}

在这里插入图片描述

8.6 alarm()和pause()函数

8.6.1 alarm()函数

使用 alarm()函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM 信号,其函数原型如下所示:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

seconds:设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟。

返回值:如果在调用 alarm()时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0。

#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);
}

在这里插入图片描述

8.6.2 pause()函数

pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信 号处理函数并从其返回时,pause()才返回,在这种情况下,pause()返回-1,并且将 errno 设置为 EINTR。其 函数原型如下所示:

#include <unistd.h>
int pause(void);
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    puts("Alarm timeout");
}
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);
    /* 进入休眠状态 */
    pause();
    puts("休眠结束");
    exit(0);
}

在这里插入图片描述

8.7 信号集

能表示多个信号(一组信号)的数据类型——信号集

信号集其实就是 sigset_t 类型数据结构

# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
    unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;

8.7.1 初始化信号集

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);

8.7.2 向信号集中添加/删除信号

#include <signal.h>
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);

8.7.3 测试信号是否在信号集中

#include <signal.h>
int sigismember(const sigset_t *set, int signum);

8.8 获取信号的描述信息

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    printf("SIGINT 描述信息: %s\n", sys_siglist[SIGINT]);
    printf("SIGQUIT 描述信息: %s\n", sys_siglist[SIGQUIT]);
    printf("SIGBUS 描述信息: %s\n", sys_siglist[SIGBUS]);
    exit(0);
}

在这里插入图片描述

8.8.1 strsignal()函数

#include <string.h>
char *strsignal(int sig);
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    printf("SIGINT 描述信息: %s\n", strsignal(SIGINT));
    printf("SIGQUIT 描述信息: %s\n", strsignal(SIGQUIT));
    printf("SIGBUS 描述信息: %s\n", strsignal(SIGBUS));
    printf("编号为 1000 的描述信息: %s\n", strsignal(1000));
    exit(0);
}

在这里插入图片描述

8.8.2 psignal()函数

psignal()可以在标准错误(stderr)上输出信号描述信息,其函数原型如下所示:

#include <signal.h>
void psignal(int sig, const char *s);
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    psignal(SIGINT, "SIGINT 信号描述信息");
    psignal(SIGQUIT, "SIGQUIT 信号描述信息");
    psignal(SIGBUS, "SIGBUS 信号描述信息");
    exit(0);
}

在这里插入图片描述

8.9 信号掩码(阻塞信号传递)

内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号。当进程接收到一个属于 信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信 号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。

向信号掩码中添加一个信号,通常有如下几种方式:

  1. 当应用程序调用 signal()或 sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加 到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞; 当然对于 sigaction()而言,是否会如此,需要根据 sigaction()函数是否设置了 SA_NODEFER 标志 而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。
  2. 使用 sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该 组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通 过 sa_mask 参数进行设置,参考 8.4.2 小节内容。
  3. 除了以上两种方式之外,还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加/ 移除信号。
/* 向信号掩码中添加信号 */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    printf("执行信号处理函数...\n");
}
int main(void)
{
    struct sigaction sig = {0};
    sigset_t sig_set;
    /* 注册信号处理函数 */
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGINT, &sig, NULL))
        exit(-1);
    /* 信号集初始化 */
    sigemptyset(&sig_set);
    sigaddset(&sig_set, SIGINT);
    /* 向信号掩码中添加信号 */
    if (-1 == sigprocmask(SIG_BLOCK, &sig_set, NULL))
        exit(-1);
    /* 向自己发送信号 */
    raise(SIGINT);
    /* 休眠 2 秒 */
    sleep(2);
    printf("休眠结束\n");
    /* 从信号掩码中移除添加的信号 */
    if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
        exit(-1);
    exit(0);
}

从信号掩码中移除添加的信号后执行函数

在这里插入图片描述

//不向信号掩码中添加信号
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    printf("执行信号处理函数...\n");
}
int main(void)
{
    struct sigaction sig = {0};
    sigset_t sig_set;
    /* 注册信号处理函数 */
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGINT, &sig, NULL))
        exit(-1);
    /* 信号集初始化 */
    sigemptyset(&sig_set);
    sigaddset(&sig_set, SIGINT);

    /* 向自己发送信号 */
    raise(SIGINT);
    /* 休眠 2 秒 */
    sleep(2);
    printf("休眠结束\n");
    /* 从信号掩码中移除添加的信号 */
    if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
        exit(-1);
    exit(0);
}

在这里插入图片描述

8.10 阻塞等待信号sigsuspend()

#include <signal.h>
int sigsuspend(const sigset_t *mask);
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    printf("执行信号处理函数...\n");
}
int main(void)
{
    struct sigaction sig = {0};
    sigset_t new_mask, old_mask, wait_mask;
    /* 信号集初始化 */
    sigemptyset(&new_mask);
    sigaddset(&new_mask, SIGINT);
    sigemptyset(&wait_mask);
    /* 注册信号处理函数 */
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGINT, &sig, NULL))
        exit(-1);
    /* 向信号掩码中添加信号 */
    if (-1 == sigprocmask(SIG_BLOCK, &new_mask, &old_mask))
        exit(-1);
    /* 执行保护代码段 */
    puts("执行保护代码段");
    /******************/
    /* 挂起、等待信号唤醒 */
    // raise(SIGINT);  //加这里将正常执行
    if (-1 != sigsuspend(&wait_mask))
        exit(-1);

    /* 恢复信号掩码 */
    if (-1 == sigprocmask(SIG_SETMASK, &old_mask, NULL))
        exit(-1);
    exit(0);
}

在这里插入图片描述

8.11 实时信号

如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的 成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中, 为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()函数获取。

8.11.1 sigpending()函数

#include <signal.h>
int sigpending(sigset_t *set);

8.11.2 发送实时信号

等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数。

Linux 内核定义了 31 个不同的实时信号,信号编号范围为 34~64

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

union sigval 数据类型(共用体)如下所示:

typedef union sigval
{
    int sival_int;
    void *sival_ptr;
} sigval_t;

(1)发送进程使用 sigqueue()系统调用向另一个进程发送实时信号

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(int argc, char *argv[])
{
    union sigval sig_val;
    int pid;
    int sig;
    /* 判断传参个数 */
    if (3 > argc)
        exit(-1);
    /* 获取用户传递的参数 */
    pid = atoi(argv[1]);
    sig = atoi(argv[2]);
    printf("pid: %d\nsignal: %d\n", pid, sig);
    /* 发送信号 */
    sig_val.sival_int = 10; //伴随数据
    if (-1 == sigqueue(pid, sig, sig_val))
    {
        perror("sigqueue error");
        exit(-1);
    }
    puts("信号发送成功!");
    exit(0);
}

在这里插入图片描述

(2)接收进程使用 sigaction()函数为信号绑定处理函数

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

static void sig_handler(int sig, siginfo_t *info, void *context)
{
    sigval_t sig_val = info->si_value;
    printf("接收到实时信号: %d\n", sig);
    printf("伴随数据为: %d\n", sig_val.sival_int);
}
int main(int argc, char *argv[])
{
    struct sigaction sig = {0};
    int num;
    /* 判断传参个数 */
    if (2 > argc)
        exit(-1);
    /* 获取用户传递的参数 */
    num = atoi(argv[1]);
    /* 为实时信号绑定处理函数 */
    sig.sa_sigaction = sig_handler;
    sig.sa_flags = SA_SIGINFO;
    if (-1 == sigaction(num, &sig, NULL))
    {
        perror("sigaction error");
        exit(-1);
    }
    /* 死循环 */
    for (;;)
        sleep(1);
    exit(0);
}

8.12 异常退出abort()函数

对于异常退出程序,则一般使用 abort()库函数

强行退出

#include <stdlib.h>
void abort(void);
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    printf("接收到信号: %d\n", sig);
}
int main(int argc, char *argv[])
{
    struct sigaction sig = {0};
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGABRT, &sig, NULL))
    {
        perror("sigaction error");
        exit(-1);
    }
    sleep(2);
    abort(); // 调用 abort
    for (;;)
        sleep(1);
    exit(0);
}

在这里插入图片描述

第九章 进程

9.1 进程与程序

9.1.1 main()函数由谁调用?

操作系统下的应用程序在运行 main()函数之前需要先执行一段引导代码,最终由这段引导代码去调用应用 程序的 main()函数。链接器

当执行应用程序时,在 Linux 下输入可执行文件的相对路径或绝对路径就可以运行该程序。程序运行需要通过操作系统的加载器来实现

9.1.2 程序如何结束?

进程终止大体上分为正常终止和异常终止

正常终止包括:

  • main()函数中通过 return 语句返回来终止进程;
  • 应用程序中调用 exit()函数终止进程;
  • 应用程序中调用_exit()或_Exit()终止进程;

异常终止包括:

  • abort()函数
  • 进程接收到一个信号,譬如 SIGKILL 信号。

注册进程终止处理函数 atexit()

atexit()库函数用于注册一个进程在正常终止时要调用的函数,其函数原型如下所示:

#include <stdlib.h>
int atexit(void (*function)(void));

返回值:成功返回 0;失败返回非 0。

#include <stdio.h>
#include <stdlib.h>
static void bye(void)
{
    puts("Goodbye!");
}
int main(int argc, char *argv[])
{
    if (atexit(bye))
    {
        fprintf(stderr, "cannot set exit function\n");
        exit(-1);
    }
    exit(0);
}

在这里插入图片描述

9.1.3 何为进程?

进程其实就是一个可执行程序的实例

进程是一个动态过程,而非静态文件,它是程序的一次运行过程

9.1.4 进程号

ps -aux 查看 PID

可通过系统调用 getpid()来获取本进程的进程号,其函数原型如下所示:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
    pid_t pid = getpid();
    printf("本进程的 PID 为: %d\n", pid);
    exit(0);
}

在这里插入图片描述

除了 getpid()用于获取本进程的进程号之外,还可以使用 getppid()系统调用获取父进程的进程号,其函 数原型如下所示:

#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
    pid_t pid = getpid(); //获取本进程 pid
    printf("本进程的 PID 为: %d\n", pid);
    pid = getppid(); //获取父进程 pid
    printf("父进程的 PID 为: %d\n", pid);
    exit(0);
}

在这里插入图片描述

9.2 进程的环境变量

env 命令查看到 shell 进程的所有环境变量

使用 export 命令还可以添加一个新的环境变量或删除一个环境变量:

export LINUX_APP=123456 # 添加 LINUX_APP 环境变量
export -n LINUX_APP # 删除 LINUX_APP 环境变量

9.2.1 应用程序中获取环境变量

进程的环境变量是从其父进程中继承 过来的

#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[])
{
    int i;
    /* 打印进程的环境变量 */
    for (i = 0; NULL != environ[i]; i++)
        puts(environ[i]);
    exit(0);
}

在这里插入图片描述

获取指定环境变量 getenv()

如果只想要获取某个指定的环境变量,可以使用库函数 getenv(),其函数原型如下所示:

#include <stdlib.h>
char *getenv(const char *name);
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    const char *str_val = NULL;
    if (2 > argc)
    {
        fprintf(stderr, "Error: 请传入环境变量名称\n");
        exit(-1);
    }
    /* 获取环境变量 */
    str_val = getenv(argv[1]);
    if (NULL == str_val)
    {
        fprintf(stderr, "Error: 不存在[%s]环境变量\n", argv[1]);
        exit(-1);
    }
    /* 打印环境变量的值 */
    printf("环境变量的值: %s\n", str_val);
    exit(0);
}

在这里插入图片描述

9.2.2 添加/删除/修改环境变量

putenv()函数

putenv()函数可向进程的环境变量数组中添加一个新的环境变量,或者修改一个已经存在的环境变量对 应的值,其函数原型如下所示:

#include <stdlib.h>
int putenv(char *string);

setenv()函数

setenv()函数可以替代 putenv()函数

#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);

9.2.3 清空环境变量

清除环境变量表中的所有变量,然后再进行重建

可以通过将全局变量 environ 赋值为 NULL 来清空所有变量。

environ = NULL;

也可通过 clearenv()函数来操作

#include <stdlib.h>
int clearenv(void);

9.2.4 环境变量的作用

环境变量常见的用途之一是在 shell 中,每一个环境变量都有它所表示的含义,譬如 HOME 环境变量表 示用户的家目录,USER 环境变量表示当前用户名,SHELL 环境变量表示 shell 解析器名称,PWD 环境变 量表示当前所在目录等,在我们自己的应用程序当中,也可以使用进程的环境变量。

9.3 进程的内存布局

历史沿袭至今,C 语言程序一直都是由以下几部分组成的:

  • 正文段。也可称为代码段。这是 CPU 执行的机器语言指令部分,文本段具有只读属性
  • 初始化数据段。通常将此段称为数据段。包含了显式初始化的全局变量和静态变量
  • 未初始化数据段。包含了未进行显式初始化的全局变量和静态变量,通常将此段称为 bss 段
  • 栈。函数内的局部变量以及每次函数调用时所需保存的信息
  • 堆。如 malloc()

可以用 size 查看

在这里插入图片描述

在这里插入图片描述

9.4 进程的虚拟地址空间

所有应用程序运行在自己的虚拟地址空间中,使得进程的虚拟地址空 间和物理地址空间隔离开来

9.5 fork()创建子进程

#include <unistd.h>
pid_t fork(void);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    pid_t pid;
    pid = fork();
    switch (pid)
    {
    case -1:
        perror("fork error");
        exit(-1);
    case 0:
        printf("这是子进程打印信息<pid: %d, 父进程 pid: %d>\n",
               getpid(), getppid());
        _exit(0); //子进程使用_exit()退出
    default:
        printf("这是父进程打印信息<pid: %d, 子进程 pid: %d>\n",
               getpid(), pid);
        exit(0);
    }
}

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    pid_t pid;
    pid = fork();
    switch (pid)
    {
    case -1:
        perror("fork error");
        exit(-1);
    case 0:
        printf("这是子进程打印信息\n");
        printf("%d\n", pid);
        _exit(0);
    default:
        printf("这是父进程打印信息\n");
        printf("%d\n", pid);
        exit(0);
    }
}

在这里插入图片描述

子进程被创建出来之后,便是一个独立的进程,拥有自己独立的进程空间,系统内唯一的进程号,拥有 自己独立的 PCB(进程控制块),子进程会被内核同等调度执行,参与到系统的进程调度中。

9.6 父、子进程间的文件共享

调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
    pid_t pid;
    int fd;
    int i;
    fd = open("./test.txt", O_RDWR | O_TRUNC);
    if (0 > fd)
    {
        perror("open error");
        exit(-1);
    }
    pid = fork();
    switch (pid)
    {
    case -1:
        perror("fork error");
        close(fd);
        exit(-1);
    case 0:
        /* 子进程 */
        for (i = 0; i < 4; i++) //循环写入 4 次
            write(fd, "1122", 4);
        close(fd);
        _exit(0);
    default:
        /* 父进程 */
        for (i = 0; i < 4; i++) //循环写入 4 次
            write(fd, "AABB", 4);
        close(fd);
        exit(0);
    }
}

验证的便是两个进程对文件的写入操作是分别各自写入、还是每次都在文件末尾接续写入

fork() 在父进程打开文件后才调用。

在这里插入图片描述

父进程在调用 fork()之后,此时父进程和子进程都去打开同一个文件,然后再对文件进行写入操作

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
    pid_t pid;
    int fd;
    int i;
    pid = fork();
    switch (pid)
    {
    case -1:
        perror("fork error");
        exit(-1);
    case 0:
        /* 子进程 */
        fd = open("./test.txt", O_WRONLY);
        if (0 > fd)
        {
            perror("open error");
            _exit(-1);
        }
        for (i = 0; i < 4; i++) //循环写入 4 次
            write(fd, "1122", 4);
        close(fd);
        _exit(0);
    default:
        /* 父进程 */
        fd = open("./test.txt", O_WRONLY);
        if (0 > fd)
        {
            perror("open error");
            exit(-1);
        }
        for (i = 0; i < 4; i++) //循环写入 4 次
            write(fd, "AABB", 4);
        close(fd);
        exit(0);
    }
}

会出现覆盖的情况

在这里插入图片描述

9.7 系统调用vfork()

除了 fork()系统调用之外,Linux 系统还提供了 vfork()系统调用用于创建子进程

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);

可以将 fork()认作对父进程的数据段、堆段、栈段以及其它一些数据结构创建拷贝,使用 fork()系统调用的代价是很大的

vfork()系统调用效率要高于 fork()函数

vfork()与 fork()函数主要有以下两个区别:

  • vfork()函数并不会将父进程的地址空间完全复制到子进程 中,因为子进程会立即调用 exec(或_exit)
  • vfork()保证子进程先运行,子进程调用 exec 之后父进程才可能被调度运行

但是 vfork()可能会导致一些难以察觉的程序 bug

除非速度绝对重要的场合, 我们的程序当中应舍弃 vfork()而使用 fork()。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
    pid_t pid;
    int num = 100;
    pid = vfork();
    switch (pid)
    {
    case -1:
        perror("vfork error");
        exit(-1);
    case 0:
        /* 子进程 */
        printf("子进程打印信息\n");
        printf("子进程打印 num: %d\n", num);
        _exit(0);
    default:
        /* 父进程 */
        printf("父进程打印信息\n");
        printf("父进程打印 num: %d\n", num);
        exit(0);
    }
}

在这里插入图片描述

9.8 fork()之后的竞争条件

父子进程的执行顺序不确定

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
static void sig_handler(int sig)
{
    printf("接收到信号\n");
}
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())
    {
    case -1:
        perror("fork error");
        exit(-1);
    case 0:
        /* 子进程 */
        printf("子进程开始执行\n");
        printf("子进程打印信息\n");
        printf("~~~~~~~~~~~~~~~\n");
        sleep(2);
        kill(getppid(), SIGUSR1); //发送信号给父进程、唤醒它
        _exit(0);
    default:
        /* 父进程 */
        if (-1 != sigsuspend(&wait_mask)) //挂起、阻塞
            exit(-1);
        printf("父进程开始执行\n");
        printf("父进程打印信息\n");
        exit(0);
    }
}

在这里插入图片描述

9.9 进程的诞生与终止

9.9.1 进程的诞生

Linux 系统下的所有进程都是由其父进程创建而来,譬如在 shell 终端通过命令的方式执行一个程序./app,那么 app 进程就是由 shell 终端进程创建出来的,shell 终端就是该进程的父进程。

init 进程是由内核启动,因此理论上说它没有父进程,init 进程的 PID 总是为 1。

9.9.2 进程的终止

通常,进程有两种终止方式:异常终止和正常终止

exit()函数会执行的动作如下:

  1. 如果程序中注册了进程终止处理函数,那么会调用终止处理函数。在 9.1.2 小节给大家介绍如何注 册进程的终止处理函数;
  2. 刷新 stdio 流缓冲区。
  3. 执行_exit()系统调用。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("Hello World!\n");
    switch (fork())
    {
    case -1:
        perror("fork error");
        exit(-1);
    case 0:
        /* 子进程 */
        exit(0);
    default:
        /* 父进程 */
        exit(0);
    }
}

在复制进程之前缓冲区被刷空了

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("Hello World!");
    switch (fork())
    {
    case -1:
        perror("fork error");
        exit(-1);
    case 0:
        /* 子进程 */
        exit(0);
    default:
        /* 父进程 */
        exit(0);
    }
}

缓冲区还没被刷新,exit(0) 刷新

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("Hello World!");
    switch (fork())
    {
    case -1:
        perror("fork error");
        exit(-1);
    case 0:
        /* 子进程 */
        _exit(0);
    default:
        /* 父进程 */
        _exit(0);
    }
}

_exit() 函数没有刷新操作

在这里插入图片描述

9.10 监视子进程

在很多应用程序的设计中,父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视。

9.10.1 wait()函数

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int main(void)
{
    int status;
    int ret;
    int i;
    /* 循环创建 3 个子进程 */
    for (i = 1; i <= 3; i++)
    {
        switch (fork())
        {
        case -1:
            perror("fork error");
            exit(-1);
        case 0:
            /* 子进程 */
            printf("子进程<%d>被创建\n", getpid());
            sleep(i);
            _exit(i);
        default:
            /* 父进程 */
            break;
        }
    }
    sleep(1);
    printf("~~~~~~~~~~~~~~\n");
    for (i = 1; i <= 3; i++)
    {
        ret = wait(&status);
        if (-1 == ret)
        {
            if (ECHILD == errno)
            {
                printf("没有需要等待回收的子进程\n");
                exit(0);
            }
            else
            {
                perror("wait error");
                exit(-1);
            }
        }
        printf("回收子进程<%d>, 终止状态<%d>\n", ret,
               WEXITSTATUS(status));
    }
    exit(0);
}

在这里插入图片描述

9.10.2 waitpid()函数

使用 wait()系统调用存在着一些限制,这些限制包括如下:

  1. 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
  2. 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
  3. 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止 (注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就 无能为力了。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int main(void)
{
    int status;
    int ret;
    int i;
    /* 循环创建 3 个子进程 */
    for (i = 1; i <= 3; i++)
    {
        switch (fork())
        {
        case -1:
            perror("fork error");
            exit(-1);
        case 0:
            /* 子进程 */
            printf("子进程<%d>被创建\n", getpid());
            sleep(i);
            _exit(i);
        default:
            /* 父进程 */
            break;
        }
    }
    sleep(1);
    printf("~~~~~~~~~~~~~~\n");
    for (i = 1; i <= 3; i++)
    {
        ret = waitpid(-1, &status, 0);
        if (-1 == ret)
        {
            if (ECHILD == errno)
            {
                printf("没有需要等待回收的子进程\n");
                exit(0);
            }
            else
            {
                perror("wait error");
                exit(-1);
            }
        }
        printf("回收子进程<%d>, 终止状态<%d>\n", ret,
               WEXITSTATUS(status));
    }
    exit(0);
}

9.10.3 waitid()函数

waitid()提供了更多的扩展功能。用时再查

9.10.4 僵尸进程与孤儿进程

孤儿进程

父进程先于子进程结束,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    /* 创建子进程 */
    switch (fork())
    {
    case -1:
        perror("fork error");
        exit(-1);
    case 0:
        /* 子进程 */
        printf("子进程<%d>被创建, 父进程<%d>\n", getpid(), getppid());
        sleep(3);                          //休眠 3 秒钟等父进程结束
        printf("父进程<%d>\n", getppid()); //再次获取父进程 pid
        _exit(0);
    default:
        /* 父进程 */
        break;
    }
    sleep(1); //休眠 1 秒
    printf("父进程结束!\n");
    exit(0);
}

在这里插入图片描述

僵尸进程

进程结束之后,通常需要其父进程为其“收尸”,回收子进程占用的一些内存资源,父进程通过调用 wait()(或其变体 waitpid()、waitid()等)函数回收子进程资源,归还给系统。

如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。

如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并 自动调用 wait(),故而从系统中移除僵尸进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    /* 创建子进程 */
    switch (fork())
    {
    case -1:
        perror("fork error");
        exit(-1);
    case 0:
        /* 子进程 */
        printf("子进程<%d>被创建\n", getpid());
        sleep(1);
        printf("子进程结束\n");
        _exit(0);
    default:
        /* 父进程 */
        break;
    }
    for (;;)
        sleep(1);
    exit(0);
}

在这里插入图片描述

父进程结束后子进程也结束了。

9.10.5 SIGCHLD信号

子进程的终止属于异步事件,父进程事先是无法预知的,如果父进程有自己需要做的事情,它不能一直 wait()阻塞等待子进程终止(或轮训),这样父进程将啥事也做不了,那么有什么办法来解决这样的尴尬情况,当然有办法,那就是通过 SIGCHLD 信号。

父进程会收到 SIGCHLD 信号,SIGCHLD 信号的系统默认处理方式是将其忽略,所以我们要捕获它、绑定信号处理函数,在信号处理函数中调用 wait()收回子进程,回收完毕之后再回到父进程自己的工作流程中。

例子有误

9.11 执行新程序

在前面已经提到了 exec 函数,当子进程的工作不再是运行父进程的代码段,而是运行另一个新程序的代码,那么这个时候子进程可以通过 exec 函数来实现运行另一个新的程序。

9.11.1 execve()函数

系统调用 execve()可以将新程序加载到某一进程的内存空间,通过调用 execve()函数将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main()函数开始执行。

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);

filename:参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是 相对路径。

argv:参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组,该数组对应于 main(int argc, char *argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。 argv[0]对应的便是新程序自身路径名。

envp:参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程 序的 environ(系统的) 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value。

在这里插入图片描述

9.11.2 exec库函数

exec 族函数包括多个不同的函数,这些函数命名都以 exec 为前缀

9.11.3 exec族函数使用示例

举例:

execl()函数运行 ls 命令。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    execl("/bin/ls", "ls", "-a", "-l", NULL);
    perror("execl error");
    exit(-1);
}

在这里插入图片描述

9.11.4 system()函数

使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令

#include <stdlib.h>
int system(const char *command);

回想起os课设里面掺 bash 的 git 脚本

9.12 进程状态与进程关系

9.12.1 进程状态

Linux 系统下进程通常存在 6 种不同的状态:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。

9.12.2 进程关系

在 Linux 系统下,每个进程都有自己唯一的标识:进程号(进程 ID、PID),也有自己的生命周期,进程都有自己的父进程、而父进程也有父进程,这就形成了一 个以 init 进程为根的进程家族树;当子进程终止时,父进程会得到通知并能取得子进程的退出状态。

1、无关系

2、父子进程关系

如果“生父” 先与子进程结束,那么 init 进程(“养父”)就会成为子进程的父进程,它们之间同样也是父子进程关系。

3、进程组

有了进程组的概念之后,就可以将这 100 个进程设置为一个进程组,终止这 100 个进程只需要终止该进程组即可。

通过系统调用 getpgrp()或 getpgid()可以获取进程对应的进程组 ID,其函数原型如下所示:

#include <unistd.h>
pid_t getpgid(pid_t pid);
pid_t getpgrp(void);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    pid_t pid = getpid();
    printf("进程组 ID<%d>---getpgrp()\n", getpgrp());
    printf("进程组 ID<%d>---getpgid(0)\n", getpgid(0));
    printf("进程组 ID<%d>---getpgid(%d)\n", getpgid(pid), pid);
    exit(0);
}

在这里插入图片描述

4、会话

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("会话 ID<%d>\n", getsid(0));
    exit(0);
}

在这里插入图片描述

9.13 守护进程

9.13.1 何为守护进程

守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点:

  1. 长期运行。守护进程是一种生存期很长的一种进程,它们一般在系统启动时开始运行,除非强行终止,否则直到系统关机都会保持运行。
  2. 与控制终端脱离。在 Linux 中,系统与用户交互的界面称为终端,普通进程都是和运行该进程的终端相绑定的;但守护进程脱离终端并且在后台运行。

守护进程是一种很有用的进程。Linux 中大多数服务器就是用守护进程实现的。

9.13.2 编写守护进程程序

编写守护进程一般包含如下几个步骤:

\1) 创建子进程、终止父进程

\2) 子进程调用 setsid 创建会话

\3) 将工作目录更改为根目录

\4) 重设文件权限掩码 umask

\5) 关闭不再需要的文件描述符

\6) 将文件描述符号为 0、1、2 定位到/dev/null

\7) 其它:忽略 SIGCHLD 信号

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
int main(void)
{
    pid_t pid;
    int i;
    /* 创建子进程 */
    pid = fork();
    if (0 > pid)
    {
        perror("fork error");
        exit(-1);
    }
    else if (0 < pid) //父进程
        exit(0);      //直接退出
    /*
     *子进程
     */
    /* 1.创建新的会话、脱离控制终端 */
    if (0 > setsid())
    {
        perror("setsid error");
        exit(-1);
    }
    /* 2.设置当前工作目录为根目录 */
    if (0 > chdir("/"))
    {
        perror("chdir error");
        exit(-1);
    }
    /* 3.重设文件权限掩码 umask */
    umask(0);
    /* 4.关闭所有文件描述符 */
    for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
        close(i);
    /* 5.将文件描述符号为 0、1、2 定位到/dev/null */
    open("/dev/null", O_RDWR);
    dup(0);
    dup(0);
    /* 6.忽略 SIGCHLD 信号 */
    signal(SIGCHLD, SIG_IGN);
    /* 正式进入到守护进程 */
    for (;;)
    {
        sleep(1);
        puts("守护进程运行中......");
    }
    exit(0);
}

在这里插入图片描述

9.13.3 SIGHUP信号

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
int main(void)
{
    signal(SIGHUP, SIG_IGN);
    for (;;)
    {
        sleep(1);
        puts("进程运行中......");
    }
}

成功运行之后,关闭终端,再重新打开终端,使用 ps 命令查看进程依然还在运行,但此时它已经变成了守护进程,脱离了控制终端。所以由此可知, 当程序当中忽略 SIGHUP 信号之后,进程不会随着终端退出而退出。

9.14 单例模式运行

通常情况下,一个程序可以被多次执行,即程序在还没有结束的情况下,又再次执行该程序,也就是系统中同时存在多个该程序的实例化对象(进程)

但对于有些程序设计来说,不允许出现这种情况,程序只能被执行一次,只要该程序没有结束,就无法再次运行,我们把这种情况称为单例模式运行。譬如系统中守护进程,这些守护进程一般都是服务器进程, 服务器程序只需要运行一次即可,多次同时运行并没 有意义、甚至还会带来错误。

9.14.1 通过文件存在与否进行判断

用一个文件的存在与否来做标志,在程序运行正式代码之前,先判断一个特定的文件是否存在,如果存在则表明进程已经运行。当程序结束时再删除该文件

#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#define LOCK_FILE "./testApp.lock"
static void delete_file(void)
{
    remove(LOCK_FILE);
}
int main(void)
{
    /* 打开文件 */
    int fd = open(LOCK_FILE, O_RDONLY | O_CREAT | O_EXCL, 0666);
    if (-1 == fd)
    {
        fputs("不能重复执行该程序!\n", stderr);
        exit(-1);
    }
    /* 注册进程终止处理函数 */
    if (atexit(delete_file))
        exit(-1);
    puts("程序运行中...");
    sleep(10);
    puts("程序结束");
    close(fd); //关闭文件
    exit(0);
}

在这里插入图片描述

9.14.2 使用文件锁

#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#define LOCK_FILE "./testApp.pid"
int main(void)
{
    char str[20] = {0};
    int fd;
    /* 打开 lock 文件,如果文件不存在则创建 */
    fd = open(LOCK_FILE, O_WRONLY | O_CREAT, 0666);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 以非阻塞方式获取文件锁 */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB))
    {
        fputs("不能重复执行该程序!\n", stderr);
        close(fd);
        exit(-1);
    }
    puts("程序运行中...");
    ftruncate(fd, 0); //将文件长度截断为 0
    sprintf(str, "%d\n", getpid());
    write(fd, str, strlen(str)); //写入 pid
    for (;;)
        sleep(1);
    exit(0);
}

第十章 进程间通信简介

10.1 进程间通信简介

两个进程之间的通信

10.2 进程间通信的机制有哪些?

Linux 内核提供了多种 IPC 机制

在这里插入图片描述

10.3 管道和FIFO

管道包括三种:

  1. 普通管道 pipe:通常有两种限制,一是单工,数据只能单向传输;二是只能在父子或者兄弟进程间使用;
  2. 流管道 s_pipe:去除了普通管道的第一种限制,为半双工,可以双向传输;只能在父子或兄弟进程间使用;
  3. 有名管道 name_pipe(FIFO):去除了普通管道的第二种限制,并且允许在不相关(不是父子或兄弟关系)的进程间进行通讯。

10.4 信号

关于信号相关的内容在本书第八章中给大家介绍过,用于通知接收信号的进程有某种事件发生,所以可用于进程间通信;除了用于进程间通信之外,进程还可以发送信号给进程本身。

10.5 消息队列

消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺陷。

10.6 信号量

信号量是一个计数器,它常作为一种锁机制。

10.7 共享内存

共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但其它的多个进程都可以访问

10.8 套接字(Socket)

Socket 是一种 IPC 方法,是基于网络的 IPC 方法,允许位于同一主机(计算机)或使用网络连接起来的不同主机上的应用程序之间交换数据,说白了就是网络通信

第十一章 线程

11.1 线程概述

11.1.1 线程概念

什么是线程?

线程是参与系统调度的最小单位。是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。

线程是如何创建起来的?

当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程。应用程序都是以 main()做为入口开始运行的,所以 main()函数就是主线程的入口函数,main()函数所执行的任务就是主线程需要执行的任务。

线程的特点?

进程不能运行,真正运行的是进程中的线程。

同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack,我们称为线程栈),自己的寄存器环境(register context)、自己的线程本地存储(thread-local storage)。

线程与进程?

进程创建多个子进程可以实现并发处理多任务,多线程同样也可以实现 (一个多线程进程)并发处理多任务的需求,那我们究竟选择哪种处理方式呢?

多进程编程的劣势:

  1. 进程间切换开销大。
  2. 进程间通信较为麻烦。

11.1.2 并发和并行

相比于串行和并行,并发强调的是一种时分复用

与串行的区别在于,它不必等待上一个任务完成之后再做下一个任务,可以打断当前执行的任务切换执行下一个任务

11.2 线程ID

一个线程可通过库函数 pthread_self()来获取自己的线程 ID,其函数原型如下所示:

#include <pthread.h>
pthread_t pthread_self(void);

可以使用 pthread_equal()函数来检查两个线程 ID 是否相等,其函数原型如下所示:

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

11.3 创建线程

主线程可以使用库函数 pthread_create()负责创建一个新的线程,创建出来的新线程被称为主线程的子线程,其函数原型如下所示:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
    printf("新线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
    return (void *)0;
}
int main(void)
{
    pthread_t tid;
    int ret;
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "Error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
    sleep(1);
    exit(0);
}

在这里插入图片描述

11.4 终止线程

pthread_exit()函数将终止调用它的线程

#include <pthread.h>
void pthread_exit(void *retval);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
    sleep(1);
    printf("新线程 start\n");
    printf("新线程 end\n");
    pthread_exit(NULL);
}
int main(void)
{
    pthread_t tid;
    int ret;
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "Error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("主线程 end\n");
    pthread_exit(NULL);
    exit(0);
}

在这里插入图片描述

主线程调用 pthread_exit()终止之后,整个进程并没有结束,而新线程还在继续运行。

11.5 回收线程

在父、子进程当中,父进程可通过 wait()函数(或其变体 waitpid())阻塞等待子进程退出并获取其终止状态,回收子进程资源;而在线程当中,也需要如此,通过调用 pthread_join()函数来阻塞等待线程的终止, 并获取线程的退出码,回收线程资源;

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
    printf("新线程 start\n");
    sleep(2);
    printf("新线程 end\n");
    pthread_exit((void *)9);
}
int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid, &tret);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);
    exit(0);
}

在这里插入图片描述

11.6 取消线程

在通常情况下,进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程 中会调用 pthread_exit()退出,或在线程 start 函数执行 return 语句退出。

有时候,在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出

11.6.1 取消一个线程

通过调用 pthread_cancel()库函数向一个指定的线程发送取消请求,其函数原型如下所示:

#include <pthread.h>
int pthread_cancel(pthread_t thread);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
    printf("新线程--running\n");
    for (;;)
        sleep(1);
    return (void *)0;
}
int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    sleep(1);
    /* 向新线程发送取消请求 */
    ret = pthread_cancel(tid);
    if (ret)
    {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);
    exit(0);
}

在这里插入图片描述

11.6.2 取消状态以及类型

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
    /* 设置为不可被取消 */
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    for (;;)
    {
        printf("新线程--running\n");
        sleep(2);
    }
    return (void *)0;
}
int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    sleep(1);
    /* 向新线程发送取消请求 */
    ret = pthread_cancel(tid);
    if (ret)
    {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);
    exit(0);
}

不可被取消

在这里插入图片描述

注释掉不可被取消后:

在这里插入图片描述

11.6.3 取消点

若将线程的取消性类型设置为 PTHREAD_CANCEL_DEFERRED 时(线程可以取消状态下),收到其 它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。

取消点有一些,用时再查

sleep()函数可以作为取消点,当新线程接收到取消请求之后,便会立马退出

上面的例子删去 sleep() 后死循环

11.6.4 线程可取消性的检测

可以使用 pthread_testcancel(),该函数目的很简单,就是产生一个取消点

#include <pthread.h>
void pthread_testcancel(void);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
    printf("新线程--start run\n");
    for (;;)
    {
        pthread_testcancel();
    }
    return (void *)0;
}
int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    sleep(1);
    /* 向新线程发送取消请求 */
    ret = pthread_cancel(tid);
    if (ret)
    {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);
    exit(0);
}

在这里插入图片描述

11.7 分离线程

有 时,程序员并不关系线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。

#include <pthread.h>
int pthread_detach(pthread_t thread);

一个线程既可以将另一个线程分离,同时也可以将自己分离

pthread_detach(pthread_self());
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
    int ret;
    /* 自行分离 */
    ret = pthread_detach(pthread_self());
    if (ret)
    {
        fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
        return NULL;
    }
    printf("新线程 start\n");
    sleep(2); //休眠 2 秒钟
    printf("新线程 end\n");
    pthread_exit(NULL);
}
int main(void)
{
    pthread_t tid;
    int ret;
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    sleep(1); //休眠 1 秒钟
    /* 等待新线程终止 */
    ret = pthread_join(tid, NULL);
    if (ret)
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
    pthread_exit(NULL);
}

在这里插入图片描述

11.8 注册线程清理处理函数

线程通过函数 pthread_cleanup_push()和 pthread_cleanup_pop()分别负责向调用线程的清理函数栈中添加和移除清理函数,函数原型如下所示:

#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void cleanup(void *arg)
{
    printf("cleanup: %s\n", (char *)arg);
}
static void *new_thread_start(void *arg)
{
    printf("新线程--start run\n");
    pthread_cleanup_push(cleanup, "第 1 次调用");
    pthread_cleanup_push(cleanup, "第 2 次调用");
    pthread_cleanup_push(cleanup, "第 3 次调用");
    sleep(2);
    pthread_exit((void *)0); //线程终止
    /* 为了与 pthread_cleanup_push 配对,不添加程序编译会通不过 */
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
}
int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);
    exit(0);
}

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void cleanup(void *arg)
{
    printf("cleanup: %s\n", (char *)arg);
}
static void *new_thread_start(void *arg)
{
    printf("新线程--start run\n");
    pthread_cleanup_push(cleanup, "第 1 次调用");
    pthread_cleanup_push(cleanup, "第 2 次调用");
    pthread_cleanup_push(cleanup, "第 3 次调用");
    pthread_cleanup_pop(1); //执行最顶层的清理函数
    printf("~~~~~~~~~~~~~~~~~\n");
    sleep(2);
    pthread_exit((void *)0); //线程终止
    /* 为了与 pthread_cleanup_push 配对 */
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
}
int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);
    exit(0);
}

在这里插入图片描述

11.9 线程属性

#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

11.9.1 线程栈属性

每个线程都有自己的栈空间,pthread_attr_t 数据结构中定义了栈的起始地址以及栈大小

#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
static void *new_thread_start(void *arg)
{
    puts("Hello World!");
    return (void *)0;
}
int main(int argc, char *argv[])
{
    pthread_attr_t attr;
    size_t stacksize;
    pthread_t tid;
    int ret;
    /* 对 attr 对象进行初始化 */
    pthread_attr_init(&attr);
    /* 设置栈大小为 4K */
    pthread_attr_setstacksize(&attr, 4096);
    /* 创建新线程 */
    ret = pthread_create(&tid, &attr, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待新线程终止 */
    ret = pthread_join(tid, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 销毁 attr 对象 */
    pthread_attr_destroy(&attr);
    exit(0);
}

创建新的线程,将线程的栈大小设置为 4Kbyte。

在这里插入图片描述

11.9.2 分离状态属性

如果对现已创建的某个线程的终止状态不感兴趣,可以使用 pthread_detach()函数将其分离,那么该线程在退出时,操作系统会自动回收它所占用的资源。

#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
    puts("Hello World!");
    return (void *)0;
}
int main(int argc, char *argv[])
{
    pthread_attr_t attr;
    pthread_t tid;
    int ret;
    /* 对 attr 对象进行初始化 */
    pthread_attr_init(&attr);
    /* 设置以分离状态启动线程 */
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    /* 创建新线程 */
    ret = pthread_create(&tid, &attr, new_thread_start, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    sleep(1);
    /* 销毁 attr 对象 */
    pthread_attr_destroy(&attr);
    exit(0);
}

以分离状态启动线程

在这里插入图片描述

11.10 线程安全

11.10.1 线程栈

进程中创建的每个线程都有自己的栈地址空间,将其称为线程栈。

11.10.2 可重入函数

单线程程序只有一条执行流(一个线程就是一条执行流),贯穿程序始终;而对于多线程程序而言,同一进程却存在多条独立、并发的执行流。

进程中执行流的数量除了与线程有关之外,与信号处理也有关联。

如果一个函数被同一进程的多个不同的执行流同时调用,每次函数调用总是能产生正确的结果(或者叫产生预期的结果),把这样的函数就称为可重入函数。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void func(void)
{
    /*...... */
}
static void sig_handler(int sig)
{
    func();
}
int 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 (;;)
    {
        printf("b\n");
        func();
    }
    exit(0);
}

在这里插入图片描述

11.10.3 线程安全函数

一个函数被多个线程同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数。

11.10.4 一次性初始化

在多线程编程环境下,有些代码段只需要执行一次,譬如一些初始化相关的代码段,通常比较容易想到的就是将其放在 main()主函数进行初始化,这样也就是意味着该段代码只在主线程中被调用,只执行过一 次。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void initialize_once(void)
{
    printf("initialize_once 被执行: 线程 ID<%lu>\n", pthread_self());
}
static void func(void)
{
    pthread_once(&once, initialize_once); //执行一次性初始化函数
    printf("函数 func 执行完毕.\n");
}
static void *thread_start(void *arg)
{
    printf("线程%d 被创建: 线程 ID<%lu>\n", *((int *)arg), pthread_self());
    func();             //调用函数 func
    pthread_exit(NULL); //线程终止
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(void)
{
    pthread_t tid[5];
    int j;
    /* 创建 5 个线程 */
    for (j = 0; j < 5; j++)
        pthread_create(&tid[j], NULL, thread_start, &nums[j]);
    /* 等待线程结束 */
    for (j = 0; j < 5; j++)
        pthread_join(tid[j], NULL); //回收线程
    exit(0);
}

在这里插入图片描述

11.10.5 线程特有数据

线程特有数据也称为线程私有数据

没怎么看懂

11.10.6 线程局部存储

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
static __thread char buf[100];
static void *thread_start(void *arg)
{
    strcpy(buf, "Child Thread\n");
    printf("子线程: buf (%p) = %s", buf, buf);
    pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
    pthread_t tid;
    int ret;
    strcpy(buf, "Main Thread\n");
    /* 创建子线程 */
    if (ret = pthread_create(&tid, NULL, thread_start, NULL))
    {
        fprintf(stderr, "pthread_create error: %d\n", ret);
        exit(-1);
    }
    /* 等待回收子线程 */
    if (ret = pthread_join(tid, NULL))
    {
        fprintf(stderr, "pthread_join error: %d\n", ret);
        exit(-1);
    }
    printf("主线程: buf (%p) = %s", buf, buf);
    exit(0);
}

在这里插入图片描述

11.11 更多细节问题

本小节将对线程各方面的细节做深入讨论,其主要包括线程与信号之间牵扯的问题、线程与进程控制 (fork()、exec()、exit()等)之间的交互。

11.11.1 线程与信号

Linux 信号模型是基于进程模型而设计的,信号的问世远早于线程;

⑴、信号如何映射到线程

⑵、线程的信号掩码

⑶、向线程发送信号

⑷、异步信号安全函数

⑸、多线程环境下信号的处理

第十二章 线程同步

线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一致的问题。

12.1 为什么需要线程同步?

线程同步是为了对共享资源的访问进行保护。

保护的目的是为了解决数据一致性的问题。

出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)。

12.2 互斥锁

互斥锁使用 pthread_mutex_t 数据类型表示,在使用互斥锁之前,必须首先对它进行初始化操作,可以 使用两种方式对互斥锁进行初始化操作。

12.2.1 互斥锁初始化

1、使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥锁

2、使用 pthread_mutex_init()函数初始化互斥锁

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

12.2.2 互斥锁加锁和解锁

互斥锁初始化之后,处于一个未锁定状态,调用函数 pthread_mutex_lock()可以对互斥锁加锁、获取互斥锁,而调用函数 pthread_mutex_unlock()可以对互斥锁解锁、释放互斥锁。其函数原型如下所示:

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++)
    {
        pthread_mutex_lock(&mutex); //互斥锁上锁

        l_count = g_count;
        l_count++;
        g_count = l_count;
        pthread_mutex_unlock(&mutex); //互斥锁解锁
    }
    return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int ret;
    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);
    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);
    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);
}

在这里插入图片描述

12.2.3 pthread_mutex_trylock()函数

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++)
    {
        while (pthread_mutex_trylock(&mutex))
            ; //以非阻塞方式上锁
        l_count = g_count;
        l_count++;
        g_count = l_count;
        pthread_mutex_unlock(&mutex); //互斥锁解锁
    }
    return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int ret;
    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);
    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);
    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);
}

在这里插入图片描述

12.2.4 销毁互斥锁

当不再需要互斥锁时,应该将其销毁,通过调用 pthread_mutex_destroy()函数来销毁互斥锁,其函数原型如下所示:

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++)
    {
        pthread_mutex_lock(&mutex); //互斥锁上锁
        l_count = g_count;
        l_count++;
        g_count = l_count;
        pthread_mutex_unlock(&mutex); //互斥锁解锁
    }
    return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int ret;
    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);
    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);
    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    /* 销毁互斥锁 */
    pthread_mutex_destroy(&mutex);
    exit(0);
}

在这里插入图片描述

12.2.5 互斥锁死锁

譬如,程序中使用 一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞 状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。

12.2.6 互斥锁的属性

互斥锁的类型属性控制着互斥锁的锁定特性,一共有 4 中类型:

  1. PTHREAD_MUTEX_NORMAL:一种标准的互斥锁类型,不做任何的错误检查或死锁检测。
  2. PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查。
  3. PTHREAD_MUTEX_RECURSIVE:此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行 多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁
  4. PTHREAD_MUTEX_DEFAULT : 此 类 互 斥 锁 提 供 默 认 的 行 为 和 特 性 。

12.3 条件变量

条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。

12.3.1 条件变量初始化

条件变量使用 pthread_cond_t 数据类型来表示

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

12.3.2 通知和等待条件变量

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

12.3.3 条件变量的判断条件

使用 while 循环,而不是 if 语句

12.3.4 条件变量的属性

条件变量包括两个属性:进程共享属性和时钟属性。

12.4 自旋锁

自旋锁的不足之处在于:自旋锁一直占用的 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。

12.4.1 自旋锁初始化

#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

12.4.2 自旋锁加锁和解锁

#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_spinlock_t spin; //定义自旋锁
static int g_count = 0;
static void *new_thread_start(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++)
    {
        pthread_spin_lock(&spin); //自旋锁上锁

        l_count = g_count;
        l_count++;
        g_count = l_count;
        pthread_spin_unlock(&spin); //自旋锁解锁
    }
    return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int ret;
    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);
    /* 初始化自旋锁(私有) */
    pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    /* 销毁自旋锁 */
    pthread_spin_destroy(&spin);
    exit(0);
}

在这里插入图片描述

12.5 读写锁

互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。

读写锁有 3 种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和 不加锁状态

读写锁比互斥锁具有更高的并行性

读写锁有如下两个规则:

  1. 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读 模式加锁还是以写模式加锁)的线程都会被阻塞。
  2. 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。

读写锁非常适合于对共享数据读的次数远大于写的次数的情况。

12.5.1 读写锁初始化

譬如使用宏 PTHREAD_RWLOCK_INITIALIZER 进行 初始化必须在定义读写锁时就对其进行初始化:

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

12.5.2 读写锁上锁和解锁

不管是以何种方式锁住读写锁,均可以调用 pthread_rwlock_unlock()函 数解锁,其函数原型如下所示:

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_rwlock_t rwlock; //定义读写锁
static int g_count = 0;
static void *read_thread(void *arg)
{
    int number = *((int *)arg);
    int j;
    for (j = 0; j < 10; j++)
    {
        pthread_rwlock_rdlock(&rwlock); //以读模式获取锁
        printf("读线程<%d>, g_count=%d\n", number + 1, g_count);
        pthread_rwlock_unlock(&rwlock); //解锁
        sleep(1);
    }
    return (void *)0;
}
static void *write_thread(void *arg)
{
    int number = *((int *)arg);
    int j;
    for (j = 0; j < 10; j++)
    {
        pthread_rwlock_wrlock(&rwlock); //以写模式获取锁
        printf("写线程<%d>, g_count=%d\n", number + 1, g_count += 20);
        pthread_rwlock_unlock(&rwlock); //解锁
        sleep(1);
    }
    return (void *)0;
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char *argv[])
{
    pthread_t tid[10];
    int j;
    /* 对读写锁进行初始化 */
    pthread_rwlock_init(&rwlock, NULL);
    /* 创建 5 个读 g_count 变量的线程 */
    for (j = 0; j < 5; j++)
        pthread_create(&tid[j], NULL, read_thread, &nums[j]);
    /* 创建 5 个写 g_count 变量的线程 */
    for (j = 0; j < 5; j++)
        pthread_create(&tid[j + 5], NULL, write_thread, &nums[j]);
    /* 等待线程结束 */
    for (j = 0; j < 10; j++)
        pthread_join(tid[j], NULL); //回收线程
    /* 销毁自旋锁 */
    pthread_rwlock_destroy(&rwlock);
    exit(0);
}

在这里插入图片描述

12.5.3 读写锁的属性

#include <pthread.h>
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);

12.6 总结

本章介绍了线程同步的几种不同的方法,包括互斥锁、条件变量、自旋锁以及读写锁

第十三章 高级I/O

主要包括:非阻塞 I/O、I/O 多路复用、异步 I/O、存储映射 I/O 以及文件锁

13.1 非阻塞I/O

阻塞其实就是进入了休眠状态,交出了 CPU 控制权。

非阻塞式 I/O 就是对文件的 I/O 操作是非阻塞的。

如果是非阻 塞式 I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误

普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内 返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的;但是对于某些文件 类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O 进行操作。

13.1.1 阻塞I/O与非阻塞I/O读文件

普通文件的读写操作是不会 阻塞的,它总是以非阻塞的方式进行 I/O 操作

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
    char buf[100];
    int fd, ret;
    /* 打开文件 */
    fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 读文件 */
    memset(buf, 0, sizeof(buf));
    for (;;)
    {
        ret = read(fd, buf, sizeof(buf));
        if (0 < ret)
        {
            printf("成功读取<%d>个字节数据\n", ret);
            close(fd);
            exit(0);
        }
    }
}

服务器没有鼠标。。这个实验跑不了

13.1.2 阻塞I/O的优点与缺点

当对文件进行读取操作时,如果文件当前无数据可读,那么阻塞式 I/O 会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞;而对于非阻塞 I/O,应用程序不会被挂起,而是会立即返 回,它要么一直轮训等待,直到数据可读,要么直接放弃

阻塞式 I/O 的优点在于能够提升 CPU 的处理效率

13.1.3 使用非阻塞I/O实现并发读取

阻塞式 I/O 的一个困境,无法实现并发读取

13.2 I/O多路复用

上一小节虽然使用非阻塞式 I/O 解决了阻塞式 I/O 情况下并发读取文件所出现的问题,但依然不够完 美,使得程序的 CPU 占用率特别高。解决这个问题,就要用到本小节将要介绍的 I/O 多路复用方法。

13.2.1 何为I/O多路复用

I/O 多路复用通过一种机制,可以监视多个文件描述符,一旦某个文件可以执行 I/O 操作时,能够通知应用程序进行相应的读写操作。

I/O 多路复用存在一个非常明显的特征:外部阻塞式,内部监视多路 I/O。

13.2.2 select()函数介绍

系统调用 select()可用于执行 I/O 多路复用操作,调用 select()会一直阻塞,直到某一个或多个文件描述 符成为就绪态(可以读或写)。其函数原型如下所示:

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

13.2.3 poll()函数介绍

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

13.2.4 总结

在使用 select()或 poll()时需要注意一个问题,当监测到某一个或多个文件描述符成为就绪态(可以读或 写)时,需要执行相应的 I/O 操作,以清除该状态,否则该状态将会一直存在。

13.3 异步IO

而在异步 I/O 中,当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务直到文件描述符可以执行 I/O 操作为止,此时内核会发送信号给进程。

按照步骤

13.4 优化异步I/O

当需要检查的文件描述符并不是很多时,使用 select()或 poll()是一种非常不错的方案

当需要检查大量文件描述符时,可以使用 epoll 解决 select()或 poll()性能低的问题

13.4.1 使用实时信号替换默认信号SIGIO

SIGIO 作为异步 I/O 通知的默认信号,是一个非实时信号

fcntl(fd, F_SETSIG, SIGRTMIN);

13.4.2 使用sigaction()函数注册信号处理函数

在应用程序当中需要为实时信号注册信号处理函数,使用 sigaction 函数进行注册,并为 sa_flags 参数指 定 SA_SIGINFO

13.4.3 使用示例

实验做不了,以后需要再做

13.5 存储映射I/O

存储映射 I/O(memory-mapped I/O)是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据,将数据写入这段内存时,则相当于将数据直接写入文件中。这样就可以在 不使用基本 I/O 操作函数 read()和 write()的情况下执行 I/O 操作。

13.5.1 mmap()和munmap()函数

我们需要告诉内核将一个给定的文件映射到进程地址空间中的一块 内存区域中,这由系统调用 mmap()来实现。其函数原型如下所示:

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
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);
}

在这里插入图片描述

13.5.2 mprotect()函数

使用系统调用 mprotect()可以更改一个现有映射区的保护要求,其函数原型如下所示:

#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);

13.5.3 msync()函数

在第四章中提到过,read()和 write()系统调用在操作磁盘文件时不会直接发起磁盘访问(读写磁盘硬件), 而是仅仅在用户空间缓冲区和内核缓冲区之间复制数据,在后续的某个时刻,内核会将其缓冲区中的数据 写入(刷新至)磁盘中

我们可以调用 msync()函数将映射区中的数据刷写、更新至磁盘文件中

#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);

13.5.4 普通I/O与存储映射I/O比较

普通 I/O 方式的缺点

普通 I/O 方式一般是通过调用 read()和 write()函数来实现对文件的读写,中间涉及到很多的函数调用过程,数据需要在不同的 缓存间倒腾,效率会比较低。

存储映射 I/O 的优点

存储映射 I/O 的实质其实是共享

通过存储映射 I/O 将文件直接映射到应用程序地址空间中的一块内存区域中,也就是映射区;直接 将磁盘文件直接与映射区关联起来。

可以认为映射区就是应用层 与内核层之间的共享内存。

存储映射 I/O 的不足

它所映射的文件只能是固定大小。另外,文件映射的内存区域的大小必须是系统页大小的整数倍

存储映射 I/O 的应用场景

存储映射 I/O 在处理大量数据时效率高,对于少量数据处理不是很划算,所以通常来 说,存储映射 I/O 会在视频图像处理方面用的比较多

13.6 文件锁

对于有些应用程序,进程有时需要确保只有它自己能够对某一文件进行 I/O 操作,在这段时间内不允许其它进程对该文件进行 I/O 操作。为了向进程提供这种功能,Linux 系统提供了文件锁机制。

互斥锁、自旋 锁、读写锁主要用在多线程环境下,对共享资源的访问进行保护,做到线程同步。

而文件锁,是一种应用于文件的锁机制

文件锁的分类

文件锁可以分为建议性锁和强制性锁两种:

  1. 建议性锁。本质上是一种协议,程序访问文件之前,先对文件上锁,上锁成功之后再访问文件
  2. 强制性锁。是一种强制性的要求,如果进程对文件上了强制性锁,其它的进程在没有获取 到文件锁的情况下是无法对文件进行访问的。

13.6.1 flock()函数加锁

flock()函数只能产生建议性锁

#include <sys/file.h>
int flock(int fd, int operation);

先给一个文件加锁,让这个文件一直持有锁。其它程序仍然能未获取锁情况下读写文件。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <signal.h>
static int fd = -1; //文件描述符
/* 信号处理函数 */
static void sigint_handler(int sig)
{
    if (SIGINT != sig)
        return;
    /* 解锁 */
    flock(fd, LOCK_UN);
    close(fd);
    printf("进程 1: 文件已解锁!\n");
}
int main(int argc, char *argv[])
{
    if (2 != argc)
    {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        exit(-1);
    }
    /* 打开文件 */
    fd = open(argv[1], O_WRONLY);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 以非阻塞方式对文件加锁(排它锁) */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB))
    {
        perror("进程 1: 文件加锁失败");
        exit(-1);
    }
    printf("进程 1: 文件加锁成功!\n");
    /* 为 SIGINT 信号注册处理函数 */
    signal(SIGINT, sigint_handler);
    for (;;)
        sleep(1);
}

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <string.h>
int main(int argc, char *argv[])
{
    char buf[100] = "Hello World!";
    int fd;
    int len;
    if (2 != argc)
    {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        exit(-1);
    }
    /* 打开文件 */
    fd = open(argv[1], O_RDWR);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 以非阻塞方式对文件加锁(排它锁) */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB))
        perror("进程 2: 文件加锁失败");
    else
        printf("进程 2: 文件加锁成功!\n");
    /* 写文件 */
    len = strlen(buf);
    if (0 > write(fd, buf, len))
    {
        perror("write error");
        exit(-1);
    }
    printf("进程 2: 写入到文件的字符串<%s>\n", buf);
    /* 将文件读写位置移动到文件头 */
    if (0 > lseek(fd, 0x0, SEEK_SET))
    {
        perror("lseek error");
        exit(-1);
    }
    /* 读文件 */
    memset(buf, 0x0, sizeof(buf)); //清理 buf
    if (0 > read(fd, buf, len))
    {
        perror("read error");
        exit(-1);
    }
    printf("进程 2: 从文件读取的字符串<%s>\n", buf);
    /* 解锁、退出 */
    flock(fd, LOCK_UN);
    close(fd);
    exit(0);
}

在这里插入图片描述

正确的使用方式是,在加锁失败之后不要再对文件进行 I/O 操作了,遵循这个协议。

解锁后再尝试读写

在这里插入图片描述

在这里插入图片描述

关于 flock()的几条规则

  1. 同一进程对文件多次加锁不会导致死锁。
  2. 文件关闭的时候,会自动解锁。
  3. 一个进程不可以对另一个进程持有的文件锁进行解锁。
  4. 由 fork()创建的子进程不会继承父进程所创建的锁。
flock(fd, LOCK_EX); //加锁
new_fd = dup(fd);
flock(new_fd, LOCK_UN); //解锁

13.6.2 fcntl()函数加锁

而 fcntl()可以对文件的某个区域(某部分内容)进行加锁 /解锁,可以精确到某一个字节数据。

flock()仅支持建议性锁类型;而 fcntl()可支持建议性锁和强制性锁两种类型。

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );

两种类型的锁:F_RDLCK 和 F_WRLCK

共享性读锁(F_RDLCK)和独占性写锁(F_WRLCK)

对文件的 100~200 字节区间加了一个写锁, 对文件的 400~500 字节区间加了一个读锁。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
    struct flock wr_lock = {0};
    struct flock rd_lock = {0};
    int fd = -1;
    /* 校验传参 */
    if (2 != argc)
    {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        exit(-1);
    }
    /* 打开文件 */
    fd = open(argv[1], O_RDWR);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 将文件大小截断为 1024 字节 */
    ftruncate(fd, 1024);
    /* 对 100~200 字节区间加写锁 */
    wr_lock.l_type = F_WRLCK;
    wr_lock.l_whence = SEEK_SET;
    wr_lock.l_start = 100;
    wr_lock.l_len = 100;
    if (-1 == fcntl(fd, F_SETLK, &wr_lock))
    {
        perror("加写锁失败");
        exit(-1);
    }
    printf("加写锁成功!\n");
    /* 对 400~500 字节区间加读锁 */
    rd_lock.l_type = F_RDLCK;
    rd_lock.l_whence = SEEK_SET;
    rd_lock.l_start = 400;
    rd_lock.l_len = 100;
    if (-1 == fcntl(fd, F_SETLK, &rd_lock))
    {
        perror("加读锁失败");
        exit(-1);
    }
    printf("加读锁成功!\n");
    /* 对文件进行 I/O 操作 */
    // ......
    // ......
    /* 解锁 */
    wr_lock.l_type = F_UNLCK; //写锁解锁
    fcntl(fd, F_SETLK, &wr_lock);
    rd_lock.l_type = F_UNLCK; //读锁解锁
    fcntl(fd, F_SETLK, &rd_lock);
    /* 退出 */
    close(fd);
    exit(0);
}

13.6.3 lockf()函数加锁

lockf()函数是一个库函数,其内部是基于 fcntl()来实现的,所以 lockf()是对 fcntl 锁的一种封装

13.7 小结

非阻塞 I/O、I/O 多路复用、异步 I/O、存储映射 I/O、以及文件 锁,其中有许多的功能,我们将会在后面的提高篇和进阶篇章节实例中使用到。

第十四章 本篇总结

学到了很多没见过的知识点,虽然不太懂但大受震撼.jpg

个人感觉入门篇不算简单,认识到了课堂上学的很多概念还是太浅了。

后面的教程就是实战运用了,期待🤩

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注