文章目录
1. gcc编译
1.1 gcc编译四步骤

注:
- 编译阶段会将.i文件转换为汇编语言的文件
- 链接阶段会将生成的hello.o 以及头文件中包含的 .o文件一起链接,生成可执行文件
1.2 gcc常用参数

例:
-I : 指定头文件目录

-g:增加调试信息,可用gdb调试


-Wall:提示所有警告信息

-D:动态注册宏定义(常用于调试)

2. 库

2.1 静态库和动态库理论比对

静态库: 将库文件与源文件编译成一个可执行文件

动态库: 在使用库函数时,才去调用动态库加载函数

2.2 制作静态库


例:


2.3 静态库使用及头文件对应
(1)创建静态库中函数的声明

(2)添加静态库头文件

(3)输入所有警告信息并运行结果

2.4 动态库制作理论
在链接时,将源代码生成的二进制文件中使用动态库函数的进行位置替换,将源码暂存的位置替换为动态库函数的位置。

例: printf函数是在动态库内,源码生成的汇编文件在对动态库函数进行@plt标记。在链接阶段,通过符号链接找到动态库中的printf函数,进行替换。也就是地址回填。

生成与位置无关的代码:

2.5 动态库制作
(1)生成动态库

(2)运行调用动态库

报错原因:

(3)解决报错方法一:设置链接库环境变量

(4)其余解决方法,第三种不推荐

2.5 数据段合并–链接阶段

一个内存页大小为4k,为了减少空间的浪费,将数据段合并
.rodata和.text 合并为一页,ro(只读数据段)
.bss和 .data 合并为一页, rw(读写数据段)
3. gdb调试工具
3.1 基础指令

3.2 其他指令



随着函数调用而在stack上开辟的一片内存空间,用于存放函数调用时产生的局部变量和临时值。

4. makefile项目管理
用途:

4.1 基础规则

例:

若规则需要的依赖不存在,则查看其他规则是否能生成该依赖
例:规则中依赖的test.o本来不存在,另外的规则可以生成test.o规则

4.2 makefile一个规则

makefile会默认将第一组规则的目标设置为终极目标,完成终极目标则结束。可以使用all 来指定终极目标
例:

- 先编译.o 再 链接的目的是为了在修改某源码时,不会重新编译其他源码,而是直接将其他源码的.o 文件和修改后的文件编译生成的.o文件链接即可,降低了编译运行整个文件的速度(编译阶段最耗时)。
- makefile会根据规则中目标和依赖的修改时间来判断依赖是否修改,进而判断是否需要修改目标文件。
- 可用all指定终极目标。

4.3 两个函数


%格式:模式匹配符,用于定义通用规则时使用。
例如:%.c 代表任何以 .c 结尾的文件,并且可以与模式规则一起使用,以生成相应的目标文件。%.o : %.c 表示任何.o文件都可以由相应的.c文件生成。使用 %.c 时,make 会根据需要自动推导出依赖关系。
例:

obj = $(patsubst %.c, %.o, $(src))
图中的 obj = $(patsubst *.c, *.o, $(src))会出错
执行clean规则:

-n 参数表示显示要执行的命令
makefile中 rm前的 “ - ”表示错误依然执行
4.4 三个自动变量和模式规则
(1)

使用自动变量的原因:便于扩展

例:

测试可扩展性:添加一个乘法

输出结果:

(2)静态模式规则:

例:

(3) 伪目标:为了防止clean ,ALL等目标被当前目录中的同名文件夹影响,使用.PHONY将他们设置为伪目标

(4)参数:

例: make -f m(m是makefile的文件名)
4.5 实例
要求:
- src中保存所有.c文件
- inc中保存所有.h文件
- obj中保存所有.o文件
具体实现:
(1)mymath.h : 实现一个宏,包含头文件和声明

(2)test.c: 引入自定义头文件

(3)makefile

