【Linux | 系统编程】Linux系统编程(文件、进程线程、进程间通信)

文章目录

  • Linux系统编程
    • 文件IO
      • open/close函数
      • read/write函数
      • 文件描述符
      • 阻塞、非阻塞
      • fcntl函数
      • lseek函数
      • 传入传出参数
    • 文件系统
      • 文件存储
    • 文件操作
      • stat函数
      • access/chmod函数
      • link/unlink函数
      • readlink函数
      • rename函数
    • 目录操作
      • getcwd/chdir函数
      • 文件、目录权限
      • opendir/closedir函数
      • readdir函数
      • rewinddir函数
      • telldir/seekdir函数
      • dup/dup2函数
    • 进程概述
      • 概念
      • PCB进程控制块
      • 环境变量
    • 进程控制
      • fork函数
      • exec函数族
      • wait/waitpid函数
    • IPC方法
      • 概念
      • 管道
        • pipe函数
        • 管道的读写行为
        • FIFO
      • 消息队列
        • msgget函数
        • ftok函数
        • msgctl函数
        • msgsnd函数
        • msgrcv函数
      • 共享内存映射
        • 文件实现进程间通信
        • 共享存储映射
        • mmap父子间通信
        • mmap非血缘关系进程间通信
        • 匿名映射
      • 信号
        • 概念
        • kill函数
        • alarm函数
        • setitimer函数
        • 信号集操作函数
        • 信号捕捉
        • SIGCHLD信号
        • 中断系统调用
    • 守护进程、线程
      • 进程组和会话
      • 守护进程
      • 线程
      • 线程同步
        • 互斥锁
        • 读写锁
        • 条件变量
        • 生产者消费者模型——互斥锁mutex
        • 信号量
        • 生产者消费者模型——信号量sem

Linux系统编程

系统调用:由操作系统实现并提供给外部程序的编程接口(API)

函数实现从用户区到内核区的数据传输就是依靠系统调用

在man手册的第二卷存放的就是系统调用。但是严格来说应该叫做系统函数,因为它对真正的系统调用做了一次浅封装,即仅仅是用另一个名字不同的函数来封装系统调用。如man手册中的open、write、read函数,其真正的系统调用为sys_open、sys_write、sys_read

文件IO

open/close函数

open和close函数需要头文件#include,参数中的宏定义需要头文件#include

open函数

原型:

  • int open(const char *pathname, int flag);
  • int open(const char *pathname, int flag, mode_t mode);

参数:

  • pathname:文件路径
  • flag:打开方式。取值为O_RDONLY只读、O_WRONLY只写、O_RDWR读写、O_APPEND追加、O_CREAT创建、O_EXCL存在、O_TRUNC截断、O_NONBLOCK非阻塞。使用多个参数可以用|来连接
  • mode:三位八进制文件权限码。八进制表示需要以0开头,如0644。文件最终权限会受到umask掩码影响,计算方式为mode & ~umask

返回值:一个文件描述符,当为-1时表示打开失败,出现错误

close函数

原型:

  • int close(int fd);

参数:

  • fd:文件描述符。将open函数返回的文件描述符作为参数即可关闭该文件

返回值:0表示关闭成功,-1表示关闭失败

open常见错误:①打开文件不存在;②以写方式打开只读文件(打开文件没有对应权限);③以只写方式打开目录

errno:最后一次错误的序号,是一个全局变量。当程序出现错误时的一个用于指明错误的错误号,使用时需要头文件#include

strerror:string strerror(int errno);用于解释errno所代表的错误内容

perror:void perror(const char *s);直接输出自定义的错误信息,参数就是用户自定义的错误信息字符串

read/write函数

需要头文件#include

read函数

原型:ssize_t read(int fd, void *buf, size_t count);

参数:

  • fd:文件描述符
  • buf:存储数据的缓冲区
  • count:缓冲区的大小

返回值:返回一个正数表示读取成功,是读取到的字节数;0表示已经读取到文件尾,读取结束;-1表示读取失败,设置errno;-1且errno=RAGIN或EWOULDBLOCK表示不是读取失败,而是read在以非阻塞方式读一个设备文件或网络文件,且文件无数据

write函数

原型:ssize_t write(int fd, const void *buf, size_t count);

参数:

  • fd:文件描述符
  • buf:待写出数据的缓冲区
  • count:实际写入内容的大小

返回值:返回一个正数表示写入成功,是写入的字节数;0表示写入完毕,没有更多内容可写入;-1表示写入失败,设置errno

strace:strace ./test可以跟踪命令执行时的系统调用

优先使用库函数而不是系统调用,系统调用效率不一定比库函数高

