【Linux 系统编程】

目录

1、目录相关的操作

获得当前工作目录

char* getcwd(char* buf, size_t size)

参数

  1. buf,传出参数,用来接收当前工作目录
  2. size,buf 的容量

返回值

  1. 成功,返回当前的工作路径,即 buf
  2. 失败,返回 NULL ,并设置 errno

注意点

  1. 如果传入的 buf 为 NULL,size 为 0,那么会自动在堆上 malloc 一个合适大小的空间,填入当前工作路径,并返回,这样使用之后,需要 手动free 返回值。

改变当前工作路径

int chdir(const char* path)

参数

  1. path,改变后的工作路径

返回值

  1. 成功,0
  2. 失败,返回 -1,并设置 errno

注意点

  1. 当前工作路径是进程的属性,子进程在创建后后继承父进程的工作路径,chdir() 只能修改调用进程的工作路径

创建目录

int mkdir(const char* pathname, mode_t mode)

参数

  1. pathname,要创建的目录的路径,对路径或相对路径
  2. mode,目录的权限,会受 umask 的影响

返回值

  1. 成功,0
  2. 失败,返回 -1,并设置 errno

注意点

  1. 当前工作路径是进程的属性,子进程在创建后后继承父进程的工作路径,chdir() 只能修改调用进程的工作路径

删除空目录

int rmdir(const char* pathname)

参数

  1. pathname,要删除的目录的路径

返回值

  1. 成功,0
  2. 失败,返回 -1,并设置 errno

注意点

  1. 只能删除空目录

目录流

目录是由一个个目录项组成的,这也是一种流模型

打开目录流

DIR* opendir(const char* name)

参数

  1. name,要打开的目录的路径

返回值

  1. 成功,返回指向目录流的指针
  2. 失败,返回 NULL,并设置 errno

关闭目录流

int closedir(DIR* dirp)

参数

  1. dirp,要关闭的目录流指针

返回值

  1. 成功,返回 0
  2. 失败,返回 -1,并设置 errno

读目录流

struct dirent* readdir(DIR* dirp)

参数

  1. dirp,要读的目录流指针

返回值

  1. 成功,返回指向下一个目录项的指针,当读到流的末尾,返回 NULL,不设置 errno
  2. 失败,返回 NULL,并设置 errno
struct dirent
{
	ino_t d_ino; // inode 编号
	off_t d_off; // 与下一个目录项的距离
	unsigned short d_reclen; // 结构体的长度
	unsigned char d_type; // 文件的类型
	char d_name[256]; //文件名
};
// d_type 的选项
	DT_BLK ---块设备
	DT_CHR ---字符设备
	DT_DIR ---目录
	DT_FIFO ---有名管道
	DT_LNK ---软链接
	DT_REG ---普通文件
	DT_SOCK ---本地套接字
	DT_UNKNOWN ---不确定的文件类型

目录流的位置

void seekdir(DIR* dirp, long loc)

参数

  1. dirp,目录流指针
  2. loc,要去的位置,一般需要先使用 telldir 保存当前的位置,然后读取当前目录项,再调用此函数,处理当前目录项,因为 readdir 调用后,流指针会指向下一项

long telldir(DIR* dirp)

参数

  1. dirp,目录流指针

返回值

  1. 成功,返回目录流当前的位置
  2. 失败,返回 -1,并设置 errno

void rewinddir(DIR* dirp) —移动指针到目录流的起始位置

参数

  1. dirp,目录流指针
//tree.c 
//实现了一个简易的 tree 命令
#include <func.h>

//类似于树的先序遍历
void dfs_print(const char* path, int width) // width 控制缩进
{
    DIR* stream = opendir(path);
    struct dirent* current = 0;
    errno = 0;
    while((current = readdir(stream)) != NULL)
    {
		//跳过隐藏文件或目录
        if(current->d_name[0] == '.')
            continue;
		//如果是目录,则递归处理
        if(current->d_type == DT_DIR)
        {
            for(int i = 0; i < width; ++i)
                printf(" ");
            directories++;
            printf("%s\n", current->d_name);
            char p[1024];
            sprintf(p, "%s/%s", path, current->d_name);
            dfs_print(p, width + 4);
        }
        //如果是文件,则直接输出
        else
        {
            for(int i = 0; i < width; ++i)
                printf(" ");
            files++;
            printf("%s\n", current->d_name);
        }
    }
	//判断是读到了流的末尾,还是 readdir 发生了错误
    if(errno)
        error(1, errno, "readdir");

    closedir(stream);
}
//记录目录和文件数量
int directories = 0, files = 0;

int main(int argc, char *argv[])
{
    if(argc != 2)
        error(1, 0, "Usage: %s path", argv[0]);
	// 打印目录的名字
    puts(argv[1]);		
	// 递归打印每一个目录项
    dfs_print(argv[1], 4);	
	// 最后打印统计信息
    printf("\n%d directories, %d files\n", directories, files);	
    	
    return 0;
}