当匹配obj的路径时,若在.c文件前不加 ./src ,则% 匹配的是 ./src,那么后面的 ./obj/ %.o = ./obj/src/*.o 。而加了./src ,则 % .c= * .c。后面也是同理
结果:

4.6 实例2
将当前目录下的.c文件全部编译生成可执行文件
makefile:

5. 文件I/O
5.1 系统调用

5.2 open和close函数
(1)
系统调用中的open函数:(命令模式下输入 K 即可跳转)

参数:
pathname是文件路径,flags表示只读/只写/读写
mode表示文件的权限,在创建新文件的时候使用第二种open
返回值:返回文件描述符——文件打开表中该文件的索引号
(2)

umask是权限掩码,默认为022。而文件的默认权限为 777-022=755,目录的默认权限为666 -022 = 644。
mode &~umask 表示 mode与 ~umask作 位与运算。
注: “ | ” 在c语言中表示按位运算
(3)实例:

(4)常见错误


errno.h 中包含出错时的错误号errno
string.h 中包含streror函数,结果为错误号对应的具体错误



(5) open/close函数总结

5.3 read和write函数
(1)
read函数:

write函数:

(2)使用read和write函数实现复制功能
mycp.c:

makefile:

指定文件运行makefile:

结果:

错误处理:perror


5.4 系统调用和库函数的比较

使用fputc函数时:
buf每次读一个字节,传入程序缓冲区(蓝框),当程序缓冲区的字节数到达一定值(如4096),则将程序缓冲区的所有字节一并使用系统调用传入内核缓冲区
而使用系统调用,设置缓冲区大小为1时:
buf每次读一个字节后就换传入内核缓冲区,没有程序缓冲区的暂缓存。因此降低了程序读写的效率。因此直接使用系统调用并不一定比库函数效率高。
5.5 文件描述符

5.6 阻塞和非阻塞

5.7 fcntl函数

实例:

flags |= O_NONBLOCK;做位或运算,将位图中的O_NONBLOCK位置变为1

5.8 lseek函数

返回值 = 起始偏移量 + 偏移量
例1: 文件读写同一偏移位置:

第21行使用lseek将当前位置重新移到文件起始位置。 如果不使用lseek,则会导致后续代码读的时候的位置在文件末尾(由于前面写文件,将位置移到末尾)
例2:使用lseek获取文件大小

将偏移起始位置设置为文件末尾,lseek返回当前位置距离起始位置(文件起始位置。而非偏移起始位置)的偏移量
例3:使用lseek拓展文件大小

lseek设置偏移位置后必须有I/O操作才能实现文件大小变化。也可以使用truncate来改变文件大小

查看文件的十进制和十六进制:

5.9 传入传出参数

6. 文件操作
6.1 文件存储

(1)inode

(2)目录项dentry

6.2 stat函数 和 lstat函数
头文件 # include<sys/stat.h>
stat结构体内部成员:

st_mode相关函数:


stat和lstat的区别:stat会穿透符号链接,lstat不会
实例:

6.3 access函数

6.4 link和unlink函数
link函数: 给oldpath文件添加新的硬链接newpath。成功返回0,失败返回-.1

unlink函数: 删除文件的一个目录项。成功返回0,失败返回-1,并设置errno

unlink删除目录项会在进程结束后由操作系统择机删除

实例:利用link 和unlink实现MV操作.
myMV.c:

将当前目录的test.c移动到当前目录改名为testMV.c:

6.5 隐式回收

6.6readlink函数 和 rename函数

当读软链接时,输出为软链接指向的文件/目录的路径

7. 目录操作
7.1 getcwd和chdir函数

7.2 文件、目录权限
目录的内容是目录项

7.3 目录操作函数
DIR是目录流

dirent的具体格式: 常用inode和dname

readdir成功返回一个dirent指针,失败返回NULL,并设置errno。若读到底,则返回NULL,不设置errno。
实现ls:
myLS.c

结果:

7.4 递归遍历目录-实现ls-R
mylsr.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<string.h>
#include<dirent.h>
#include<unistd.h>
void recursion(char* path){
DIR* dp = opendir(path); // 打开目录,返回目录流
struct dirent *sdp; // 接收readdir的返回值
while(sdp=readdir(dp)){
if(sdp->d_name[0] == '.')
continue;
// 构建完整路径
char newpath[256];
snprintf(newpath, sizeof(newpath), "%s/%s", path, sdp->d_name);
// 使用stat判断是否为目录
struct stat buf; // 保存stat的结果
int ret = stat(newpath, &buf); // 使用拼接后的路径
if(ret == -1){
perror("stat error");
continue;
}
if(S_ISDIR(buf.st_mode)){ // 如果是目录,则继续递归
recursion(newpath);
}
printf("%s\t", sdp->d_name);
}
printf("\n");
closedir(dp); // 关闭目录
return;
}
int main(int argc, char* argv[]){ // argc 是参数的个数
recursion(argv[1]);
return 0;
}
运行结果

可能出错的地方:
-
未分配内存的指针:在recursion函数中,newpath是一个未初始化的指针,直接使用sprintf写入会导致段错误。
-
错误的stat调用:你在调用stat时使用了目录路径而不是完整的文件路径,这会导致无法正确获取文件信息。
-
路径拼接问题:在递归处理子目录时,路径拼接不正确。
7.5 dup和dup2

dup函数用于将复制一个文件描述符,两个文件描述符可以对同一个文件操作。
实例:将输出重定向到fd1指向的文件

总结:

dup2中newfd赋值为oldfd,即后一个文件重定向为前一个文件描述符
7.6 fcntl1实现dup

实例:

fd1 =3
fd2 =4
fd3 = 7
8. 进程
8.1 相关概念
(1)进程和程序

(2)虚拟内存和物理内存的映射关系

内核区的pcb映射到物理内存的同一块区域。那一块区域会保存多个pcb

(3)pcb

8.2 环境变量

例:

8.3 fork函数
创建子进程;

实例:创建子进程并打印pid
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc, char* argv[]){
printf("before fork-1\n");
printf("before fork-2\n");
printf("before fork-3\n");
printf("before fork-4\n");
// 创建子进程,在fork时,生成子进程
// 父进程返回子进程pid,子进程返回0
pid_t pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}
else if(pid == 0){ // 等于0表示为子进程
printf("子进程打印~~~~\n");
printf("子进程pid=%d,子进程的父进程pid=%d\n",getpid(), getppid());
}else if(pid > 0){ // 大于0时,表示其为父进程
printf("父进程打印~~~~\n");
printf("子进程id=%d,父进程id=%d,父进程的父进程id=%d\n",pid, getpid(),getppid());
sleep(1);
}
printf("================end file\n");
return 0;
}
运行结果:

- 在父进程fork时,会创建出子进程,父进程中的pid变量保存子进程的pid,而创建出的子进程中pid变量保存0。父进程可以打印代码中所有打印的,而子进程只能打印fork以后打印的。
- 在打印时父进程需sleep,否则,父进程先结束,则子进程的父进程pid会打印出1。
8.4 循环创建多个子进程
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc, char* argv[]){
printf("before fork-1\n");
printf("before fork-2\n");
printf("before fork-3\n");
printf("before fork-4\n");
int i = 0;
for(i = 0;i<5;i++){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}
else if(pid == 0){ // 等于0表示为子进程
printf("子进程%d打印~~~~\n", i + 1);
printf("子进程pid=%d,子进程的父进程pid=%d\n",getpid(), getppid());
break; // 防止子进程再创建子进程
}else if(pid > 0){ // 大于0时,表示其为父进程
printf("父进程打印~~~~\n");
printf("子进程id=%d,父进程id=%d,父进程的父进程id=%d\n",pid, getpid(),getppid());
sleep(1);
}
}
printf("================end file\n");
return 0;
}
运行结果:

8.5 父子进程共享

父子进程间遵循 “读时共享,写时复制” 的原则
即任一进程修改共享内容时还是会发生写时复制,就是为自己再创建一个副本

共享的内容:代码段,全局变量和静态变量,文件描述符和其他系统资源
不共享的内容:数据段(Data Segment)、堆(Heap)和栈(Stack)—这些在fork时,会产生副本,当时与父进程内容相同但不是同一个;以及进程不同的那些,如pid…
文件描述符存储在内核空间pcb中,会在fork时继承,但父子间的文件描述符关闭不互相影响
8.6 父子进程gdb调试

例:

8.7 exec函数族


(1)execlp函数:

实例:
exec_fork.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc, char* argv[]){
int i = 0;
pid_t pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}
else if(pid == 0){ // 等于0表示为子进程
execlp("ls","ls","-l","-h", NULL);
perror("execlp error"); // 只有execlp出错才执行
exit(1);
}
else if(pid > 0){ // 大于0时,表示其为父进程
printf("parentID:%d\n", getpid());
sleep(1);
}
printf("================end file\n");
return 0;
}
运行结果:

execlp执行系统命令,第一个参数是路径,会在$PATH下找;第二个参数是命令的参数,一般和参数一相同;后续是可变参数
若execlp执行成功,则会将其代码后续换成execlp需执行的命令,不会执行perror;若执行失败,则会执行后续代码,perror执行
参数末尾必须加上NULL
(2)execl函数: 传入可执行文件路径,名字和参数执行

注 : 第一个参数必须是可执行文件的路径,即把.c文件编译后的文件的路径
例:

执行结果:

execl 与execlp的区别是,execl需传入可执行文件的路径,而execl第一个参数传入的文件名,会默认在PATH环境变量下查找该可执行文件。后面的参数类似。
实例:将进程信息保存在文件中,使用dup2,execlp

(3)

(4) exec族一般规律

总结:

8.8 回收进程
(1)孤儿进程

(2)僵尸进程

若子进程变为僵尸进程,kill父进程后,子进程被init进程接管。init进程发现僵尸进程会自动清除
8.9 wait函数–回收子进程


判断status的宏函数:


实例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main(int argc, char* argv[]){
pid_t pid, wpid;
int status;
pid = fork();
if(pid == 0){ // 子进程
printf("--child, my id = %d, going to sleep\n", getpid());
sleep(10);
printf("-------child die\n");
exit(72);
}
else if(pid > 0){ //父进程
// wpid = wait(NULL); 不关心子进程结束状态的写法
wpid = wait(&status); // 如果子进程未终止,父进程会阻塞在这
if(wpid == -1){
perror("wait error");
exit(1);
}
if(WIFEXITED(status)){ // 判断进程是否正常结束
// 使用WEXITSTATUS获取子进程的退出状态(exit的参数)
printf("child exit with %d\n", WEXITSTATUS(status));
}
if(WIFSIGNALED(status)){ // 判断进程是否异常终止
// 使用WTERMSIG获取终止进程的信号编号(异常终止均为信号终止)
printf("child kill with %d\n", WTERMSIG(status));
}
}
return 0;
}
运行结果:
(1)

(2)

注: kill -数字 进程号 =》 表示使用不同的信号终止进程

总结:

8.10 waitpid – 回收子进程(指定进程)


返回值:

实例:回收第三个子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main(int argc, char* argv[]){
pid_t pid, tmppid, wpid;
int i = 0;
for(i = 0; i < 5; i++){
pid = fork();
if(pid == 0) { // 子进程
break;
}
if(i == 2) { // 指定第三个子进程
tmppid = pid; // 记录第三个子进程的pid
}
}
if(i == 5) { // 父进程执行
// sleep(5);
printf("I am a parent, my child id is %d\n", tmppid);
wpid = waitpid(tmppid, NULL, WNOHANG); // 设置非阻塞
// wpid = waitpid(tmppid, NULL, 0); // 设置阻塞,等同于wait
printf("wpid = %d\n", wpid);
}
else{ // 子进程执行
sleep(i);
printf("I am %d th child\n", i + 1);
}
return 0;
}
运行结果:
父进程设置sleep

父进程未设置sleep

实例2:回收多个子进程

总结:

注:wpid若设置了WNOHANG,则未回收子进程,不会改变status的值
8.11 进程间通信常见方式

内核空间的pcb存储在同一块物理块

8.12 管道通信(通过内核缓冲区通信)
(1)管道的特质

总结:

(2)管道的基本用法

fd[2]中保存使用管道的文件描述符
pipe函数的图示:创建匿名管道

pipe会创建管道,并保存读端和写端的文件描述符
实例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main(int argc, char* argv[]){
int fd[2], ret;
pid_t pid;
ret = pipe(fd); // 将两个描述符返回在fd中,fd[0]读,fd[1]写
if(ret < 0){
perror("pipe error");
exit(1);
}
char* str = "hello, pipe\n";
char buf[1024]; // 定义缓冲区
pid = fork(); // 创建子进程
// 父子进程共享文件描述符
if(pid > 0){ // 父进程
close(fd[0]); // 关闭读端
write(fd[1], str, strlen(str));
close(fd[1]);
}
else if(pid == 0) { // 子进程
close(fd[1]); // 关闭写端
// count保存实际接受的字节数
int count = read(fd[0], buf, sizeof(buf));
write(STDOUT_FILENO, buf, count); // 将读到的内容打印
close(fd[0]);
}
return 0;
}
运行结果:

总结:

(3)管道的读写行为

总结:

实例2:使用管道实现" ls | wc -l " =》 输出当前文件夹中的文件数
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main(int argc, char* argv[]){
int fd[2], ret;
pid_t pid;
ret = pipe(fd); // 创建管道
if(ret == -1){
perror("pipe error");
exit(1);
}
pid = fork(); // 创建子进程
if(pid == 0){ // 子进程执行ls命令
close(fd[0]); // 关闭读端
// 将标准输出重定向为管道的写端
dup2(fd[1], STDOUT_FILENO);
// 执行ls命令
execlp("ls", "ls", NULL);
perror("ls error");
exit(1);
}
else if(pid > 0){ // 父进程执行wc -l命令
close(fd[1]); // 关闭写端
// 将标准输入重定向为管道的读端(wc -l默认从标准输入读)
dup2(fd[0], STDIN_FILENO);
// 执行wc -l命令
execlp("wc", "wc", "-l", NULL);
perror("wc -l error");
exit(1);
}
return 0;
}
运行结果:

注: 父进程实现读操作,子进程实现写操作。否则,父进程会先结束,使子进程变为孤儿进程
**实例3:使用兄弟进程的管道实现" ls | wc -l ", 父进程回收子进程 **
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
int main(int argc, char* argv[]){
int fd[2], ret, i;
pid_t pid;
ret = pipe(fd); // 创建管道
if(ret == -1){
perror("pipe error");
exit(1);
}
for(i = 0;i < 2; i++){
pid = fork(); // 创建子进程
if(pid == 0)
break;
}
if(i == 0){ // 兄进程执行ls命令
close(fd[0]); // 关闭读端
// 将标准输出重定向为管道的写端
dup2(fd[1], STDOUT_FILENO);
// 执行ls命令
execlp("ls", "ls", NULL);
perror("ls error");
exit(1);
}
else if(i== 1){ // 弟进程执行wc -l命令
close(fd[1]); // 关闭写端
// 将标准输入重定向为管道的读端(wc -l默认从标准输入读)
dup2(fd[0], STDIN_FILENO);
// 执行wc -l命令
execlp("wc", "wc", "-l", NULL);
perror("wc -l error");
exit(1);
}
else if( i == 2){ // 父进程执行回收子进程
close(fd[0]);
close(fd[1]);
wait(NULL);
wait(NULL);
}
return 0;
}
运行结果:

注: 父进程需要关闭读写两端,保证管道是单向流动的
(4)管道缓冲区大小

(5)管道的优劣

8.13 命名管道FIFO


成功返回0, 失败返回-1; mode是权限设置
实例:创建一个命名管道

实例2: 利用fifo实现两个无血缘关系的进程间通信
写进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(int argc, char* argv[]){
int fd;
char buf[4096];
// 创建管道
int ret = mkfifo("testfifo", 0644);
if(ret == -1){
perror("mkfifo error");
exit(1);
}
fd = open("testfifo", O_WRONLY);
if(fd < 0){
perror("open error");
exit(1);
}
int i = 0;
while(1){
sprintf(buf, "hello, %d\n", i++); // 格式化输入buf
write(fd, buf, strlen(buf));
sleep(1);
}
return 0;
}
读进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
int main(int argc, char* argv[]){
int fd, len; // len保存读到的字节数
char buf[4096];
fd = open("testfifo", O_RDONLY);
if(fd < 0){
perror("open error");
exit(1);
}
while(1){
len = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
sleep(1);
}
return 0;
}
运行结果:

8.14 文件用于进程间通信(外存磁盘通信)

两个进程对同一个文件进行读写操作,实现通信

8.15 存储映射I/O — mmap

(1)mmap的函数原型


mmap的返回值为一个指向共享映射区的指针。具体的指针类型需根据取决于映射内存的用途或访问方式。例: 访问文本/字符串,使用char* ;访问二进制数据,使用int*/float* …
munmap函数:释放共享映射区