预读入缓输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I8HunCfY-1664298269830)(https://gitee.com/AnonymYH/markdown-image/raw/master/img/20200723142043813.png)]

系统维护一个内核缓冲区,当进行写入操作(缓输出)时,数据从用户去写入内核区缓存时即认为是写入成功,但是系统会利用算法在合适的时机把内核缓冲区的数据真正写入到磁盘上;进行读取操作(预读入)时,如果读取的字节数太小,系统仍会把一整块数据从磁盘读取到内核缓冲区上,在下次读取时不需要再去进行磁盘上的物理操作,节省时间

文件描述符

每一个进程对应一个PCB,PCB进程控制块是一个结构体struct task_struct,其中一个指针成员指向文件描述符表,其中存放着该进程所使用的文件对应的文件描述符。一个进程能打开的最大文件数默认为1023

文件描述符:是一个键值对(key, value),其中key为一个整数,value是一个指针,指向一个文件结构体struct file,文件结构体中保存着文件的各种描述信息。通过整数key即可访问文件

新打开的文件的文件描述符一定是当前文件描述符表中最小的整数

阻塞、非阻塞

阻塞场景:读常规文件不会发生阻塞,只有读设备文件、网络文件时才会发生阻塞

终端对应的文件为/dev/tty

阻塞是设备文件、网络文件的属性。可以通过open函数的flag参数O_NONBLOCK将文件设置为非阻塞状态

当设备文件、网络文件设置为非阻塞时,设备无数据时read不会等待输入而是直接返回-1,并且将errno设置为EAGIN或EWOULDBLOCK

阻塞和非阻塞都不是最好的读取设备或网络文件的方式,最好的方式为设备或网络文件需要被读时向系统主动发出请求,如epoll

fcntl函数

需要头文件#include#include

获取/修改文件状态

原型:int fcntl(int fd, int cmd, .../*arg*/ );

参数:

  • fd:文件标识符
  • cmd:命令。取值为F_GETFL和F_SETFL,获取和设置文件状态

返回值:-1时表示cmd操作失败

修改文件状态:利用F_GETFL获取文件状态的位图flag,利用位运算修改flag中对应位的值即表示修改文件状态,修改后利用F_SETFL将文件状态设置为flag使修改生效

lseek函数

需要头文件#include

设置文件中的偏移位置(文件读写位置)

原型:off_t lseek(int fd, off_t offset, int whence);

参数:

  • fd:文件描述符
  • offset:偏移量。正值表示向后偏移,负值表示向前偏移
  • whence:偏移的起始位置。取值为SEEK_SET文件起始位置、SEEK_CUR当前位置、SEEK_END文件末尾位置

返回值:正数表示从文件起始位置开始的偏移量;-1表示失败,设置errno

文件读写使用的是同一偏移位置

可以用lseek(fd,0,SEEK_END)返回值统计文件大小;用lseek(fd,size,SEEK_END)拓展文件大小,但是想要真正拓展文件大小必须引起IO操作,所以还需要在结尾调用write(fd,"\0",1)

拓展文件还可以使用truncate(path, length)实现,将路径path的文件拓展length长度。成功返回0,失败返回-1并设置errno

传入传出参数

传入参数定义:

  1. 指针作为函数参数
  2. 通常有const关键字修饰
  3. 指针指向有效区域,在函数内做读操作

char *strcpy(char *dest, const char *src);中的src

传出参数定义:

  1. 指针作为函数参数
  2. 在函数调用之前,指着指向的空间可以无意义,但必须有效
  3. 在函数内做写操作
  4. 函数调用结束后,充当函数返回值

char *strcpy(char *dest, const char *src);中的dest

传入传出参数定义:

  1. 指针作为函数参数
  2. 在函数调用之前,指针指向的空间有实际意义
  3. 在函数内先做读操作,再做写操作
  4. 函数调用结束后,充当函数返回值

char *strtok_r(char *str, const char *delim, char **saveptr);中的saveptr

文件系统

文件存储

inode:索引节点,其实是一个结构体struct ,存储文件的属性信息。大多数的inode都存储在磁盘上(存储在内存中的inode为VFS inode),inode中的某个成员指向磁盘上文件存储的盘块位置

dentry:目录项,是一个结构体,主要包含文件名inode号,通过inode号就能找到磁盘上的inode结点。创建硬链接就是相当于创建了一个新的dentry,其中的inode号指向同一个inode结点

文件操作

stat函数

需要头文件#include#include

获取文件属性(从inode中获取)

原型:int stat(const char *path, struct stat *buf);

参数:

  • path:文件路径
  • buf:存放文件属性的缓冲区。是一个传出参数

返回值:0表示获取成功;-1表示获取失败,设置errno

用stat函数去获取符号链接的属性时或发生stat穿透,获取到的是符号链接指向的文件的属性而不是链接本身的属性。解决方法为,使用lstat函数获取符号链接属性

lstat函数:int lstat(const char *path, struct stat *buf);原型和返回值与stat函数相同,不会发生符号穿透

判断文件类型:利用获取的属性中的成员buf.st_mode作为参数传入宏函数S_ISREG()普通文件、S_ISDIR()目录、S_ISCHR()字节设备、S_ISBLK()块设备、S_ISFIFO()管道、S_ISLNK()符号链接、S_ISSOCK()套接字

成员变量st_mode是一个16位的位图,0-3位表示文件类型,4-6表示特殊权限码,7-15表示u、g、o的权限码。通过执行与运算屏蔽其他位来获取每部分的值

access/chmod函数

需要头文件#include

access:int access(const char *pathname, int mode);测试指定文件是否有mode所表示的权限。参数mode表示权限码。返回0表示具有该权限;-1表示不具有该权限,设置errno

需要头文件#include

chmod:int chmod(const char *path, mode_t mode);修改文件访问权限为mode表示的权限。返回0表示修改成功了;-1表示修改失败,设置errno。也可以用int fchmod(int fd, mode_t mode);用文件描述符来指定需要修改的文件

link/unlink函数

需要头文件#include

link函数

为已存在的文件创建硬链接,即目录项

原型:int link(const char *oldpath, const char *newpath);

参数:

  • oldpath:原文件路径。需要是已经存在的路径
  • newpath:创建的硬链接路径

返回值:0表示创建成功;-1表示创建失败,设置errno

unlink函数

删除文件的一个目录项,使硬链接计数减一

原型:int unlink(const char *pathname);

参数:

  • pathname:一个硬链接文件名

返回值:0表示删除成功;-1表示删除失败,设置errno

清除文件时,如果文件的硬链接计数为0,没有目录项dentry对应时,文件并不会马上被释放,而是等到所有打开该文件的进程关闭该文件系统才会在合适的时间释放。所以unlink函数只是让文件具备了被释放的条件,并不是立刻删除了文件

==隐式回收:==当进程结束运行时,所有该进程打开的文件都会被关闭,申请的内存空间会被释放。是系统特性,但是不允许程序员依赖此特性

readlink函数

需要头文件#include

读取符号链接文件本身,得到链接指向的文件名

原型:ssize_t readlink(const char *path, char *buf, size_t bufsiz);

参数:

  • path:符号链接的路径
  • buf:缓冲区。将读取文件的内容写入该缓冲区
  • bufsiz:缓冲区大小

返回值:0表示读取成功;-1表示读取失败,设置errno

在终端可以直接使用readlink命令来读取链接文件的内容

rename函数

需要头文件#include

重命名一个文件

原型:int rename(const char *oldpath, const char *newpath);

参数:

  • oldpath:旧文件的路径
  • newpath:新文件的路径

返回值:0表示重命名成功;-1表示重命名失败,设置errno

目录操作

getcwd/chdir函数

需要头文件#include

getcwd函数

获取进程当前工作目录

原型:char *getcwd(char *buf, size_t size);

参数:

  • buf:缓冲区。保存当前进程工作目录位置
  • size:缓冲区大小

返回值:返回路径表示成功;NULL表示失败

chdir函数

改变当前进程的工作目录

原型:int chdir(const char *path);

参数:

  • path:修改后的工作目录

返回值:0表示修改成功;-1表示修改失败,设置errno

文件、目录权限

目录文件也是文件,文件内容是该目录下所有子文件的目录项dentry

目录文件权限:

  • 读:目录可以被浏览。如ls、tree等
  • 写:创建、删除、修改文件。如mv、touch、mkdir等
  • 执行:可以被打开、进入。如cd

目录设置黏着位:目录的删除、修改只能由文件所有者或者root用户操作。写操作不受影响。黏着位只对目录有效,对文件无效

opendir/closedir函数

需要头文件#include

opendir函数

根据传入的目录名打开一个目录

原型:DIR *opendir(const char *name);

参数:

  • name:目录名。支持绝对路径和相对路径

返回值:返回指向该目录的结构体指针表示成功;NULL表示打开失败

closedir函数

关闭打开的目录

原型:int closedir(DIR *dirp);

参数:

  • dirp:指向目录的指针

返回值:0表示关闭成功;-1表示关闭失败,设置errno

readdir函数

需要头文件#include

读取目录。一次读取一个目录项

原型:struct dirent *readdir(DIR *dirp);

参数:

  • dirp:指向目录的指针

返回值:返回目录项结构体指针表示成功;NULL表示读取结束;NULL且设置errno时表示读取错误

读取返回指针的成员变量d_name可以获取目录项对应的文件名dirp->d_name

rewinddir函数

需要头文件#include

回卷目录读写位置至起始位置

不是系统调用,是库函数

原型:void rewinddir(DIR *dirp);

参数:

  • dirp:指向目录的指针

telldir/seekdir函数

需要头文件#include

telldir函数

获取目录读写位置

原型:long telldir(DIR *dirp);

参数:

  • dirp:指向目录的指针

返回值:返回目录的读写位置表示获取成功;-1表示获取失败,设置errno

seekdir函数

修改目录读写位置

原型:void seekdir(DIR *dirp, long loc);

参数:

  • dirp:指向目录的指针
  • loc:修改后目录的读写位置。一般由telldir获取

dup/dup2函数

需要头文件#include

新建或令已有的一个文件描述符指向旧文件

原型:

  • int dup(int oldfd);
  • int dup2(int oldfd, int newfd);

参数:

  • oldfd:旧的文件描述符
  • newfd:新的文件描述符

返回值:返回新的文件描述符表示成功;-1表示失败,设置errno

可以用于实现重定向,让标准输出的文件描述符STDOUT_FILENO指向想要重定向的文件dup2(fd, STDOUT_FILENO)

fcntl函数也能实现dup函数的功能。参数cmd取值为F_DUPFD,就会返回一个新建的文件描述符。也可以再添加一个整数参数指定文件描述符的key值,若该文件描述符未被占用则令其指向旧文件描述符对应的文件;若被占用则返回从该数字开始最小的文件描述符

进程概述

概念

程序:是存储在磁盘上的编译好的二进制文件

进程:是活跃的程序,占用系统资源,在内存中执行

并发:在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但是任一时刻点上只有一个进程在运行

单道程序设计:所有进程一个个排队依次执行

多道程序设计:在计算机内存中同时存放几道相互独立的程序,在管理程序控制下相互穿插运行

MMU:内存管理单元。负责进行虚拟地址到物理地址的映射管理

孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程。此时init进程会领养孤儿进程,即成为它的父进程

僵尸进程:子进程终止,父进程尚未回收,子进程残留资源即PCB存放于内核中,变成僵尸进程。无法被kill终止,因为僵尸进程本身就已经终止,结束僵尸进程的方式为kill它的父进程,让僵尸进程成为孤儿进程被init收养,init会自动回收它

PCB进程控制块

PCB是一个结构体task_struct,来维护进程相关信息和资源

task_struct的主要内部成员:

  • 进程的id。每个进程都有唯一的id,在C语言中用pid_t类型表示
  • 进程状态。有就绪、运行、挂起、停止
  • 进程切换时需要保存和恢复的一些CPU寄存器
  • 描述虚拟地址空间的信息
  • 描述控制终端的信息
  • 当前工作目录
  • umask掩码
  • 文件描述符表。存放一个进程所使用的所有文件描述符
  • 和信号相关的信息
  • 用户id和组id
  • 会话和进程组
  • 进程可以使用的资源上限

环境变量

PATH:可执行文件的搜索路径。可以包含多个路径,执行命令时会依次去每个路径下找

SHELL:当前Shell

TERM:当前终端类型

LANG:语言和locale,决定了字符编码以及时间、货币等信息的显示格式

HOME:当前用户的宿主目录(家)

进程控制

fork函数

需要头文件#include

创建一个子进程

原型:pid_t fork(void)

返回值:父进程返回子进程的id;子进程返回0;返回-1表示创建子进程失败

fork之后的代码会被父进程和子进程分别执行,所以会执行两遍

可以通过getpid函数获取当前进程的pid,通过getppid获取当前进程的父进程的pid;通过getuid函数获取当前进程实际用户id,通过geteuid函数获取当前进程有效用户id;通过getgid函数获取当前进程使用用户组id,通过getegid函数获取当前进程有效用户组id

父子进程遵循读时共享写时复制原则。当子进程只是读父进程用户空间的内容,那就不用复制,直接使用父进程用户空间的内容;当父进程或者子进程需要写入内容时,再复制父进程的用户空间的相应内容进行写操作

父子进程相同的部分:data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式

父子进程不同的部分:进程id、fork返回值、各自的父进程、进程创建时间、闹钟、未决信号集

**父子进程共享的部分:**文件描述符、mmap映射区

父子进程的执行顺序取决于内核使用的调度算法

gdb调试时只能跟踪一个进程,当进程中调用了fork函数产生了子进程后,gdb默认跟踪父进程。也可以使用命令set follow-fork-mode child在fork函数调用之前设置

父进程结束后就会返回至shell进程,此时会打印命令提示符。但是父进程结束时子进程不一定执行结束,所以可能出现命令提示符比子进程的输出先打印的情况,说明bash在子进程之前强占了资源执行了打印

exec函数族

需要头文件#incldue

调用exec函数可以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以进程的id并不改变,换核不换壳

exec执行结束后不能返回给调用者

原型:

  • int execl(const char *path, const char *arg, ...);
  • int execlp(const char *file, const char *arg, ...);
  • int execle(const char *path, const char *arg, ..., char *const envp[]);
  • int execv(const char *path, char *const argv[]);
  • int execvp(const char *file, char *const argv[]);
  • int execve(const char *path, char *const argv[], char *const envp[]);

函数名中的p表示PATH环境变量

execlp函数

加载一个进程,借助PATH环境变量。通常来调用系统调用

参数

  • file为可执行文件名
  • arg表示命令的选项参数,可以传入多个选项。因为arg是从0开始的,所以arg的第一个参数和file相同。最后一个参数必须是NULL,作为哨兵声明不再传入参数。

返回值:正常情况没有返回值,只有当出错时才会返回-1,设置errno

execl函数

加载一个指定路径下的进程

参数:

  • path:可执行文件名所在的路径
  • arg:同execlp

返回值:同execlp

bash进程使用命令执行其他可执行程序的过程就是依靠exec函数族

其他exec函数都是库函数,只有execve是系统调用,其他exec函数的实现都是通过封装execve实现的

wait/waitpid函数

需要头文件#include

回收子进程终止信息。功能包括①阻塞等待子进程退出;②回收子进程残留资源;③获取子进程结束状态(退出原因)

wait函数

原型:pid_t wait(int *status);

参数:

  • status:传出参数。传出子进程的退出状态

返回值:返回子进程的id表示回收成功;-1表示没有子进程

通过宏函数解释传出参数status表示的子进程退出状态,具体宏函数查看wait函数的man手册

一次wait调用只能回收一个子进程

waitpid函数

原型:pid_t waitpid(pid_t pid, int *status, int options);

参数

  • pid:指定回收的进程。取值为-1时表示任意子进程,相当于wait;0表示回收和当前调用一个进程组的所有子进程
  • status:同wait
  • options:额外参数。通常用于设置非阻塞,不设置的话置0

返回值:返回终止的子进程id表示回收成功;-1表示回收失败;0表示options指定了WNOHANG即不挂起,且没有子进程结束

IPC方法

概念

IPC:进程间通信。进程地址空间相互彼此独立,每个进程都有各自不同的用户地址空间,想要交换数据需要通过内核,在内核中开辟一块缓冲区,通过共享的一块内核缓冲区进行通信

方法:文件、管道、信号、共享内存、消息队列、套接字、命名管道

伪文件:管道、块设备、字符设备、套接字。不占用磁盘空间

管道

是伪文件,即内核缓冲区

两个文件描述符引用,一个表示读端,一个表示写端。规定数据从写端流入,从读端流出管道,

数据在管道里只能单向流动,数据不可以反复读,读完就释放

原理:内核使用环形队列机制,借助4K内核缓冲区实现

一般用于有血缘关系的进程之间,如父子进程

pipe函数

需要头文件#include

创建匿名管道,并打开管道

原型:int pipe(int pipefd[2]);

参数:

  • pipefd[2]:读端和写端。pipefd[0]是读端,pipefd[1]是写端

返回值:0表示创建成功;-1表示创建失败,设置errno

管道的缓冲区大小为4096字节

管道的读写行为

读管道:

  • 管道中有数据,read返回实际读到的字节数
  • 管道无数据:①管道写端被全部关闭,read返回0;②管道写端没有全部关闭,read阻塞等待

写管道:

  • 管道读端全部被关闭,进程异常终止
  • 管道没有全部关闭:①管道已满,write阻塞;②管道未满,write将数据写入,返回实际的字节数

一般一个管道只有一个读端和一个写端,在使用管道时要将不使用读/写端的进程的对应端close掉。但是管道允许一个写端与多个读端多个写端与一个读端

FIFO

命名管道。匿名管道pipe只能用于有血缘关系的进程之间,FIFO能用于不相关的进程之间的通信

创建FIFO文件:命令mkfifo 管道名、函数int mkfifo(const char *pathname, mode_t mode);

函数mkfifo

需要头文件#include#include

原型:int mkfifo(const char *pathname, mode_t mode);

参数:同open函数

  • pathname:创建管道的路径名
  • mode:权限码。最终权限会受umask影响mode & ~umask

返回值:0表示创建成功;-1表示创建失败,设置errno

写入:先以只写方式打开FIFO文件open(path,O_WRONLY),然后再向文件里写入数据write(fd,buf,len)

读出:先以只读方式打开FIFO文件open(path,O_RDONLY),然后再读出文件里的数据read(fd,buf,size)

消息队列

消息队列是消息的链表,存放在内核中并由消息队列标识符表示

消息队列提供了一个从一个进程向另一个进程发送数据块的方法,每个数据块都可以被认为是有一个类型,接受者接受的数据块可以有不同的类型

每条消息的最大长度上限为MSGMAX,每个消息队列的最大字节数为MSGMNB,每个消息队列的最大消息数为MSGMNI,具体的值可以通过cat /proc/sys/kernel/msgmax查看

特点:

  • 支持双向通信
  • 使用格式化数据进行传输

msgget函数

需要头文件#include #include #include

创建和访问一个消息队列

原型:int msgget(key_t key, int msgflag);

参数:

  • key:消息队列的IPC key值,通过ftok()函数返回
  • msgflag:为IPC_CREATIPC_EXCL,前者为如果消息队列不存在就创建,如果存在就打开该消息队列并返回,后者单独使用无意义,两者一起使用表示消息队列不存在则创建,若存在则返回错误

返回值:成功返回非负整数作为消息队列的标识码,失败返回-1并设置error

ftok函数

需要头文件#include #include

把从指定路径的文件与一个低序8位的整数标识符组合成一个整数IPC键

原型:key_t ftok(const char* pathname, int proj_id);

参数:

  • pathname:文件路径
  • proj_id:8位标识符,范围为0-255,可以自定义

返回值:成功返回IPC键值,错误返回-1并设置error

ftok的实现是通过调用stat函数获取文件的属性,利用属性中的st_dev文件所在文件系统的信息、st_ino文件的索引节点号、proj_id低序8为标识符组合产生一个32为IPC键值,具体组合为**proj_id的后8位+st_dev的后8位+st_ino的后16位**

msgctl函数

需要头文件#include #include #include

原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数:

  • msqid:调用msgget函数返回的消息队列标识码
  • cmd:为IPC_STAT、IPC_SET、IPC_RMID。IPC_STAT获取消息队列的属性并通过buf传出;IPC_SET使用buf设置消息队列的属性;IPC_RMID删除消息队列
  • buf:根据cmd的值,作为传入参数或传出参数

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

msgsnd函数

需要头文件#include #include #include

原型:int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);