文件

文件描述符:文件描述符是一种整数,用于标识被打开的文件。在操作系统层面,文件描述符是一个抽象的句柄 (handler) ,它指向操作系统内核中与文件相关的数据结构。
在这里插入图片描述

1、对文件描述符的操作

打开 / 关闭文件描述符

int open(const char* pathname, int flags)
int open(const char* pathname, int flags, mode_t mode)

失败,返回 -1,并设置 errno
成功,返回最小可用文件描述符
参数:

  1. pathname —文件的绝对路径或相对路径
  2. flags —标志位(位图)
标志位含义
O_RDONLY只读
O_WRONLY只写
O_RDWR读写
O_CREAT文件不存在则创建
O_EXCL一般和 O_CREAT 连用,文件存在则报错
O_TRUNC文件存在则截断为 0
O_APPEND追加
  1. mode — 用来指定文件的权限,会受 umask 的影响

int close(int fd) —关闭文件描述符

失败,返回 -1,并设置 errno
成功,返回 0

读 / 写文件描述符

ssize_t read(int fd, void* buf, size_t count)

失败,返回 -1,并设置 errno
成功,返回实际读取的字节数目,下一次读的位置也会改变,返回 0 时,表示读到了文件末尾。

ssize_t write(int fd, const void* buf, size_t count)

失败,返回 -1,并设置 errno
成功,返回实际写入的字节数目。

改变文件位置

off_t lssk(int fd, off_t offset, int whence)

失败,返回 -1,并设置 errno
成功,返回移动后文件的位置
参数:

  1. fd —文件描述符
  2. offset —偏移量
  3. whence —参考位置

SEEK_SET —文件起始位置
SEEK_CUR —文件当前位置
SEEK_END —文件末尾位置

持久化到磁盘

int fsync(int fd)

失败,返回 -1,并设置 errno
成功,返回 0
fsync()会把内核中和文件描述符 fd 相关的脏页刷新到磁盘。

截断文件

int ftruncate(int fd, off_t length)

失败,返回 -1,并设置 errno
成功,返回 0

  1. 如果 length < 原文件大小,那么超出部分的数据会丢失。
  2. 如果 length > 原文件大小,那么扩展的部分会填充空字符 (‘\0’),可能出现文件空洞。

获取文件的元数据

stat filename —显示文件的元数据信息
int fstat(int fd, struct stat* statbuf)

失败,返回 -1,并设置 errno
成功,返回 0
参数:statbuf —传出时表示文件的元数据

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; // 设备 id
	off_t st_size; // 文件大小,单位是比特
	blksize_t st_blksize; // 块的大小
	blkcnt_t st_blocks; // 占用块的数量
	struct timespec st_atime; // 最近访问时间
	struct timespec st_mtime; // 最近修改时间
	struct timespec st_ctime; // 状态最近改变时间
	#define st_atime st_atim.tv_sec
	#define st_mtime st_mtim.tv_sec
	#define st_ctime st_ctim.tv_sec
};

2、内存映射 I / O

进程

进程是操作系统分配资源的最小单位

1、进程的基本操作

获取id

pid_t getpid(void) —获得当前进程的id
pid_t getppid(void)—获得当前父进程的id

这两个函数总是成功,意味如果如果函数返回了则一定调用成功了,如果失败了则不会返回。

创建进程

pid_t fork(void) —创建一个子进程

创建失败,返回 -1 ,并设置 errno
创建成功,父进程返回子进程的 id ,子进程返回 0

  1. 创建成功时子进程会复制父进程的 PCB 并且修改一些信息,pid、parent指针等。父子进程的起始点都是 fork 函数返回之前的那一刻,在返回时父进程返回子进程的 id,子进程返回 0 ,之后父子进程执行各自的指令。
  2. 创建成功后,父子进程会共享父进程的虚拟内存空间、页表、文件描述符表、打开文件表等信息;但是虚拟内存空间(栈、堆、数据段)、用户态缓冲区、页表这些内容会采取写时复制技术,因此这些区域可以说是进程私有的,其他区域是进程共享的。

终止进程

进程终止有两种方式:①正常终止,指调用 _exit() 函数。②异常终止,指由信号导致的。

void _exit(int status) —正常终止程序

_exit() 函数总是成功,从不返回。
status:表示程序终止的状态,父进程可以调用wait() 函数捕获这个状态信息,虽是 int 类型,但是只有低 8 位有效,0 表示执行成功,非 0 表示因某种原因而执行失败。

_exit() 函数会直接结束进程,无论执行成功与否,但是有时在执行失败时我们需要输出一些其他信息,exit()库函数可以实现这种的效果。

void exit(int status)

exit 函数的执行过程

  1. 调用退出处理函数,但是执行顺序与注册顺序相反。

注册退出处理函数有两种方式:
① int atexit(void (* function)(void));
② int on_exit(void (* function)(int, void *), void * arg);

  1. 刷新 stdio 流缓冲区。
  2. 将 status 作为参数,调用 _exit() 系统调用。