传入的参数分别为共享映射区的首地址和映射区大小
总结:

其中,flags的参数中, MAP_SHARED表示修改共享映射区中的内容会同时修改磁盘上的内容;而MAP_PRIVATE则不修改。
MAP_SHARED表示对映射的更新对其他相关进程可见。 映射此文件,并贯穿到基础文件中。
(2) mmap建立映射区
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/mman.h>
#include<string.h>
#include<fcntl.h>
int main(int argc, char* argv[]){
int fd, ret;
char* ptr = NULL; // 用于接收返回值,保存映射区地址
// 创建文件
fd = open("testmmap", O_RDWR |O_CREAT | O_TRUNC, 0664);
// 对文件进行扩容
ftruncate(fd, 20); // 也可以使用lseek扩容
int len = lseek(fd, 0, SEEK_END); // 使用lseek获取文件长度
printf("len = %d\n", len);
ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED){
perror("mmap error");
exit(1);
}
// 使用ptr对文件进行读写操作
strcpy(ptr, "hello, mmap");
printf("----------%s\n", ptr);
// 使用munmap释放共享映射区
ret = munmap(ptr, len);
if(ret == -1){
perror("munmap error");
exit(1);
}
return 0;
}
运行结果:

使用mmap创建共享映射区, 使用munmap释放映射区
(3)mmap的注意事项