参数:

  • msqid:调用msgget函数返回的消息队列标识码
  • msgp:发送的消息。可以为任意结构体,但首字段必为long类型
  • msgsz:发送消息的长度
  • msgflg:默认为0。可以为IPC_NOWAIT、IPC_NOERROR。IPC_NOWAIT表示消息队列满时,该函数不等待立即返回;IPC_NOERROR表示当发送长度大于msgsz时,丢弃多余部分且不通知发送进程

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

msgrcv函数

需要头文件#include #include #include

原型:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数:

  • 前面同msgsnd
  • msgtyp:为0表示接收第一个消息;大于0表示接受类型等于msgtyp的第一个消息;小于0接收类型小于等于msgtyp绝对值的第一个消息
  • msgflg:默认为0,表示阻塞式接收消息。为IPC_NOWAIT、IPC_EXCEPT、IPC_NOERROR。IPC_EXCEPT表示返回消息队列中第一个类型不为msgtyp的消息

返回值:成功返回接收到的数据长度,失败返回-1并设置error

共享内存映射

文件实现进程间通信

原理:打开的文件是内核中的一块缓冲区。多个无血缘关系的进程,可以同时访问该文件

在读文件时如果没有数据不会阻塞,直接返回0。因为只有读管道、设备和网络文件时才会阻塞,读普通文件不会发生阻塞