void abort(void) —异常终止程序

abort() 函数不返回,本质是请求操作系统给自己发送 SIGABRY(6) 信号
会产生 core 文件
异常终止的程序,不会执行退出处理函数,也不会刷新用户态缓冲区

监控子进程

僵尸进程:进程死亡时其父进程没有调用 wait() 进行回收,其始终占用系统资源的进程
孤儿进程:父进程先于子进程死亡,子进程会成为孤儿进程,孤儿进程都会被 init 进程收养

pid_t wait(int* status) —等待子进程终止

失败,返回 -1 ,设置 errno
成功,返回终止进程的 id
调用 wait() 之后,进程会被一直阻塞,直到等到了一个子进程终止
wait() 函数只能监控子进程是否终止,而不能监控子进程的挂起和继续执行状态。
status 是传入传出参数,会接受子进程终止的信息

若子进程正常终止,WIFEXITED(status) 宏会返回 trueWEXITSTATUS(status) 可以获取子进程的退出状态码。
若子进程异常终止,WIFSIGNALED(status) 宏会返回 trueWTERMSIG(status) 可以获取导致子进程终止的信号。
若子进程产生 core 文件,则宏 WCOREDUMP(status) 会返回 ture

pid_t waitpid(pid_t pid, int* status, int options) —等待子进程状态发生改变

失败,返回 -1 ,并设置 errno
成功,返回状态发生改变的子进程 id,如果指定了 WNOHANG ,但没有任何指定的子进程状态发生改变,则返回 0
参数:

  1. pid —用来指定需要等待的子进程

pid > 0,表示等待 ID 为 pid 的这个子进程
pid = 0,表示等待同进程组的所有子进程
pid = -1,表示等待任意子进程
pid < -1,表示等待进程组 ID 为 |pid| 的所有子进程

  1. status —传入传出参数,用于保存子进程的终止状态信息

若子进程因为信号而停止,宏 WIFSTOPPED(status) 返回 trueWSTOPSIG(status) 会返回导致子进程停止的信号。
若子进程收到SIGCONT信号而恢复执行,WIFCONTINUED(status) 返回 ture

  1. options —标志位(位图表示)

WNOHANG 不阻塞。如果监控的子进程没有一个发生变化,立即返回 0。
WUNTRACED 监控子进程是否因为某个进程而停止。
WCONTINUED 监控已停止的子进程是否收到 SIGCONT(18) 信号而恢复执行。

执行程序

int execve(const char* pathname, char* const argv[], char* const envp[]) —执行其他程序

失败,返回 -1,并设置 errno
成功,不返回。返回时一定是执行失败了
execve() 在调用时会清除原来进程的虚拟内存空间,然后在该空间中加载入新的程序的信息,并重新设置命令函参数和环境变量,从新程序的 mian() 开始执行。
参数:

pathname —新程序的绝对路径或相对路径。
argv —新程序的命令行参数,必须以 NULL 结束。
envp —新程序的环境变量,·必须以 NULL 结束。(环境变量的格式一般是 name=value)

exec 函数簇
以下函数都是建立在 execve() 系统调用基础上的库函数

函数指定可执行程序(-, p)指定命令行参数(v, l)环境变量来源(e, -)
execlpathname列表(可变长参数)不替换
execlpfilename + PATH列表(可变长参数)不替换
execlepathname列表(可变长参数)envp 参数
execvpathname数组不替换
execvpfilename + PATH数组不替换
execvpefilename + PATH数组envp 参数
execvepathname数组envp 参数

int system(const char* command) —执行 shell 命令

system() 函数会创建一个子进程来运行 shell ,该子进程会执行 command 命令

2、进程间通信

特征

  1. 管道是字节流
  1. 字节流中的数据是没有边界的;不像消息或数据报,有明显的边界,只能以消息或数据报为单位进行传输。
  2. 顺序传输——从管道中读取出来的字节的顺序与它们被写入管道的顺序是完全一样的。
  3. 不能随机访问数据——管道无法使用 lseek() 来移动文件位置。
  1. 管道是半双工的

管道中的数据是单向传递的,一端用于读,一端用于写。

  1. 管道的容量是有限的

管道被写满之后写操作会被阻塞,一般来讲,应用程序无需知道管道的实际存储能力。如果需要防止写管道的进程阻塞,那么读管道的进程应该设计为以尽快的速度读取管道。(默认情况下,Linux 管道的存储能力为 65536 个字节)

  1. 从管道中读数据

试图从一个空的管道中读取数据将会被阻塞,直到至少有一个字节被写入到管道中为止。如果管道的写入端关闭了,那么从管道中读取数据的进程在读完管道中剩余的数据之后将会看到文件结束标志 EOF 此时 read() 会返回 0。

管道和FIFO

int pipe(int fields[2]) —创建一条管道