注: 不同进程使用同一个文件建立mmap,是一份
(4)mmap的保险写法

(5)父子间通信–mmap

实例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/mman.h>
#include<string.h>
#include<fcntl.h>
// 全局变量,读时共享,写时复制
int var = 100;
int main(int argc, char* argv[]){
int fd, ret;
int* ptr; // 用于接收返回值,保存映射区地址
// 创建文件
fd = open("test", O_RDWR |O_CREAT | O_TRUNC, 0664);
// 对文件进行扩容
ftruncate(fd, 4); // 也可以使用lseek扩容
int len = lseek(fd, 0, SEEK_END); // 使用lseek获取文件长度
printf("len = %d\n", len);
// flag必须为MAP_SHARED,保证对映射区的操作多个进程间能共享
ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED){
perror("mmap error");
exit(1);
}
close(fd);
// 创建子进程
pid_t pid = fork();
if(pid <= -1){
perror("fork error");
exit(1);
}
else if(pid == 0){ // 子进程写映射区
*ptr = 2000;
var = 1000;
printf("child. *ptr = %d, var = %d\n", *ptr, var);
}
else if(pid > 0){ // 父进程读映射区
sleep(1);
printf("parent . *ptr = %d, var = %d\n", *ptr, var);
}
// 使用munmap释放共享映射区
ret = munmap(ptr, len);
if(ret == -1){
perror("munmap error");
exit(1);
}
return 0;
}
运行结果:

总结:

(6)非血缘关系通信
实例:
写进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/mman.h>
#include<string.h>
#include<fcntl.h>
typedef struct student{
int num;
char name[1024];
int age;
}student;
int main(int argc, char* argv[]){
int fd;
student* p = NULL;
student stu = {1, "xiaoming", 18};
fd = open("testwr", O_RDWR|O_CREAT|O_TRUNC, 0664);
if(fd == -1){
perror("open error");
exit(1);
}
// 对文件进行扩容
ftruncate(fd, sizeof(stu));
// 创建共享映射区
p = mmap(NULL, sizeof(stu), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){
perror("mmap error");
exit(1);
}
close(fd);
// 写映射空间
while(1){
// 将内存中一块区域复制到共享内存
memcpy(p, &stu, sizeof(stu));
stu.num++;
sleep(1);
}
return 0;
}
memcpy函数对内存进行操作

读进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/mman.h>
#include<string.h>
#include<fcntl.h>
typedef struct student{
int num;
char name[1024];
int age;
}student;
int main(int argc, char* argv[]){
int fd;
student* p = NULL;
student stu;
fd = open("testwr", O_RDWR|O_CREAT|O_TRUNC, 0664);
if(fd == -1){
perror("open error");
exit(1);
}
// 对文件进行扩容
ftruncate(fd, sizeof(stu));
// 创建共享映射区
p = mmap(NULL, sizeof(stu), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){
perror("mmap error");
exit(1);
}
close(fd);
// 读映射空间
while(1){
printf("num=%d, name=%s, age=%d\n", p->num, p->name, p->age);
sleep(1);
}
return 0;
}
运行结果:

当多个进程使用 mmap 映射同一个文件(并指定 MAP_SHARED 标志)时,它们最终会访问同一块物理内存(在不同进程的虚拟地址可能不同)
当指定MAP_PRIVATE时,遵循“读时共享,写时复制”
总结:

(7) 匿名映射 – 血缘关系进程间通信

实例:

总结:

9. 信号
9.1 信号的概念和机制
(1)概念

(2)机制

总结:

信号都是由内核产生,人为只能驱使内核产生和处理信号。
9.2 信号相关的事件和状态
(1)产生信号

(2)递达和未决

(3)信号的处理方式

(4)阻塞信号集和未决信号集

图示:

总结:

9.3 信号四要素和常规信号
(1)通过kill -l 查看信号,其中前31个为常规信号,有默认事件

32-64为实时信号,一般会捕捉使用
(2) 信号四要素

只有每个信号的事件发生后,信号才会递送,但不一定递达
默认处理动作:

(3)常见信号一览表


其中,9和19号默认处理动作不能被设置为忽略和捕捉,只能执行
后面的不常用,用的时候再查
9.4 kill命令和kill函数


实例:循环创建五个子进程,父进程使用kill函数终止任一子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
int main(int argc, char* argv[]){
pid_t childID[5], pid; // 保存子进程id
int i = 0;
for(i = 0; i < 5; i++){
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}
else if(pid == 0){
printf("child %d : id = %d\n", i + 1, getpid());
while(1){
sleep(1);
}
exit(0);
}
else{
childID[i] = pid; // 保存子进程的id
}
}
sleep(2); // 父进程等待一会儿,确保所有子进程都启动
int killNUM = 2; // 设置要杀死的子进程索引
printf("Parent is killing child %d (PID: %d)\n", killNUM + 1, childID[killNUM]);
int ret = kill(childID[killNUM], SIGKILL);
if(ret == -1){
perror("kill error");
exit(1);
}
sleep(5); // 保证子进程被完全杀死,SIGKILL 是异步的,操作系统需要时间处理
// 父进程等待所有进程退出
for(i = 0 ; i < 5; i++){
int status; // 设置状态
pid_t wpid = waitpid(childID[i], &status, WNOHANG);
if (wpid == -1) {
perror("waitpid error");
} else if (wpid > 0) {
if (WIFSIGNALED(status)) {
printf("Child %d (PID: %d) was killed by signal %d\n",
i+1, wpid, WTERMSIG(status));
} else {
printf("Child %d (PID: %d) exited normally\n", i+1, wpid);
}
} else {
printf("Child %d (PID: %d) is still running\n", i+1, childID[i]);
}
}
printf("All children have exited. Parent is terminating.\n");
return 0;
}
运行结果:

注:其他几个子进程并未结束和回收,需手动结束并回收
总结:

9.5 alarm函数


返回值为当前时间距上一个闹钟设置时间相差的秒数
实例:测试计算机一秒能写多少个数
#include<stdio.h>
#include<unistd.h>
int main(int argc, char* argv[]){
int i = 0;
alarm(1);
while(++i){
printf("i=%d\n", i);
}
return 0;
}
运行结果:使用time ./alarm运行

real - user - sys 的剩余时间用于等待,本次是等待屏幕I/O
总结:

用户时间:用户CPU时间是指程序在用户模式下执行时消耗的CPU时间,即执行应用程序代码时所花费的时间
内核时间:系统CPU时间是程序在内核模式下执行时消耗的CPU时间,即执行操作系统内核代码时所花费的时间。
9.6 setitimer函数

使用不同的计时方式,发送的信号不同

参数的结构体类型:

实例:周期5s发送hello,world,定时第一次为2s
#include<stdio.h>
#include<unistd.h>
#include<sys/time.h>
#include<signal.h>
void myfunc(int sigo){
printf("hello world\n");
}
int main(int argc, char* argv[]){
signal(SIGALRM, myfunc); // 捕捉信号
struct itimerval cur, old;
// 设置周期发信号间隔为5.0s
cur.it_interval.tv_sec = 5;
cur.it_interval.tv_usec = 0;
// 设置第一次信号发送时间为2.0s
cur.it_value.tv_sec = 2;
cur.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &cur, &old);
while(1);
return 0;
}
若间隔设置为0.0s, 则只会发送一次
总结:


9.7 信号集操作函数
(1)信号集设定

(2)sigprocmask函数


(3)sigpending函数


(4) 信号集操作函数使用原理

(5)实例:屏蔽信号2,即SIGINT=按键输入ctrl+c, 查看未决信号集
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>
void print_set(sigset_t* set){
int i;
for(i = 1; i <= 32; i++){
if(sigismember(set, i)) // 查看编号i的信号是否在信号集中
putchar('1');
else
putchar('0');
}
printf("\n");
return;
}
int main(int argc, char* argv[]){
sigset_t set, oldset, pedset; // 设置集合
int ret = 0;
sigemptyset(&set); // 清空集合
sigaddset(&set, SIGINT); // 将SIGINT=2信号添加进集合
// 将set与信号屏蔽集做位或,添加新的屏蔽,oldset接收原来的信号屏蔽
ret = sigprocmask(SIG_BLOCK, &set, &oldset);
if(ret == -1){
perror("sigprocmask error");
exit(1);
}
while(1){
// 查看未决信号集,pedset保存返回值
ret = sigpending(&pedset);
if(ret == -1){
perror("sigpending error");
exit(1);
}
// 打印查询到的未决信号集(前32位)
print_set(&pedset);
sleep(1);
}
return 0;
}
运行结果:打印前32个信号的未决信号集,在屏蔽信号2,并出现信号2后,未决信号集出现信号2

此时ctrl + c不能终止进程,发送信号2,使未决信号集添加信号2
屏蔽某信号后,未决信号集不会马上添加该信号,而是发送被屏蔽的信号后才添加(例如本例中的信号2)
总结:

9.8 signal实现信号捕捉

实质是函数让内核捕捉信号

定义了一个函数指针,命名位sighandler,作为传入参数和返回值
返回值为原来的函数指针,handler为新传入的函数指针
signum 作为handler函数的输入
函数名即可表示函数的地址,不需要&
实例:屏蔽信号2,SIGINT,即ctrl +c
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>
void print_catch(int signum){
printf("signum = %d\n", signum); // 打印屏蔽的信号
return;
}
int main(int argc, char* argv[]){
signal(SIGINT, print_catch);
while(1);
return 0;
}
运行结果:每ctrl+c一次,打印一次

9.9 sigaction实现信号捕捉


sigaction结构体:

sa_mask只在捕捉函数处理期间生效。此时捕捉期间会屏蔽mask | sa_mask的所有信号
实例:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>
void print_catch(int signum){
printf("catch you: %d\n", signum); // 打印屏蔽的信号
return;
}
int main(int argc, char* argv[]){
struct sigaction act, oldact;
act.sa_handler = print_catch; // 设置回调函数
sigemptyset(&act.sa_mask); // 将执行捕捉函数的屏蔽集清空,只在捕捉函数执行间生效
act.sa_flags = 0; // 0表示执行期间会屏蔽当前信号
int ret = sigaction(SIGINT, &act, &oldact);
if(ret == -1){
perror("sigaction error");
exit(1);
}
while(1);
return 0;
}
运行结果:

若信号再被捕捉前已经被屏蔽,则在屏蔽期间无法被捕捉
9.10 信号捕捉的特性

总结:

特性1 应改为 信号屏蔽字为 mask和sa_mask 的并集
特性3中后32个实时信号支持排队,前面的常规信号只保留一个
9.11 内核实现信号捕捉的过程

步骤1中时间片结束也会从用户态-》内核态(但是要等下次调度该进程才处理)
步骤4中处理完回调函数需返回调用者,借用一个特殊的系统调用sigreturn 返回内核空间
9.11 借助信号捕捉回收子进程
(1)SIGCHLD的产生条件

(2)实例:借助SIGCHLD回收子进程
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/wait.h>
void catch(int signum){
int status;
pid_t wpid;
// 循环wait,保证能清除僵尸进程
while((wpid = waitpid(-1, &status, 0)) != -1){
if(WIFEXITED(status)){
printf("子进程正常结束\n");
}
}
return;
}
int main(int argc, char* argv[]){
pid_t pid;
int i;
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD); // 添加阻塞
sigprocmask(SIG_BLOCK, &set, NULL);
// 创建15个子进程
for(i = 0 ; i < 15 ; i++){
pid = fork();
if(pid == 0)
break;
}
if(i == 15){ // 父进程,捕捉SIGCHLD信号,回收子进程
struct sigaction act;
// 初始化act
act.sa_handler = catch;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 捕捉SIGCHLD信号
sigaction(SIGCHLD, &act, NULL);
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("parent process : id = %d\n", getpid());
while(1){}
}
else{ // 子进程
sleep(2);
printf("child process: id = %d\n", getpid());
}
return 0;
}
运行结果:

问:若处理回收其中一个子进程时,其他子进程也死亡,但信号被屏蔽不能处理,怎么保证回收了所有子进程
答: 代码12行,使用while循环进行回收,保证回收当前子进程时,其他子进程死亡变为僵尸进程,本次会将所有僵尸进程回收。
问:若在注册捕捉函数前,子进程死亡,则不会被父进程wait回收
答:代码23-26行以及43行,分别对SIGCHLD信号进行阻塞和解除阻塞。保证在注册前死亡的子进程发送的SIGCHLD信号会至少有一个被阻塞,解除阻塞后对该信号进行捕捉。处理当前进程以及前面死亡的僵尸进程。
9.12 中断系统调用

10. 进程组和会话
10.1 概念和特性

会话是多个进程组的集合

10.2 会话
(1)创建会话

新会话无终端
(2)getsid函数

(3)setsid函数

例:

10.3 守护进程
(1)基本概念

(2)守护进程创建模型

总结:

改变工作目录的作用:防止目录被卸载,以及防止守护进程导致目录不能卸载
创建守护进程实例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
void sys_err(const char* str){
perror(str);
exit(1);
}
int main(int argc, char* argv[]){
pid_t pid;
int ret, fd;
pid = fork();
if(pid == -1)
sys_err("fork error");
else if(pid > 0)
exit(0); // 父进程直接终止
pid = setsid(); // 创建新会话
if(pid == -1)
sys_err("setsid error");
ret = chdir("/home/autumn"); // 改变工作目录
if(ret == -1)
sys_err("chdir error");
umask(022); // 重设文件权限掩码,文件权限=777-umask=755
close(STDIN_FILENO); // 关闭标准输入-0
fd = open("/dev/null", O_RDWR); // 打开空洞文件,fd=0
// 将标准输出和标准错误重定向为空洞文件,等同于关闭
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
while(1); // 模拟执行守护进程
return 0;
}
运行结果:


11. 线程
11.1 概念

图示:cpu执行处理线程

A进程中有三个线程,cpu会独立处理执行三个线程
总结:

线程号 ≠ 线程id
使用ps -LF + id查看线程号

3275号进程的LWP为其线程号,NLWP为线程个数
11.2 三级映射

三级页表指页目录,页表,页面,找到具体的页面

三级页表找物理地址

11.3 线程共享和非共享
(1)共享

虽然信号处理方式共享,但不推荐信号和线程混用
线程间全局变量(除了errno)共享,因为全部变量在.data中
文件描述符存储在pcb中,同一进程的线程共享pcb,因此关闭其中一个fd,会影响其他线程的pcb
(2)不共享

(3)线程优、缺点

11.4 创建线程—线程控制原语
(1)pthread_self函数

线程ID 是 lu 类型的
(2)pthead_create函数


实例:创建线程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void sys_err(const char* str){
perror(str);
exit(1);
}
void* tfunc(void *arg){ // 线程的回调函数
printf("thread: id = %lu\n", pthread_self());
return NULL;
}
int main(int argc, char* argv[]){
pthread_t tid;
// 创建线程
int ret = pthread_create(&tid, NULL, tfunc, NULL);
if(ret != 0){ // 若成功返回0,失败则返回errno
printf("pthread_create error");
exit(1);
}
printf("main thread: id = %ld\n", pthread_self());
sleep(1); // 保证主进程不会执行太快,导致线程未执行就结束
return 0;
}
运行结果:

编译运行时需添加 -pthread
总结:

11.5 循环创建多个子线程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void sys_err(const char* str){
perror(str);
exit(1);
}
void* tfunc(void *arg){ // 线程的回调函数
int i = (int)arg; // 强转为int
printf("I'm %dth thread:pid = %d tid = %lu\n", pthread_self());
return NULL;
}
int main(int argc, char* argv[]){
pthread_t tid;
int i = 0, ret;
// 创建多个线程
for(i = 0; i < 5 ; i++){
// 传i的值使用值传递,借助强转
ret = pthread_create(&tid, NULL, tfunc, (void*)(i+1));
if(ret != 0){ // 若成功返回0,失败则返回errno,不会设置errno
printf("pthread_create err");
exit(1);
}
}
printf("main thread:pid = %d id = %ld\n",getpid(), pthread_self());
sleep(1); // 保证主进程不会执行太快,导致线程未执行就结束
return 0;
}
运行结果:

问:为什么23行采用值传递,而不传指针
答:因为 i 是一个变量,传地址的话,子进程取 i 值时可能 i 已经修改过了
11.6 pthread_exit函数 – 线程退出

实例: 退出第三个线程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void sys_err(const char* str){
perror(str);
exit(1);
}
void* tfunc(void *arg){ // 线程的回调函数
int i = (int)arg; // 强转为int
if(i == 2){ // 第三个线程退出
// exit(0); // 退出进程
// return NULL; // 返回调用者--主线程
pthread_exit(NULL); // 退出线程,参数为void*类型
}
printf("I'm %dth thread:pid = %d tid = %lu\n", i + 1, getpid(), pthread_self());
return NULL;
}
int main(int argc, char* argv[]){
pthread_t tid;
int i = 0, ret;
// 创建多个线程
for(i = 0; i < 5 ; i++){
// 传i的值使用值传递,借助强转
ret = pthread_create(&tid, NULL, tfunc, (void*)i);
if(ret != 0){ // 若成功返回0,失败则返回errno,不会设置errno
printf("pthread_create err");
exit(1);
}
}
printf("main thread:pid = %d id = %ld\n",getpid(), pthread_self());
// sleep(1); // 保证主进程不会执行太快,导致线程未执行就结束
// 在主线程调用相当于退出主线程,但不影响其他线程,进程继续执行
pthread_exit(void*(0));
}
运行结果:

注:exit是退出当前进程; return是返回到函数的调用者;
pthread_exit是退出当前线程
总结:

11.7 pthread_join–线程回收(类似waitpid)


传出参数为指针的指针类型
实例:将结构体指针作为传入值传出并回收
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void sys_err(const char* str){
perror(str);
exit(1);
}
typedef struct thread_val{ // 设置一个结构体
int val;
char str[256];
}thread_val;
void* tfn(void* arg){ // 设置线程回调函数
thread_val* tval = malloc(sizeof(thread_val));
tval->val = 100;
strcpy(tval->str, "hello, world");
return tval; // 返回结构体指针
}
int main(int argc, char* argv[]){
pthread_t tid;
thread_val* retval;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if(ret != 0){
printf("pthread_create error: %s\n", strerror(ret));
exit(1);
}
// 回收void* 类型, 使用void**类型作为传出参数接收
ret = pthread_join(tid, &retval); // 回收线程
if(ret != 0){
printf("pthread_join error: %s\n", strerror(ret));
exit(1);
}
printf("chlid thread:val = %d, str = %s\n", retval->val, retval->str);
free(retval);; // 释放内存
pthread_exit(NULL);
}
运行结果:

总结:

11.8 pthread_cancel函数–杀死线程

默认线程只有在到达取消点时才会响应取消请求。
实例

实例2:对比三种回收方式
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void* tfn1(void* arg){ // return 返回
printf("thread 1 running\n");
return (void*)111;
}
void* tfn2(void* arg){ // pthread_exit终止线程
printf("thread 2 running\n");
pthread_exit((void*)222);
}
void *tfn3(void* arg){ // pthread_cancel杀死线程
while(1){
// 主动设置取消点,防止函数不会进入内核,导致取消线程不发生
pthread_testcancel();
}
return (void*)333;
}
int main(int argc, char* argv[]){
pthread_t tid;
void* tret = NULL;
// 创建多个线程
pthread_create(&tid, NULL, tfn1, NULL);
pthread_join(tid, &tret);
printf("thread 1 exit code = %d\n", (int)tret);
pthread_create(&tid, NULL, tfn2, NULL);
pthread_join(tid, &tret);
printf("thread 2 exit code = %d\n", (int)tret);
pthread_create(&tid, NULL, tfn3, NULL);
sleep(3);
pthread_cancel(tid); // 使用pthread_cancel杀死线程,无返回值
pthread_join(tid, &tret); // 回收失败
printf("thread 3 exit code = %d\n", (int)tret);
pthread_exit((void*)0);
}
运行结果:

注:如果pthread_join回收的线程被pthread_cancel杀死, 则其传出参数的值会设置为PTHREAD_CANCELED == (void*)-1,因此这次线程3输出的值为-1
注2:pthread_cancel进入内核才能杀死线程,因此如果线程内一直未进入内核,则无法杀死该线程。可以设置pthread_testcancel(),判断是否出现pthread_cancel,保证杀死线程的触发。
总结:

11.9 pthread_detach–线程分离

实例:

运行结果:

fprintf 是 C 语言的标准格式化输出函数,可以向指定的文件流(如 stdout、stderr 或文件)写入格式化数据。
stderr(标准错误流)是默认的输出错误信息的流,与 stdout(标准输出流)不同,它通常不会被缓冲,能立即显示错误信息(即使程序崩溃或重定向 stdout)。
总结:

分离后的线程执行完回自动回收清理,不需要单独的pthread_join清理
11.10 线程控制原语与进程控制原语对比

11.11 线程属性
(1)线程属性结构体

(2)线程属性初始化

(3)设置分离–创建时设置线程属性

实例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void* tfn(void* arg){
printf("thread:tid = %lu\n", pthread_self());
}
int main(int argc, char* argv[]){
pthread_attr_t attr; // 设置属性
pthread_t tid;
int ret;
ret = pthread_attr_init(&attr); // 初始化属性
if(ret != 0){
fprintf(stderr, "pthread_attr_init error:%s\n", strerror(ret));
exit(1);
}
ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置脱离
if(ret != 0){
fprintf(stderr, "pthread_attr_setdetachstate error:%s\n", strerror(ret));
exit(1);
}
ret = pthread_create(&tid, &attr, tfn, NULL);
if(ret != 0){
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}
ret = pthread_attr_destroy(&attr); // 销毁属性
if(ret != 0){
fprintf(stderr, "pthread_attr_destroy error:%s\n", strerror(ret));
exit(1);
}
sleep(1);
printf("main thread: tid = %lu\n", pthread_self());
// 使用pthread_join回收线程,判断线程是否已经成功分离
ret = pthread_join(tid, NULL);
if(ret != 0){
fprintf(stderr, "pthread_join error:%s\n", strerror(ret));
exit(1);
}
return 0;
}
运行结果:参数错误说明已经成功分离

总结:

11.12 线程注意事项

12 同步与互斥
12.1 线程同步

互斥量: 不具备强制性,建议锁

总结:

12.2 互斥锁的使用
(1)主要函数

(2)图示

(3)实例:主子线程分别完整大小写的hello,world
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>
pthread_mutex_t mutex; // 设置一把互斥锁(全局)
void* tfunc(void *arg){ // 线程的回调函数
srand(time(NULL));
int ret;
while(1){
ret = pthread_mutex_lock(&mutex); // 加锁
if(ret != 0){ // 若成功返回0,失败则返回errno
fprintf(stderr,"pthread_mutex_lock error:%s\n", strerror(ret));
exit(1);
}
printf("hello ");
sleep(rand() % 3); // 模拟长时间操作共享资源,导致cpu易主
printf("world\n"); // printf默认行缓冲,需要\n刷新缓冲区
ret = pthread_mutex_unlock(&mutex); // 解锁
if(ret != 0){ // 若成功返回0,失败则返回errno
fprintf(stderr,"pthread_mutex_unlock error:%s\n", strerror(ret));
exit(1);
}
sleep(rand() % 3);
}
return NULL;
}
int main(int argc, char* argv[]){
pthread_t tid;
srand(time(NULL));
int ret = pthread_mutex_init(&mutex, NULL); // 初始化锁
if(ret != 0){ // 若成功返回0,失败则返回errno
fprintf(stderr,"pthread_mutex_init error:%s\n", strerror(ret));
exit(1);
}
// 创建线程
ret = pthread_create(&tid, NULL, tfunc, NULL);
if(ret != 0){ // 若成功返回0,失败则返回errno
fprintf(stderr,"pthread_create error:%s\n", strerror(ret));
exit(1);
}
while(1){
ret = pthread_mutex_lock(&mutex); // 加锁
if(ret != 0){ // 若成功返回0,失败则返回errno
fprintf(stderr,"pthread_mutex_lock error:%s\n", strerror(ret));
exit(1);
}
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n"); // printf默认行缓冲,需要\n刷新缓冲区
ret = pthread_mutex_unlock(&mutex); // 解锁
if(ret != 0){ // 若成功返回0,失败则返回errno
fprintf(stderr,"pthread_mutex_unlock error:%s\n", strerror(ret));
exit(1);
}
sleep(rand() % 3);
}
pthread_join(tid, NULL);
ret = pthread_mutex_destroy(&mutex); // 毁灭锁
if(ret != 0){ // 若成功返回0,失败则返回errno
fprintf(stderr,"pthread_mutex_destroy error:%s\n", strerror(ret));
exit(1);
}
return 0;
}
运行结果:

对标准输出加锁
总结:

初始化有动态和静态初始化两种
12.3 死锁


图示:

12.4 读写锁

当写优先的情况下,如果读进程和写进程同时排队,无论当前是写进程还是读进程持有锁,持有锁的进程结束,都会是写进程先获得锁。(为了 防止写进程饥饿)
总结:

主要使用的函数:

与互斥锁类似
实例:写优先读写锁
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
int counter = 0; // 全局变量,作为共享资源
pthread_rwlock_t rwlock; // 读写锁
void* tfn_w(void* arg){
int t, i = (int)arg;
int ret;
while(1){
ret = pthread_rwlock_wrlock(&rwlock); // 加写锁
if(ret != 0){
fprintf(stderr, "pthread_rwlock_wrlock error:%s\n", strerror(ret));
exit(1);
}
t = counter;
usleep(1000);
printf("=========write %d : %lu: counter = %d,++counter = %d\n", i, pthread_self(), t, ++counter);
ret = pthread_rwlock_unlock(&rwlock); // 解锁
if(ret != 0){
fprintf(stderr, "pthread_rwlock_unlock error:%s\n", strerror(ret));
exit(1);
}
usleep(5000);
}
return NULL;
}
void* tfn_r(void* arg){
int i = (int)arg;
int ret;
printf("============\n");
while(1){
ret = pthread_rwlock_rdlock(&rwlock); // 加读锁
if(ret != 0){
fprintf(stderr, "pthread_rwlock_rdlock error:%s\n", strerror(ret));
exit(1);
}
printf("-----------------read %d : %lu: counter = %d\n", i, pthread_self(), counter);
ret = pthread_rwlock_unlock(&rwlock); // 解锁
if(ret != 0){
fprintf(stderr, "pthread_rwlock_unlock error:%s\n", strerror(ret));
exit(1);
}
usleep(2000);
}
return NULL;
}
int main(void){
pthread_t tid[8]; // 创建8个线程
int i, ret;
ret = pthread_rwlock_init(&rwlock, NULL); // 初始化锁
if(ret != 0){
fprintf(stderr, "pthread_rwlock_init error:%s\n", strerror(ret));
exit(1);
}
for(i = 0; i < 3; i++){ // 创建写线程
ret = pthread_create(&tid[i], NULL, tfn_w, (void*)i);
if(ret != 0){
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}
}
for(i = 3; i < 8; i++){
ret = pthread_create(&tid[i], NULL, tfn_r, (void*)i);
if(ret != 0){
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}
}
for(i = 0; i < 8; i++){
ret = pthread_join(tid[i], NULL);
if(ret != 0){
fprintf(stderr, "pthread_join error:%s\n", strerror(ret));
exit(1);
}
}
ret = pthread_rwlock_destroy(&rwlock); // 破坏锁
if(ret != 0){
fprintf(stderr, "pthread_rwlock_destroy error:%s\n", strerror(ret));
exit(1);
}
return 0;
}
运行结果:

12.5 条件变量

主要函数


12.6 条件变量------wait函数

图示:

该函数会使线程陷入阻塞,并解锁。等条件满足,再上锁,执行
该函数的前提是先上锁,再判断是否满足条件
12.7 生产者-消费者模型
(1)图示

(2)实例
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void sys_err(const char* str, const int ret){
fprintf(stderr, "%s error:%s\n", str, strerror(ret));
exit(1);
}
typedef struct msg{ // 使用一个链表的结点作为条件变量
struct msg* next;
int num;
}msg;
// 静态初始化 一个条件变量和一个互斥锁
pthread_cond_t has_produce = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 设置一个指针,指向链表
msg* head = NULL;
// 消费者
void* consumer(void* arg){
msg* mp;
while(1){
pthread_mutex_lock(&mutex); // 加锁
// 条件等待
while(head == NULL){
pthread_cond_wait(&has_produce, &mutex); // 阻塞并释放锁
}
// 解除阻塞
mp = head;
head = mp->next;
pthread_mutex_unlock(&mutex);
printf("consumer: %lu,-----consume:%d\n", pthread_self(), mp->num);
free(mp);
sleep(rand() % 3);
}
return NULL;
}
// 生产者
void* producer(void* arg){
msg* mp;
while(1){
mp = malloc(sizeof(msg));
mp->num = rand() % 100;
printf("producer: %lu,-------produce:%d\n", pthread_self(), mp->num);
pthread_mutex_lock(&mutex); // 访问临界区,加锁
mp->next = head;
head = mp;
pthread_mutex_unlock(&mutex); // 访问完立即解锁
pthread_cond_signal(&has_produce); // 通知唤醒线程
sleep(rand() % 3);
}
return NULL;
}
int main(){
pthread_t pid, cid; // 生产者和消费者的线程id
int ret;
// 创建生产者线程
ret = pthread_create(&pid, NULL, producer, NULL);
if(ret != 0)
sys_err("pthread_create", ret);
// 创建消费者线程
ret = pthread_create(&cid, NULL, consumer, NULL);
if(ret != 0)
sys_err("pthread_create", ret);
// 回收线程
ret = pthread_join(pid, NULL);
if(ret != 0)
sys_err("pthread_join", ret);
ret = pthread_join(cid, NULL);
if(ret != 0)
sys_err("pthread_join", ret);
ret = pthread_mutex_destroy(&mutex); // 销毁锁
if(ret != 0)
sys_err("pthread_mutex_destroy", ret);
pthread_cond_destroy(&has_produce);
return 0;
}
运行结果:

总结:

12.8 信号量–PV操作

(1)主要函数

总结:

(2)图示

(3)实例:使用信号量实现生产者–消费者模型
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
sem_t product, empty; // 设置产品和空位的信号量
#define num 5 // 设置一个宏,保存空位的数量
int queue[num]; // 创建一个队列,为共享区
void* producer(void* arg){
int i = 0;
while(1){
sem_wait(&empty);// P操作,有空位时生产产品,无空位时阻塞
queue[i] = rand() % 1000; // 操作共享区
sem_post(&product); // V操作,生产出产品
printf("producer------- produce: %d\n", queue[i]);
i = (i + 1 ) % num;
sleep(rand() % 2);
}
return NULL;
}
void* consumer(void* arg){
int i = 0;
while(1){
sem_wait(&product);
printf("consumer------- consume: %d\n", queue[i]);
sem_post(&empty); // V操作
i = (i + 1 ) % num;
sleep(rand() % 2);
}
return NULL;
}
int main(void){
pthread_t pid, cid; // 设置生产者和消费者线程号
// 初始化信号量
sem_init(&product, 1, 0); // 参2 = 1 用于进程间同步
sem_init(&empty, 1, num);
// 创建线程
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
// 回收线程
pthread_join(pid, NULL);
pthread_join(cid, NULL);
// 销毁信号量
sem_destroy(&product);
sem_destroy(&empty);
return 0;
}
运行结果:

这里没有对共享资源在操作时上锁。在多消费者或多生产者模型下,可能会有问题。因此需要对共享资源上互斥锁。

1414

被折叠的 条评论
为什么被折叠?