阻塞与非阻塞是对于文件而言的,而不是read、write等的属性

共享存储映射

两个不相关的进程可以通过打开同一个文件进行读写来进行通信

存储映射I/O:使一个磁盘文件与存储空间中的一个缓冲区相映射

映射区的内容可以重复读取

共享内存进行消息通信是速度最快

mmap函数

需要头文件#include

创建一个内存映射区

原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数:

  • addr:指定映射区的首地址。通常取NULL,让系统自动分配到地址
  • length:共享内存映射区的大小。一般小于等于文件实际大小
  • prot:共享内存映射区的读写属性。取值为PROT_EXEC、PROT_READ、PROT_WRITE、PROT_NONE
  • flags:标注共享内存的共享属性。取值MAP_SHARED、MAP_PRIVATE
  • fd:用于创建共享内存映射区的文件的文件描述符
  • offset:偏移位置。必须是4K的整数倍。默认0,表示映射文件全部

返回值:返回映射区的首地址表示成功;MAP_FAILED表示失败,设置errno

munmap函数

需要头文件#include

释放回收指定的内存映射区

原型:int munmap(void *addr, size_t length);

参数:

  • addr:映射区的地址。即mmap的返回值
  • length:映射区的大小

返回值:0表示是释放成功;-1表示释放失败,设置errno