失败,返回 -1,并设置 errno
成功,返回 0
pipe() 调用成功会在数组 fieldes 中返回两个打开的文件描述符:fields[0] 关联管道的读端,fields[1] 关联管道的写端。
pipe管道一般是用于父子进程间通信的

mkfifo [-m mode] pathname —创建一根有名管道
int mkfifo(const char* pathname, mode_t mode) —创建一根有名管道

失败,返回 -1,并设置 errno
成功,返回 0
参数:

  1. pathname —FIFO的名字
  2. mode —有名管道的权限,会受 umask 影响

在 open 有名管道时,需要注意两个进程的打开顺序,顺序不对可能会导致死锁。open 在打开管道的一端后若另一端没有被打开,open 会被阻塞
FIFO 管道一般用于两个进程通信

I / O 多路复用

I/O 多路复用是一种同步的 I/O 事件监听机制:它可以同时监听多个 I/O 事件。任意一个监听的事件就绪,它都能感知并及时返回。避免了因一个 I/O 事件的阻塞,而导致另一个 I/O 事件不能及时被处理。

一个线程最好只有一个阻塞点。I/O 多路复用就是将可能的多个阻塞点,变成了一个阻塞点。

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout)

失败,返回 -1,并设置 errno
成功,返回就绪的 I/O 事件的个数;超时,返回 0
参数

  1. nfds —监控的最大文件描述符 +1,指明监控的上限
    在这里插入图片描述
  2. readfds —传入要监控的读事件文件描述符集合,传出已就绪的读事件文件描述符集合

void FD_ZREO(fd_set* set) —清空指向的集合
void FD_SET(int fd, fd_set* set) —将 fd 添加到监控集合中
voidFD_CLR(int fd, fd_set* set) —将 fd 从监控集合中删除
int FD_ISSET(int fd, fd_set* set) —检查 fd 是否在集合中

  1. writefds —传入要监控的写事件文件描述符集合,传出已就绪的写事件文件描述集合
  2. exceptfds —传入要监控的异常事件文件描述符集合,传出发生异常事件的文件描述集合

在 Linux 上,异常情况只会在两种情况下发生:
①流式套接字上 (stream socket,TCP) 上接收到了带外数据。
②处于信包模式下的伪终端主设备上的从设备状态发生了改变。

  1. timeout —如果值为 NULL,select() 会一直阻塞,直到有一个 I/O 事件就绪。或者指向一个 struct timeval 变量,如果两个字段都为 0 的话 select() 不会阻塞,没有 I/O 事件就绪,立即返回。timeout 是传入传出参数,传入的时候表示最长的阻塞时间 (超时时间),传出时表示还剩余的时间。
    在这里插入图片描述

信号

进程之间是隔离的,进程无法感知外部世界的存在,当进程需要感知外部世界时,就需要内核发送相应的信号,当进程收到信号后,会停下当前的执行流程,立即去处理信号。

信号因某些事件而产生。信号产生后,稍后才会递送给相应的进程,而进程也会采取某些措施来响应信号。在产生和递送期间,信号处于未决状态 (pending)(在这个期间,信号可能会丢失)。通常,通常信号会在下一次调度进程的时候,递送给该进程。

响应信号有两种方式:

  1. 默认处置
    Term:终止信号
    Ign:忽略信号
    Core:产生core文件,并终止程序
    Stop:暂停程序
    Cont:恢复进程执行
  2. 自行处置
    SIG_DFL:恢复默认行为
    SIG_IGN:忽略信号
    执行自定义的信号处理函数(捕获信号)

SIGKILLSIGSTOP 信号时不能被捕获,阻塞和忽略的。

产生信号的事件源

  1. 硬件
  2. 软件(内核或进程)
  3. 用户