使用共享映射区:

  1. 先打开想要映射的文件fd = open("file", O_RDWR)
  2. 创建映射区p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
  3. 写入映射区memcpy(p, &src, sizeof(src));读取映射区printf("%s", p)
  4. 关闭映射区munmap(p, len)

mmap注意事项:

  • 用大小为0的文件指定创建非0大小的映射区,会出现总线错误;用大小为0的文件指定创建0大小的映射区,会出现无效参数错误
  • 文件的属性为只读,映射区属性为读写时,会出现无效参数错误;文件属性为只读,映射区也为只读时,可以创建映射区;文件属性为只写,映射区也为只写时,不能创建映射区。因为创建映射区时需要读取文件内容,需要read权限,所以文件至少有一个RD权限
  • 文件描述符fd在mmap创建映射区完成后即可关闭,后续使用地址访问访问文件
  • offset必须是4K的整数倍,因为MMU映射的最小单位就是4K
  • 映射区访问权限为MAP_PRIVATE时,对内存所做的修改不会反映到磁盘文件上。且此时文件权限只需要有RD权限即可,用于创建映射区

mmap父子间通信

先mmap,再fork

使用映射区的方式同上

mmap非血缘关系进程间通信

使用两个进程,一个读,一个写

两个进程使用同一个文件分别创建内存映射区,虽然内存映射区不同,但是两个进程对各自内存映射区的操作都会反映到文件上,从而建立了通信

匿名映射

可以不依赖文件创建匿名映射区,只能用于有血缘关系之间的文件

使用:mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0)

信号

概念

信号是信息的载体

共性:简单、不能携带大量信息、满足条件发送

特性:软件层面上的“中断”

信号机制:收到信号后,不管执行到程序的什么位置,都必须暂停运行去处理信号。信号处理完毕后再继续执行

进程收到的所有信号都是由内核负责发送的,内核处理

信号产生:

  • 按键产生:Ctrl+c终止进程、Ctrl+z挂起进程到后台、Ctrl+\终止进程
  • 系统调用产生:kill、raise、abort
  • 软件条件产生:定时器alarm
  • 硬件异常产生:非法访问内存、除0、内存对齐出错
  • 命令产生:kill命令

信号状态:

  • 产生:内核生成一个信号
  • 递达:信号递送并且到达进程,直接被内核处理
  • 未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态

信号处理:

  • 执行默认动作
  • 忽略(丢弃)
  • 捕捉(调用户处理函数)

阻塞信号集(信号屏蔽字):将某些信号加入集合,对它们设置屏蔽。收到对应的信号后,它的处理将延后(解除屏蔽后)

未决信号集:①信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。处理后翻转为0;②信号产生后由于某些原因(主要是阻塞)不能递达。这类信号称为未决信号集。屏蔽解除前。信号一直处于未决状态

kill -l命令可以列出信号表。1-31为普通信号,有默认处理动作;34-64为实时信号,无默认处理动作

信号四要素:编号、名称、事件、默认处理动作

常用的常规信号:

  • 1 SIGHUP:当用户退出Shell,由该shell启动的所有进程都将收到这个信号。默认动作为终止进程
  • 2 SIGHINT:当用户按下Ctrl+c,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程
  • 3 SIGQUIT:当用户按下Ctrl+\,用户终端向正在运行中的由该终端启动的程序发出信号。默认动作为终止进程
  • 4 SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
  • 5 SIGTRAP:该信号由断点指令或其他trap指令产生。默认动作为终止进程并产生core文件
  • 6 SIGABRT:调用abort函数时产生该信号。默认动作为终止信号并产生core文件
  • 7 SIGBUS:非法访问内存地址,包括内存对齐出错。默认动作为终止进程并产生core文件
  • 8 SIGFPE:发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除0等所有的算法错误。默认动作为终止进程并产生core文件
  • 9 SIGKILL:无条件终止进程。该信号不能被忽略、处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法
  • 10 SIGUSR1:用户定义的信号,即程序员可以在程序中定义并使用该信号。默认动作为终止进程
  • 11 SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件
  • 12 SIGUSR2:另一个用户定义信号,同SIGUSR1
  • 13 SIGPIPE:向一个没有读端的管道写数据。默认动作为终止进程
  • 14 SIGALRM:定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程
  • 15 SIGTERM:程序结束信号,与SIGKILL不同,可以被阻塞和终止。通常用来要使程序正常退出,执行shell命令kill时,缺省产生这个信号。默认动作为终止进程
  • 17 SIGCHLD:子进程状态发生变化时,父进程会收到这个信号。默认动作为忽略这个信号
  • 18 SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略
  • **19 SIGSTOP:**停止进程的执行。不能被忽略、处理和阻塞。默认动作为暂停进程
  • 20 SIGTSTP:终止终端交互进程的运行。按下Ctrl+z时发出这个信号。默认动作为暂停进程

默认动作:

  • Term:终止进程
  • Ign:忽略信号
  • Core:终止进程,生成core文件
  • Stop:停止(暂停)进程
  • Cont:继续运行进程

kill命令:kill -信号名或变化 进程pid向指定进程发送指定信号

kill函数

需要头文件#include#include

向指定进程发送指定信号

原型:int kill(pid_t pid, int sig);

参数:

  • pid:进程pid号。0表示发送给与调用kill进程同一进程组的所有进程;-1表示发送给有权限发送的系统中所有进程;<0表示取绝对值发送给对应进程组
  • sig:信号编号。尽量使用宏定义中的信号名。因为不同操作系统的信号值可能不一样,但是宏定义相同

返回值:0表示发送成功;-1表示发送失败,设置errno

权限保护:root用户可以发送信号给任意用户,普通用户是不能向系统用户和其他普通用户发送信号的

alarm函数

需要头文件#include

设置定时器,经过指定seconds后,内核会给当前进程发送14 SIGALRM信号。默认动作为终止进程

每个进程都有且只有一个定时器

原型:unsigned int alarm(unsigned int seconds);

参数:

  • seconds:定时秒数

返回值:返回剩余的秒数;0表示没有定时器再运行,没有失败情况

取消定时器:alarm(0)

time命令:time ./out查看程序执行时间

实际运行时间=用户运行时间+内核运行时间+等待时间。其中等待时间主要是等待IO设备阻塞的时间,所以优化程序第一步先优化IO

setitimer函数

设置定时器。可实现周期定时

原型:int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);

参数:

  • which:指定定时形式。取值ITIMER_REAL自然定时、ITIMER_VIRTUAL虚拟空间计时,计算进程占用CPU的时间、ITIMER_PROF运行时计时,计算占用CPU及执行程序调用的时间
  • new_val:传入参数,定时秒数。
  • old_value:传出参数,上次定时剩余时间

返回值:0表示设置成功;-1表示设置是吧,设置errno

itimerval类型:

struct itimerval{
    struct timeval{
        time_t tv_sec;			//秒
        suseconds_t tv_usec;	//微妙
    }it_interval;//两次定时任务之间间隔时间
    
    struct timeval{
        time_t tv_sec;			//秒
        suseconds_t tv_usec;	//微妙
    }it_value;//定时的时长
}

信号集操作函数

sigset_t set;自定义信号集

sigemptyset(sigset_t *set);清空信号集

sigfillset(sigset_t *set);全部置1

sigaddset(sigset_t *set, int signum);将一个信号添加到集合中

sigdelset(sigset_t *set, int signum);讲一个信号从集合中移除

sigismemeber(const sigset_t *set, int signum);判断一个信号是否在集合中。返回1表示在;0表示不在

sigprocmask函数

需要头文件#include

设置屏蔽字和解除屏蔽字

原型:sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:

  • how:设置方式。取值SIG_BLOCK设置阻塞表示mask = mask | set;SIG_UNBLOCK取消阻塞表示mask = mask & ~set;SIG_SETMASK替换set表示mask = set
  • set:传入参数,新的信号屏蔽集
  • oldset:传出参数,保存旧的信号屏蔽集

返回值:0表示执行成功;-1表示执行失败,设置errno

sigpending函数

需要头文件#include

读取当前进程的未决信号集

原型:int sigpending(sigset_t *set);

参数:

  • set:传出参数,保存未决信号集

返回值:0表示读取成功;-1表示读取失败,设置errno

信号捕捉

signal函数

需要头文件#include

注册一个信号捕捉函数

由ANSI定义,在不同的Unix、Linux版本上可能有不同的行为,应该尽量避免使用它

原型:typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);

参数:

  • signum:信号编号或宏名
  • handler:指向一个返回值为空,参数为int的函数指针

返回值:旧的信号捕捉函数

sigaction函数

需要头文件#include

修改信号处理动作。通常在Linux中注册一个信号的捕捉函数

和signal的区别:signal函数修改处理函数后在信号触发一次后就恢复为旧的处理函数,sigaction函数修改处理函数后,后续信号多次触发时执行的均为新的处理函数

原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:

  • signum:信号编号或宏名
  • act:传入参数,新的处理方式
  • oldact:传出参数,旧的处理方式

返回值:0表示注册成功;-1表示注册失败设置errno

sigaction类型:

struct sigaction
{
    void (*sa_handler)(int);//捕捉函数
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;//只在捕捉函数内生效,通常传全0(sigemptyset函数)
    int sa_flags;//参数,通常传0表示屏蔽本信号
    void (*sa_restorer)(void);
}

act传参构造sigaction结构体时只需要初始化sa_handler、sa_mask、sa_flags即可

信号特性:

  • 进程正常运行时,默认PCB中的信号屏蔽字mask决定了进程自动屏蔽哪些信号。当捕捉到已经注册了捕捉函数的某个信号时,在捕捉函数执行期间所屏蔽的信号不由mask决定,而是由sa_mask决定。在捕捉函数执行结束后,重新由mask决定屏蔽信号
  • 某个信号捕捉函数执行期间,该信号会自动被屏蔽(sa_flags=0时)
  • 阻塞的常规信号不支持排队,产生多次只记录一次。实时信号支持排队