常见信号
SIGHUP Term 终端结束或断开链接
SIGTNT Term ctrl + c 中断进程
SIGQUIT Core ctrl + \ 退出进程
SIGILL Core 非法指令,由硬件产生
SIGABRT Core 调用 void abort(void) 而发出的信号,异常终止一个进程
SIGALRM Term 调用 unsigned int alarm(unsigned int seconds) 而发出的信号,设置一个传递信号的时钟
SIGFPE Core 算数异常,由硬件产生
SIGKILL Term 杀死一个进程,不可被捕获
SIGSEGV Core 访问非法内存,由硬件产生
SIGPIPE Term 写破损的管道
SIGTERM Term 软件终止进程
SIGCHLD Ign 子进程暂停或停止
SIGCONT Cont 如果进程是暂停的则唤醒继续执行
SIGSTOP Stop 暂停进程,不可被捕获、忽略
SIGTSTP Stop ctrl + z 暂停程序
`

sighandler_t signal(int signum, sighandler_t handler)

失败,返回 SIG_ERR ,并设置errno
成功,返回先前的信号处理函数(可能是默认的处理函数)
参数和返回值的类型是函数指针

  1. typedef void(*sighandler_t)(int)

int kill(pid_t pid, int sig)

失败,返回 -1 ,并设置 errno
成功,返回 0
参数:

  1. pid —用来指定要发送的进程

pid > 0,发送给 ID 为 pid 的进程
pid = 0,发送给同进程组的所有进程
pid = -1,发送给任意进程, 某些系统进程除外,比如 init 进程
pid < -1,发送给进程组 ID 为 |pid| 的所有进程

int raise(int sig) —给自己发送信号

失败,非 0 值
成功,返回 0
等价于 kill(getpid(), sig)

线程

线程是进程中的一个执行流程,是CPU调度的最小单位
之所以引入线程,是因为多进程应用存在以下一些限制:

  1. 进程之间是隔离的,进程之间通信需要打破隔离的壁障,开销比较大。
  2. 调用 fork() 创建进程的代价相对较高。即使利用写时复制 (copy-on-write) 技术,仍然需要复制
    诸如页表 (page table) 和文件描述符列表之类的多种属性,开销依然不菲。
  3. 进程之间的切换,很可能会导致 CPU 的高速缓存 (cache)、页表缓存 (TLB) 失效。

而线程可以很好地解决上面的问题:

  1. 线程之间可以方便、快速地共享信息。只需将数据放置到共享变量中即可。不过,要避免出现多个线程试图同时修改同一数据的情况,这需要用到线程间的同步技术。
  2. 创建线程比创建进程通常要快 10 倍甚至更多。
  3. 同一进程中的线程位于同一虚拟内存空间中,线程之间的切换,不太可能导致 CPU 的高速缓存
    (cache)、页表缓存 (TLB) 失效。

1、线程的基本操作

获取线程id

pthread_t pthread_self(void)

总是成功

创建线程

int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(* start_routine)(void*), void* arg)

失败,返回错误码,不会设置errno
成功,返回 0
参数:

  1. thread —传出参数,新线程的id
  2. attr —指定新线程的属性,为 NULL 时,表示默认属性
  3. start_routine —新线程的入口函数,当函数返回时,新线程也执行结束了。void* start_routine(void* arg);可以返回任意类型的返回值,返回值可以被pthread_join()接收。
    arg —start_routine 的实参

程序启动时,只有一个线程,调用 pthread_create 成功后可以创建一个新的线程,之后该执行哪个线程无法确定。

终止线程

线程终止的方式:

  1. 进程结束,所有的线程都会终止
  2. 从线程的入口函数返回
  3. 调用 pthread_exit()
  4. 相应 pthread_cancel() 的取消请求

void pthread_exit(void* retval)

总是成功
参数:

  1. retval —线程的返回值,可以被 pthread_join()接收

连接已经终止的线程

int pthread_join(pthread_t thread, void** retval)

失败,返回错误码,不设置errno
成功,返回 0
参数:

  1. 传出参数,用来接收 ID 为 thread 的线程的返回值

pthread_join() 会等待线程 ID 为 thread 的线程终止(如果这个线程已经终止了,pthread_join() 会立即返回)。这个操作叫做连接。

分离进程

int pthread_detach(pthread_t thread)

失败,返回错误码,不设置errno
成功,返回 0
一旦线程处于分离状态,就不能再使用 pthread_join() 来获取其返回状态了,也无法再回到可连接状态了。已分离的线程终止之后会自动清理

取消线程

int pthread_cancel(pthread_t thread)

失败,返回错误码,不设置errno
成功,返回 0

pthread_cancel() 会向 id 为 thread 的线程发送取消请求,发送完之后函数立即返回,目标线程会不会响应,以及何时响应,取决于线程的两个属性:

  1. 取消状态 —决定会不会响应请求

PTHREAD_CANCEL_ENABLE 可以取消,这是默认值。
PTHREAD_CANCEL_DISABLE 不可被取消,当进程收到取消请求之后,会将请求挂起,直到取消状态改变时,再响应请求
int pthread_setcancelstate(int state, int * oldstate) —设置取消状态

失败,返回错误码,不设置errno
成功,返回 0
参数:

  1. state —PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DISABLE 两种取值
  2. oldstate —传出参数,用来保存以前的取消状态
  1. 取消类型 —决定何时响应请求

PTHREAD_CANCEL_DEFERRED 延迟响应,挂起取消请求,直达下一个取消点才响应,这是默认值
PTHREAD_CANCEL_ASYNCHRONOUS 异步响应,可以在任意时间点响应取消请求(未必是立即响应)。
int pthread_setcanceltype(int type, int * oldtype) —设置取消类型

失败,返回错误码,不设置errno
成功,返回 0
参数:

  1. type —PTHREAD_CANCEL_DEFERREDPTHREAD_CANCEL_ASYNCHRONOUS 两种取值
  2. oldstate —传出参数,用来保存以前的取消状态

取消点

若将线程的取消状态和类型分别置为 ENABLE 和 DEFERRED,那么只有当线程执行到某个取消点时,取消请求才会生效。取消点是一组函数,这些函数往往可以让线程陷入无限期的阻塞。

在这里插入图片描述

如果线程的取消类型为 DEFERRED ,当线程收到取消请求后,它会在下次抵达取消点时终止。如果该线程尚未分离,其它线程调用 pthread_join() 进行连接,会收到一个特殊的返回值 PTHREAD_CANCELED 。

设置取消点
void pthread_testcancel(void)

函数 pthread_testcancel() 的目的很简单,就是产生一个取消点。线程如果有挂起的取消请求,只
要调用这个函数,线程就会终止。如果线程执行的代码中不包含取消点,可以周期性地调用
pthread_testcancel() ,以确保对取消请求做出及时响应。

线程清理函数

线程收到取消请求后,会执行到下一个取消点终止。如果线程只是草草地直接终止,可能会让程序处于不一致的状态,比如:对共享变量的修改只进行了一半啦,没有释放互斥锁啦… 这样的错误轻则导致其他线程产生错误的结果、发生死锁,重则让进程直接崩溃。
为了规避这一问题,线程可以设置一个或多个清理函数。当线程响应取消请求时,会自动执行这些清理函数。
每个线程都拥有一个清理函数栈。当线程响应取消请求时,会依次执行栈中的清理函数 (从栈顶到栈
底)。当执行完所有的清理函数后,线程终止。

void pthread_cleanup_push(void(* routine(void*)), void* arg) —向线程清理函数栈中添加一个函数

参数

  1. routine,函数指针,添加的清理函数
  2. arg,清理函数的参数

void pthread_cleanup_pop(int execute) —执行线程清理函数栈顶的函数,并出栈

参数

  1. execute,如果不为 0 则执行出栈的线程清理函数;如果为 0 则不会执行出栈的函数

在 linux 中,以上两个函数的定义为:

# define pthread_cleanup_push(routine, arg)	    \
do {                                   	        \
	__pthread_cleaup_class __clframe (routine, arg) \
# define pthread_cleanup_pop(execute)		    \
	__clframe.__setdoit (execute);     	  		\
} while (0)

从定义可以看出,入栈和出栈的操作必须一一对应,否则就会是编译错误。
从线程清理函数产生的意义来看,可以知道线程清理函数执行的时机是线程在执行过程中要终止的时候,例如:响应其他线程的取消请求,主动调用pthread_exit()函数

进程退出函数线程清理函数
从main函数中返回(执行)从start_routine返回(不执行)
执行exit(执行)执行pthread_exit(执行)
响应信号(不执行)响应取消请求(执行)

2、线程同步

竟态条件:程序执行的结果,取决于线程的调度情况,而非程序本身逻辑。

线程之间可以方便、快速地共享数据,但如果多个线程同时修改同一数据,就可能会引发一些问题,这些问题我们统称为并发安全问题。为解决并发问题,引入了互斥量和条件变量等机制。

  1. 互斥量:互斥量是用来保障线程之间可以互斥地访问共享资源。
  2. 条件变量:条件变量是用来让线程 (停止执行) 等待某个条件成立,它是一种等待唤醒机制。

互斥锁

互斥量有两种状态,lock 和 unlock ,任何时候最多只有一个线程可以锁定互斥量

初始化互斥量

//初始化互斥量的两种方式,互斥量在使用前必须要初始化

//当互斥量是全局变量时,静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//当互斥量是局部变量时,动态初始化
int pthread_mutex_init(pthread_mutex* mutex, const pthread_mutexattr_t* attr);
/*
	返回值:
	 成功,返回 0 
	 失败,返回错误码,不设置 errno
*/

上锁

//有三种方式可以锁定互斥量

//会一直阻塞,直到互斥量被释放
int pthread_mutex_lock(pthread_mutex_t* mutex);
//互斥量已被锁定,则执行失败,立即返回错误码 EBUSY
int pthread_mutex_trylock(pthread_mutex_t* mutex);
//在预定的时间内互斥量一直被锁定,超时后执行失败,返回错误码 ETIMEDOUT
int pthread_mutex_timelock(pthread_mutex_t* mutex, const struct timespec* abstime);
/*
	三者的返回值:
	 成功,返回 0 
	 失败,返回错误码,不会设置 errno 
*/

释放锁

//每次只能将一个互斥量释放,若互斥量本身就是未加锁的,则会出现异常
int pthread_mutex_unlock(pthread_mutex_t* mutex);
/*
	返回值:
	 成功,返回 0 
	 失败,返回错误码,不设置 errno
*/

销毁互斥量

当不再需要自动或动态分配的互斥量时,应使用 pthread_mutex_destroy() 将其销毁。对于静态初始化的互斥量,则无需销毁。
经 pthread_mutex_destroy() 销毁的互斥量,可调用 pthread_mutex_init() 对其重新初始化。

int pthread_mutex_destory(pthread_mutex_t* mutex)
/*
	返回值:
	 成功,返回 0 
	 失败,返回错误码,不设置 errno
*/

死锁

引入锁的机制后,可能会使程序进入死锁的状态
死锁产生的条件:
互斥:多个线程不可同时持有同一个资源。
持有并等待: 线程持有资源 1,想申请资源 2;而资源 2 被另一线程所持有,所以线程陷入阻塞;但是线程陷入阻塞之前并不会释放自己所持有的资源 1。
不可抢占:当线程已经持有了资源 ,在使用完之前不能被其他线程获取。
循环等待:线程和资源之间的关系形成了一条环路。

解决死锁问题就是要破坏死锁产生的条件

typedef struct {
	int id;
	int balance;
	pthread_mutex_t mutex;
} Account;

Account a = {1, 100, PTHREAD_MUTEX_INITIALIZER};
Account b = {2, 1000, PTHREAD_MUTEX_INITIALIZER};

pthread_mutex_t protection = PTHREAD_MUTEX_INITIALIZER; // 保护锁
//互斥是不能被破坏的

//持有并等待:要么全部持有,要么全都不持有
void function_1(void)
{
	pthread_mutex_lock(&protection);//保证了获得a和b的操作是一个原子操作
	pthread_mutex_lock(&a->mutex);
	pthread_mutex_lock(&b->mutex);
	pthread_mutex_unlock(&protection);
	//...
}

//不可抢占:主动放弃
void function_2(void)
{
start:
	pthread_mutex_lock(&a->mutex);
	if(pthread_mutex_trylock(&b->mutex) != 0)
	{
		pthread_mutex_unlock(&a->mutex);
		goto start:
	}
	//...
}

//循环等待:按固定顺序对互斥量加锁
void function_3(void)
{
	if(a.id > b.id)
	{
		pthread_mutex_lock(&a->mutex);
		pthread_mutex_lock(&b->mutex);
	}
	else
	{
		pthread_mutex_lock(&b->mutex);
		pthread_mutex_lock(&a->mutex);
	}
	//...
}

条件变量

初始化

//初始化条件变量有两种方式,条件变量在使用之前一定要初始化

//静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//动态初始化
int pthread_cont_init(pthread_cond_t* cond, const pthread_condattr_t* attr);
/*
	返回值:
	 成功,返回 0 
	 失败,返回错误码,不设置 errno
*/

等待

//当条件不成立时,线程会被阻塞,等待条件成立

//一直阻塞
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
//设定了阻塞的时间,超时返回 ETIMEOUT
int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);
/*
	返回值:
	 成功,返回 0 
	 失败,返回错误码,不设置 errno
	参数:
	 cond,要等待的条件
	 mutex,保护 cond 互斥量
*/

pthread_cond_wait() 函数的执行过程

  1. 在调用 pthread_cond_wait() 之前要先对其加锁(参数 mutex )
  2. 执行的第一步是释放 mutex
  3. 陷入阻塞状态,等待 cond 条件成立
  4. pthread_cond_wait() 返回时,调用线程一定又一次获得了 mutex

当 pthread_cond_wait() 返回时,cond 不一定是成立的,只能证明 cond 曾经成立过

唤醒

//唤醒至少一个等待 cond 的线程
int pthread_cond_signal(pthread_cond_t* cond);
//唤醒所有等待 cond 的线程
int pthread_cond_broadcast(pthread_cond_t* cond);
/*
	返回值:
	 成功,返回 0 
	 失败,返回错误码,不设置 errno
*/

销毁

当不再需要自动或动态分配的条件变量时,应调用 pthread_cond_destroy() 函数予以销毁。对于静态初始化的条件变量,则无需销毁。
经 pthread_cond_destroy() 销毁的条件变量,可以调用 pthread_cond_init()重新初始化。

int pthread_cond_destory(pthread_cond_t* cond);
/*
	返回值:
	 成功,返回 0 
	 失败,返回错误码,不设置 errno
*/

3、生产者消费者模型

生产者和消费者模型中有三个角色:

  1. 生产者:生产商品。如果队列满了,生产者陷入阻塞,等待队列不满(not_full);如果队列不满,将商品添加到队列中,此时队列不空 (not_empty),唤醒消费者。
  2. 消费者:消费商品。如果队列空了,消费者陷入阻塞,等待队列不空(not_empty);如果队列不空,从队列中删除商品,此时队列不满 (not_full),唤醒生产者。
  3. 阻塞队列:当队列满时,如果线程往阻塞队列中添加商品,该线程阻塞;当队列空时,如果线程从阻塞队列中删除商品,该线程阻塞。

模型的实现(利用简易的线程池)

//Block.h
#pragma once

#include <func.h>
#define N 1024

typedef int E;

typedef struct
{
    E elements[N];
    int front;
    int rear;
    int size;
    pthread_mutex_t mutex;
    pthread_cond_t not_empty;
    pthread_cond_t not_full;
} BlockQ;
//创建阻塞队列
BlockQ* blockq_create(void);
//销毁阻塞队列
void blockq_destroy(BlockQ* q);
//队列判空
bool blockq_empty(BlockQ* q);
//队列判满
bool blockq_full(BlockQ* q);
//入队
void blockq_push(BlockQ* q, E val);
//出队
E blockq_pop(BlockQ* q);
//查看队头元素
E blockq_peek(BlockQ* q);
/********************************************************************************/

//Block.c
#include <func.h>
#include "BlockQ.h"

//创建阻塞队列
BlockQ* blockq_create(void)
{
    BlockQ* Q = (BlockQ*)malloc(sizeof(BlockQ));
    if(Q == NULL)
        error(1, errno, "malloc");

    Q->front = 0;
    Q->rear = 0;
    Q->size = 0;

    pthread_mutex_init(&Q->mutex, NULL);
    pthread_cond_init(&Q->not_empty, NULL);
    pthread_cond_init(&Q->not_full, NULL);

    return Q;
}

//销毁阻塞队列
void blockq_destroy(BlockQ* Q)
{
    pthread_cond_destroy(&Q->not_full);
    pthread_cond_destroy(&Q->not_empty);
    pthread_mutex_destroy(&Q->mutex);

    free(Q);
}

//队列判空
bool blockq_empty(BlockQ* Q)
{
    pthread_mutex_lock(&Q->mutex);
    int sz = Q->size;
    pthread_mutex_unlock(&Q->mutex);
    return sz == 0;
}

//队列判满
bool blockq_full(BlockQ* Q)
{
    pthread_mutex_lock(&Q->mutex);
    int sz = Q->size;
    pthread_mutex_unlock(&Q->mutex);
    return sz == N;
}

//入队
void blockq_push(BlockQ* Q, E val)
{
    pthread_mutex_lock(&Q->mutex);
    //此时判满不能调用 block_full,因为 mutex 已经上锁,会导致死锁
    while(Q->size == N)
        pthread_cond_wait(&Q->not_full, &Q->mutex);// 返回时 cond 不一定成立,需要重新判断
	
    Q->elements[Q->rear] = val;
    Q->rear = (Q->rear + 1) % N;
    Q->size++;

    pthread_cond_signal(&Q->not_empty);

    pthread_mutex_unlock(&Q->mutex);
}

//出队
E blockq_pop(BlockQ* Q)
{
    pthread_mutex_lock(&Q->mutex);
    //此时判满不能调用 block_empty,因为 mutex 已经上锁,会导致死锁
    while(Q->size == 0)
        pthread_cond_wait(&Q->not_empty, &Q->mutex);//返回时 cond 不一定成立,需要重新判断

    E ret = Q->elements[Q->front];
    Q->front = (Q->front + 1) % N;
    Q->size--;

    pthread_cond_signal(&Q->not_full);

    pthread_mutex_unlock(&Q->mutex);
    return ret;
}

// 查看队头元素(暂时无用)
E blockq_peek(BlockQ* Q)
{
    pthread_mutex_lock(&Q->mutex);
    while(Q->size == 0)
        pthread_cond_wait(&Q->not_empty, &Q->mutex);
    E ret = Q->elements[Q->front];

    pthread_mutex_unlock(&Q->mutex);

    return ret;
}
/********************************************************************************/

//pc.c
#include <func.h>
#include "BlockQ.h"

#define COUNT 8 // 线程池中线程的数量

typedef struct
{
    pthread_t thread[COUNT];
    BlockQ* Q;
} pool;

//线程的执行函数
void* start_routine(void* arg)
{
    pool* pl = (pool*)arg;
    while(1)
    {
        E ret = blockq_pop(pl->Q);
        // 访问到特殊元素时结束线程
        if(ret == -1)
        {
            printf("0x%lu exit\n", pthread_self());
            sleep(3);
            pthread_exit(NULL);
        }
            sleep(3);
        printf("0x%lu EXE %d\n",pthread_self(), ret);
    }
    return NULL;
}

//创建并初始化线程池
pool* create_pool(void)
{
    pool* pl = (pool*)malloc(sizeof(pool));
    pl->Q = blockq_create();
    for(int i = 0; i < COUNT; ++i)
    { 
        int err =  pthread_create(pl->thread + i, NULL, start_routine, (void*)pl);
        if(err)
            error(1, err, "pthread_create");
    }

    return pl;
}

//销毁线程池
void destory_pool(pool* pl)
{
    blockq_destroy(pl->Q);
    free(pl);
}

int main(void)
{
    // 主线程
    // 1. 创建线程池
    pool* pl = create_pool();
    // 2. 主线程往阻塞队列中放任务; 子线程从阻塞队列中出并执行任务
    for(int i = 1; i <= 100; ++i)
        blockq_push(pl->Q, i);
    // 3. 退出线程池
    for(int i = 1; i <= COUNT; ++i)
        blockq_push(pl->Q, -1);
    for(int i = 0; i < COUNT; ++i)
        pthread_join(*(pl->thread + i), NULL);
    // 4. 销毁线程池
    destory_pool(pl);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值