内核实现信号捕捉过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lTTQvNH1-1664298269832)(https://gitee.com/AnonymYH/markdown-image/raw/master/img/image-20210815190618056.png)]

回调函数sighandler是被内核所调用的

SIGCHLD信号

产生条件:子进程状态发生改变,如子进程终止时、SIGSTOP信号停止时、子进程处于停止态被唤醒时

用SIGCHLD回收子进程:

void catch_child(int signo)
{
    pid_t wpid;
    wpid = wait(NULL);
    printf("catch child id %d\n", wpid);
    //修改为循环回收子进程
    /*
    while((wpid = wait(NULL)) != -1)
    {
    	printf("catch child id %d\n", wpid);
    }
    */
    return; 
}

int main()
{
    pid_t pid;
    //屏蔽信号
    /*
    sig_set set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);
    */
    int i;
    for(i = 0; i < 5; i++)
    {
        if((pid = fork()) == 0)
        {
            break;
        }
    }
    if(i == 5)
    {
        struct sigaction act;
        act.sa_handler(catch_child);
        sigemptyset(&act.as_mask);
        act.sa_flags = 0;
        sigaction(SIGCHLD, &act, NULL);//注册SIGCHLD信号
        //接触信号屏蔽
        /*
        sigprocmask(SIG_UNBLOCK, &set, NULL);
        */
        printf("parent, pid=%d\n",getpid());
    }
    else
    {
        printf("child, pid=%d\n", getpid());
    }
}

当有多个子进程同时发出SIGCHLD信号时,由于SIGCHLD是不排队的,父进程在处理上一个信号的时候,同时收到多个SIGCHLD信号,只会保留一个SIGCHLD,其余信号丢弃,导致这些被丢弃的信号对应的子进程无法被回收,成为僵尸进程。解决方法为在收到一个信号SIGCHLD时处理函数中回收所有已经结束的子进程(循环回收)

如果有子进程在父进程注册信号之前就结束,此时会进行默认处理动作,会出现捕捉不到的情况。解决方法为在fork之前设置SIGCHLD的屏蔽字,在注册信号后再解除屏蔽字阻塞

中断系统调用

慢速系统调用:可能使进程永远堵塞的系统调用。如果在阻塞期间收到一个信号,系统调用则被中断不再执行。如read、write、pause、wait

其他系统调用:getpid、getppid、fork

可修改sa_flags参数设置信号中断系统调用后是否重启。SA_RESTART重启、SA_INTERRUPT不重启

守护进程、线程

进程组和会话

进程组:又称作业,代表一个或者多个进程的集合。每个进程都属于一个进程组

会话:会话是一组相关进程组的集合

父进程创建子进程时,默认子进程和父进程属于同一进程组,进程组ID为第一个进程ID(组长进程),即父进程

组长进程可以创建一个进程组、已经该进程组中的进程、终止进程组中的进程。只要进程组中有一个进程存在,进程组就存在,与组长进程无关

创建会话需要注意:

  • 调用进程不能是进程组组长,否则出错返回。调用进程会变成新会话首进程和一个新进程组的组长进程
  • 需要有root权限
  • 新会话丢弃原有的控制终端,新会话没有控制终端
  • 建立新会话时,先fork,父进程终止,子进程调用setsid

getsid函数:pid_t getsid(pid_t pid);成功返回调用进程的会话id(SID);失败返回-1,设置errno。pid为0表示查看当前进程SID

setsid函数

创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID

原型:pid_t setsid(void);

返回值:返回调用进程的SID表示创建成功;-1表示创建失败,设置errno

守护进程

定义:是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字

Linux后台的一些系统服务进程,没有控制终端,不能和用户交互。不受用户登录、注销影响,一直运行。如预读入缓输出机制的实现、FTP服务器、NFS服务器等

创建守护进程:

  1. 创建子进程,父进程退出:所有工作在子进程中形式上脱离了控制终端
  2. 在子进程中创建新会话:setsid()函数。使子进程独立出来
  3. 改变当前目录位置:chdir()函数。防止占用可卸载的文件系统
  4. 重设文件权限掩码:umask()函数。防止继承的文件创建屏蔽字拒绝某些权限;增加守护进程灵活性
  5. 关闭文件描述符:继承的打开文件不会用到,浪费系统资源,无法卸载。关闭文件描述符0并且重定向文件描述符1、2至/dev/null
  6. 开始执行守护进程核心工作守护进程退出处理程序模型

线程

线程是轻量级的进程LWP,本质仍为进程

进程:独立的地址空间,拥有独立PCB

线程:有独立的PCB,但没有独立的地址空间(共享)

进程创建线程后自己变成线程,每个线程都有一个PCB,所有线程共享原进程的地址空间

线程是最小的执行单位进程是最小的资源单位,相当于只有一个线程的进程

ps -Lf pid命令:查询线程号,不是线程ID

线程号是CPU给线程分配时间片的标识根据,线程ID是进程用于标识内部线程

linux内核线程实现:内核中看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的,所以没有独立的地址空间

三级页表映射:页面 -> 页目录 -> 页表

线程中共享的资源:文件描述符表、信号处理方式、当前工作目录、用户ID和组ID、内存地址空间(.text、.data、.bss、heap、共享库)

线程中非共享资源:线程id、处理器现场和栈指针、独立的栈空间、errno变量、信号屏蔽字、调度优先级

以下函数在编译时需要加上选项-pthread

以下函数需要头文件#include

pthread_self函数

获取线程ID

原型:pthread_t pthread_self(void)

返回值:0表示获取成功

pthread_create函数

创建一个新线程

原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

参数:

  • thread:传出参数,新创建线程的线程ID
  • attr:线程属性。NULL表示默认属性
  • start_routine:回调函数,创建线程后执行
  • arg:回调函数的参数。采用值传递,利用类型强制转换

返回值:0表示创建成功;返回错误号表示创建失败

pthread_exit函数

将当前单个线程退出

exit函数退出的是进程,退出线程需要使用pthread_exit函数。使用return语句返回到调用者也能实现退出线程,但是只用于子线程,用在主线程上相当于退出进程

原型:void pthread_exit(void *retval);

参数:

  • retval:传出参数,退出状态,接收线程回调函数start_routine的退出值。通常传NULL

exit函数:退出当前进程

return语句:返回到调用者

pthread_exit():退出当前线程

pthread_join函数

阻塞等待线程退出,获取线程退出状态

原型:int pthread_join(pthread_t thread, void **retval);

参数:

  • thread:指定回收的线程变量
  • retval:传出参数,接收线程的退出状态。异常退出为-1

返回值:0表示退出成功;返回错误号表示退出失败

prthread_cancel函数

杀死线程

原型:int pthread_cancel(pthread_t thread);

参数:

  • pthread:待杀死的线程ID

返回值:0表示杀死成功;返回错误号表示杀死失败

该函数杀死线程必须有一个取消点,即一个进入内核态的时期。线程可以通过执行一个系统调用进入内核态,可以在线程中使用**pthread_testcancel()**手动添加一个取消点

被该函数杀死的线程如果用pthread_join函数回收的话,退出状态值为-1

pthread_detach函数

实现线程分离

线程分离状态:线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放

分离的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态,无需使用pthread_join回收

原型:int pthread_detach(pthread_t thread);

参数:

  • thread:待分离的线程ID

返回值:0表示分离成功;返回错误号表示分离失败

**线程中检查错误:**不能用perror函数,因为线程函数出错时直接返回错误号,所以直接用strerror函数解释函数返回值ret然后使用fprintf函数打印即可

线程属性

初始化线程属性:int pthread_attr_init(pthread_attr_t *attr);

销毁线程属性所占用的资源:int pthread_attr_destroy(pthread_attr_t *attr)

设置分离状态:int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);其中参数detachstate取值为PTHREAD_CREATE_DETACHED分离进程、PTHREAD_CREATE_JOINABLE非分离进程

pthread_join函数能够测试线程是否是分离的。如果是分离的,那回收失败,会返回错误号;如果不是分离的,则可以正常回收

线程使用注意:

  • malloc和mmap申请的内存可以被其他线程释放
  • 应避免在多线程模型中调用fork除非马上exec。子线程中只有调用fork的线程存在,其他线程均调用pthread_exit
  • 应避免在多线程模型中使用信号。当多线程模型收到一个信号时,原则上是谁先抢占接收到信号,谁处理。想要指定某一个线程处理时,可以为其他线程设置信号屏蔽字。线程之间不共享信号屏蔽字,共享未决信号集

线程同步

同步:协同步调,按预定的先后次序运行

线程同步:指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能

通过互斥量mutex来实现线程同步

互斥锁

互斥量是建议锁,不具备强制性,需要手动在程序逻辑中控制线程同步。只要有一个线程没有使用锁机制,就可能出现数据混乱

pthread_mutex_t结构体:互斥量类型

使用mutex步骤:

  1. pthread_mutex_t lock创建锁,全局变量
  2. pthread_mutex_init初始化锁
  3. pthread_mutex_lock加锁
  4. 访问共享数据
  5. pthread_mutex_unlock解锁
  6. pthread_mutex_destroy销毁锁

需要头文件#include

pthread_mutex_init函数

初始化互斥量

原型:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

**restrict关键字:**限定指针。被restrict限定的指针指向的内存操作只能由该指针完成

参数:

  • mutex:互斥量
  • attr:属性

返回值:0表示成功;返回错误号表示失败

使用pthread_mutex_init函数初始化为动态初始化,也可以使用宏进行静态初始化pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_destroy函数:int pthread_mutex_destroy(pthread_mutex_t *mutex);

pthread_mutex_lock函数:int pthread_mutex_lock(pthread_mutex_t *mutex);当锁被占用时,会阻塞线程

pthread_mutex_trylock函数:int pthread_mutex_trylock(pthread_mutex_t *mutex);锁被占用时,返回错误号EBUSY

pthread_mutex_unlock函数:int pthread_mutex_unlock(pthread_mutex_t *mutex);当锁解锁后,会唤醒阻塞的线程

互斥量mutex注意事项:

  • 尽量保证锁的粒度越小越好,即访问共享数据前加锁,访问结束后立即解锁
  • 互斥锁本质为结构体,但是工作原理可以看作是一个资源为1的整数。初始化时将mutex置为1,加锁时mutex–变为0,解锁时mutex++变为1

**死锁:**是不恰当地使用锁导致的现象。①一个线程对一把锁多次加锁;②线程1拥有A锁请求B锁,线程2拥有B锁请求A锁

读写锁

读写锁只有一把锁,读模式下加锁状态为读锁,写模式下加锁状态为写锁

特点:读共享写独占,写锁优先级高。当读写同时加锁时,优先让写模式加锁;当读写并行阻塞时,写模式排队在读前面

pthread_rwlock_t结构体:读写锁

应用函数:需要头文件#include

  • pthread_rwlock_init函数
  • pthread_rwlock_destroy函数
  • pthread_rwlock_rdlock函数
  • pthread_rwlock_wrlock函数
  • pthread_rwlock_tryrdlock函数
  • pthread_rwlock_trywrlock函数
  • pthread_rwlock_unlock函数

函数用法同互斥锁mutex

条件变量

条件变量本身不是锁,但是通常结合互斥锁mutex来使用

pthread_cond_t结构体:定义条件变量

pthread_condattr_t结构体:初始化属性

应用函数:需要头文件#include

  • pthread_cond_init函数,动态初始化
  • pthread_cond_destroy函数
  • pthread_cond_wait函数
  • pthread_cond_timedwait函数
  • pthread_cond_signal函数,一对一(至少一个)
  • pthread_cond_broadcast函数,一对多

初始化也可以使用宏pthread_cond_t cond = PTHREAD_COND_INITIALIZER;进行静态初始化条件变量

pthread_cond_wait函数

阻塞等待一个条件变量

作用:①阻塞等待条件变量cond满足;②阻塞等待时释放已掌握的互斥锁,相当于unlock了mutex;③条件满足被唤醒时,解除阻塞,重新加锁,即重新lock了mutex

原型:int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

参数:

  • cond:条件变量
  • mutex:互斥锁

返回值:0表示阻塞成功;返回错误号表示阻塞失败

生产者消费者模型——互斥锁mutex

主程序:

  1. 初始化互斥量、条件变量、资源池
  2. 分别创建生产者、消费者线程pthread_creaet()
  3. 阻塞等待生产者、消费者线程回收pthread_join()
struct mas
{
    int num;
    struct msg *next;
}
struct msg *head;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态初始化互斥量
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER;//静态初始化条件变量

int main()
{
    int ret;
    pthread_t pid, cid;
    ret = pthread_create(&tid, NULL, producer, NULL);
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create:%s\n", strerr(ret));
        exit(1);
    }
    ret = pthread_create(&cid, NULL, consumer, NULL);
    if(ret != 0)
    {
        fprintf(strerr, "pthread_create:%s\m", strerr(ret));
        exit(1);
    }
    pthread_join(pid, NULL);
    pthread_join(cid, NULL);
    return 0;
}

生产者:

  1. 生产数据
  2. 加锁pthread_mutex_lock(&mutex)
  3. 将数据放置到公共区域
  4. 解锁pthread_mutex_unlock(&mutex)
  5. 通知阻塞再条件变量上的线程pthread_cond_signal()pthread_cond_broadcast()
  6. 循环第一步
void *producer(void *p)
{
    struct msg *mg;
    for(;;)
    {
        mp = malloc(sizeof(struct msg));
        mp->num = rand() % 1000 + 1;
        printf("-Produce ------ %d\n", mp->num);
        pthread_mutex_lock(&lock);
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&lock);
        pthread_cond_signal(&has_product);
        sleep(rand() % 5);
    }   	
}

消费者:

  1. 加锁pthread_mutex_lock(&mutex)
  2. 判断公共区域是否有数据,若没有则调用pthread_cond_wait()进行阻塞等待
  3. 等到公共区域有数据后,满足条件时唤醒线程,消费一个产品
  4. 解锁pthread_mutex_unlock(&mutex)
  5. 循环第一步
void *consumer(void *p)
{
    struct msg *mp;
    for(;;)
    {
        pthread_mutex_lock(&lock);
        while(head == NULL)
        {
            pthread_cond_wait(&has_product, &lock);
        }
        mp = head;
        head = mp-next;
        pthread_mutex_unlock(&lock);
        printf("-Consume %lu---%d\m", pthread_self(), mp->num);
        free(mp);
        sleep(rand() % 5);
    }
}

信号量

相当于初始化值为N的互斥量(把互斥量理解为资源为1的锁)

允许同时有N个线程来访问共享资源

可用于线程同步和进程同步

sem_t结构体:信号量

应用函数:需要头文件#include

  • sem_init函数:int sem_init(sem_t *sem, int pshared, unsigned int value);参数pshared为0表示线程间同步,非0表示进程间同步;value指定同时访问的线程数N
  • sem_destroy函数
  • sem_wait函数:相当于lock,信号量自减。信号量为0时阻塞
  • sem_trywait函数:非阻塞
  • sem_timewait函数
  • sem_post函数:相当于unlock,信号量自增。信号量为N时阻塞

生产者消费者模型——信号量sem

主程序:

#define NUM 5
int queue[NUM];
sem_t blank_number, product_number;//空闲块数、生产资源数

int main()
{
    pthread_t pid, cid;
    sem_init(&blank_number, 0, NUM);
    sem_init(&product_number, 0, 0);
    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);
    pthread_join(&pid, NULL);
    pthread_join(&cid, NULL);
    sem_destroy(&blank_number);
    sem_destroy(&product_number);
    return 0;
}

生产者:

  1. 空格信号量减少sem_wait(&blank_number)
  2. 生产产品
  3. 产品信号量增加sem_post(&product_number)
  4. 循环第一步
void *producer(void *arg)
{
    int i = 0;
    while(1)
    {
        sem_wait(&blank_number);
        queue[i] = rand() % 1000 + 1;
        printf("----Produce---%d\n", queue[i]);
        sem_post(&product_number);
        i = (i+1) % NUM;
        sleep(rand()%1);
    }
    return NULL;
}

消费者:

  1. 产品信号量减少sem_wait(&product_number)
  2. 生产产品
  3. 空格信号量增加sem_post(&blank_number)
  4. 循环第一步
void *consumer(void *arg)
{
    int i = 0;
    while(1)
    {
        sem_wait(&product_number);
        printf("-Consumer---%d\n", queue[i]);
        queue[i] = 0;
        sem_post(&blank_number);
        i = (i+1) % NUM;
        sleep(rand() % 3);
    }
    return NULL;
}

你可能感兴趣的:(笔记总结,linux)