一、gcc
1.1 gcc编译四步
1.2 gcc编译常用参数
-I 指定头文件所在目录位置(当头文件和源码不在同一目录下,gcc -I ./hellodir hello.c -o hello)
-E 只做预处理。生成预处理文件
-S 只做预处理,编译。
-c 只做预处理,编译,汇编。得到二进制文件
-g 编译时添加调试文件,用于gdb调试
-Wall 显示所有警告信息
-D 向程序中“动态”注册宏定义
-l 指定动态库库名
-L 指定动态库路径
1.3 静态库与动态库
静态库在文件中静态展开,所以有多少文件就展开多少次,非常吃内存,100M展开100次,就是1G,但是这样的好处就是静态加载的速度快。
使用动态库会将动态库加载到内存,10个文件也只需要加载一次,然后这些文件用到库的时候临时去加载,速度慢一些,但是很省内存。
动态库和静态库各有优劣,根据实际情况合理选用即可。
1.3.1 静态库的制作与使用
静态库名字以lib开头,以.a结尾
例如:libmylib.a
静态库生成指令
ar rcs libmylib.a file1.o
制作及使用步骤:
1、生成.o文件
gcc -c add.c -o add.o
2、使用ar工具制作静态库
ar rcs lib库名.a add.o
3、编译静态库到可执行文件中:(库名一定要在源码的后面)
gcc test.c lib库名.a -o a.out
1.3.2 动态库的制作
制作动态库的步骤
1、生成位置无关的.o文件
gcc -c add.c -o add.o -fPIC
使用这个参数过后,生成的函数就和位置无关,挂上@plt标识,等待动态绑定。
2、使用 gcc -shared制作动态库
gcc -shared -o lib库名.so add.o sub.o div.o
3、编译可执行程序时指定所使用的动态库。-l:指定库名 -L:指定库路径
gcc test.c -o a.out -l mymath -L ./lib
4、运行可执行程序
./a.out 出错!!!
出错原因分析:
连接器: 工作于链接阶段,工作时需要 -l 和 -L
动态链接器: 工作于程序运行阶段,工作时需要提供动态库所在目录位置
指定动态库路径并使其生效,然后再执行文件
通过环境变量指定动态库所在位置:export LD_LIBRARY_PATH=动态库路径
当关闭终端,再次执行a.out时,又报错。
这是因为,环境变量是进程的概念,关闭终端之后再打开,是两个进程,环境变量发生了变化。
要想永久生效,需要修改bash的配置文件:vi ~./bashrc。
修改后要使配置文件立即生效:. .bashrc 或者 source .bashrc 或者重开终端让其自己加载。
这下再执行a.out就不会报错了。
1.4 Makefile项目管理
命名:makefile Makefile — make 命令
1 个规则:
目标:依赖条件
(一个tab缩进)命令
a. 目标的时间必须晚于依赖条件的时间,否则,更新目标
b. 依赖条件如果不存在,找寻新的规则去产生依赖条件。
ALL:指定 makefile 的终极目标。
2 个函数:
src = $(wildcard ./*.c): 匹配当前工作目录下的所有.c 文件。将文件名组成列表,赋值给变量 src。 src = add.c sub.c div1.c
obj = $(patsubst %.c, %.o, $(src)): 将参数3中,包含参数1的部分,替换为参数2。 obj = add.o sub.o div1.o
clean: (没有依赖)
-rm -rf $(obj) a.out “-”:作用是,删除不存在文件时,不报错。顺序执行结束。
3 个自动变量:
$@: 在规则的命令中,表示规则中的目标。
$^: 在规则的命令中,表示所有依赖条件。
$<: 在规则的命令中,表示第一个依赖条件。如果将该变量应用在模式规则中,它可将依赖条件列表中的依赖依次取出,套用模式规则。
模式规则:
%.o:%.c
gcc -c $< -o %@
二、系统调用
什么是系统调用?由操作系统实现并提供给外部应用程序的接口。是应用程序同系统之间数据交互的桥梁。
2.1 open/close函数
2.1.1 open函数
manpage 第二卷,open函数如下,有两个版本的
open函数:
int open(char *pathname, int flags) #include <unistd.h>
参数:
pathname: 欲打开的文件路径名
flags:文件打开方式: #include <fcntl.h>
O_RDONLY|O_WRONLY|O_RDWR O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK ....
返回值:
成功: 打开文件所得到对应的 文件描述符(整数)
失败: -1, 设置errno
int open(char *pathname, int flags, mode_t mode) 123 775
参数:
pathname: 欲打开的文件路径名
flags:文件打开方式: O_RDONLY|O_WRONLY|O_RDWR O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK ....
mode: 参数3使用的前提, 参2指定了 O_CREAT。 取值8进制数,用来描述文件的 访问权限。 rwx 0664
创建文件最终权限 = mode & ~umask
返回值:
成功: 打开文件所得到对应的 文件描述符(整数)
失败: -1, 设置errno
open常见错误:
1.打开文件不存在
2.以写方式打开只读文件(权限问题)
3.以只写方式打开目录
当open出错时,程序会自动设置errno,可以通过strerror(errno)来查看报错数字的含义
以打开不存在文件为例:
2.2 read和write函数
2.2.1 read函数:
ssize_t read(int fd, void *buf, size_t count);
参数:
fd:文件描述符
buf:存数据的缓冲区
count:缓冲区大小
返回值:
0:读到文件末尾。
成功; > 0 读到的字节数。
失败: -1, 设置 errno
-1: 并且 errno = EAGIN 或 EWOULDBLOCK, 说明不是read失败,而是read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据。
2.2.2 write函数:
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd:文件描述符
buf:待写出数据的缓冲区
count:数据大小
返回值:
成功; 写入的字节数。
失败: -1, 设置 errno
三、进程相关
3.1 进程和程序
程序:死的。只占用磁盘空间。
进程:活的。运行起来的程序。占用内存、cpu等系统资源。
3.2 并发
并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但,任一时刻上仍只有一个进程在运行。
3.3 fork函数
pid_t fork(void)
创建子进程。父子进程各自返回。父进程返回子进程pid。 子进程返回 0.
下面是一个fork函数的例子,代码如下:
编译运行,如下:
关于这里为啥终端提示符和输出信息混在了一起,循环创建多个子进程(后面第二节)那一节会进行分析,现在先不用管。
fork之前的代码,父子进程都有,但是只有父进程执行了,子进程没有执行,fork之后的代码,父子进程都有机会执行。
3.4 get_pid和get_ppid函数
pid_t getpid() 获取当前进程id
pid_t getppid() 获取当前进程的父进程id
3.5 创建多个子进程
所以,直接用个for循环是要出事情的,因为子进程也会fork新的进程
这里,对调用fork的进程进行判定,只让父进程fork新的进程就行,代码如下:
编译执行,如图:
出现了问题:进程多了一个,而且不是按顺序来的。这里多出的一个,是父进程,因为父进程才有i=5跳出循环这一步。所以,对父进程进行判定并处理
修改代码如下:
编译运行,结果如下:
现在还有两个问题,
一个就是包括父进程在内的所有进程不是按顺序出现,多运行几次,发现是随机序列出现的。这是要因为,对操作系统而言,这几个子进程几乎是同时出现的,它们和父进程一起争夺cpu,谁抢到,谁打印,所以出现顺序是随机的。
第二问题就是终端提示符混在了输出里,这个是因为,loop_fork是终端的子进程,一旦loop_fork执行完,终端就会打印提示符。就像之前没有子进程的程序,一旦执行完,就出现了终端提示符。这里也就是这个道理,loop_fork执行完了,终端提示符出现,然而loop_fork的子进程还没执行完,所以输出就混在一起了。
下面通过sleep延时来解决父进程先结束这个问题。代码如下,就是给父进程加了个sleep:
3.6 父子进程的共享
父子进程:
刚fork后。 data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式相同。
进程id、返回值、各自的父进程、进程创建时间、闹钟、未决信号集不同。
父子进程共享:
读时共享、写时复制。———————— 全局变量。
1.文件描述符 2. mmap映射区。
3.7 exec函数族
3.7.1 exec函数族:
使进程执行某一程序。成功无返回值,失败返回 -1
int execlp(const char *file, const char *arg, ...); 借助 PATH 环境变量找寻待执行程序
参1: 程序名
参2: argv0
参3: argv1
...: argvN
哨兵:NULL
int execl(const char *path, const char *arg, ...); 自己指定待执行程序路径。
int execvp();
ps ajx --> pid ppid gid sid
3.7.2 execlp和ececl函数
int execlp(const char *file, const char *arg, …)
成功,无返回,失败返回-1
参数1:要加载的程序名字,该函数需要配合PATH环境变量来使用,当PATH所有目录搜素后没有参数1则返回出错。
该函数通常用来调用系统程序。如ls、date、cp、cat命令。
execlp这里面的p,表示要借助环境变量来加载可执行文件
可变参数那里,是从argv[0]开始计算的,示例代码,通过execlp让子进程去执行ls命令:
int execl(const char *path, const char *arg, …)
这里要注意,和execlp不同的是,第一个参数是路径,不是文件名。
这个路径用相对路径和绝对路径都行。
调用的代码如下:
用execl也能执行ls这些,把路径给出来就行,但是这样麻烦,所以对于系统指令一般还是用execlp
3.8 回收子进程
3.8.1 孤儿进程
父进程先于子进终止,子进程沦为“孤儿进程”,会被 init 进程领养。
3.8.2 僵尸进程
子进程终止,父进程尚未对子进程进行回收,在此期间,子进程为“僵尸进程”。 kill 对其无效。这里要注意,每个进程结束后都必然会经历僵尸态,时间长短的差别而已。
子进程终止时,子进程残留资源PCB存放于内核中,PCB记录了进程结束原因,进程回收就是回收PCB。回收僵尸进程,得kill它的父进程,让孤儿院去回收它。
3.8.3 wait回收子进程
wait函数: 回收子进程退出资源, 阻塞回收任意一个。
pid_t wait(int *status)
参数:(传出) 回收进程的状态。
返回值:成功: 回收进程的pid
失败: -1, errno
函数作用1: 阻塞等待子进程退出
函数作用2: 清理子进程残留在内核的 pcb 资源
函数作用3: 通过传出参数,得到子进程结束状态
获取子进程正常终止值:
WIFEXITED(status) --》 为真 --》调用 WEXITSTATUS(status) --》 得到 子进程 退出值。
获取导致子进程异常终止信号:
WIFSIGNALED(status) --》 为真 --》调用 WTERMSIG(status) --》 得到 导致子进程异常终止的信号编号。
3.8.4 waitpid回收子进程
waitpid函数: 指定某一个进程进行回收。可以设置非阻塞。
waitpid(-1, &status, 0) == wait(&status);
pid_t waitpid(pid_t pid, int *status, int options)
参数:
pid:指定回收某一个子进程pid
> 0: 待回收的子进程pid
-1:任意子进程
0:同组的子进程。
status:(传出) 回收进程的状态。
options:WNOHANG 指定回收方式为,非阻塞。
返回值:
> 0 : 表成功回收的子进程 pid
0 : 函数调用时, 参3 指定了WNOHANG, 并且,没有子进程结束。
-1: 失败。errno
一次wait/waitpid函数调用,只能回收一个子进程。
3.9 进程间通信常见方式
进程间通信的常用方式,特征:
管道:简单
信号:开销小
mmap映射:非血缘关系进程间
socket(本地套接字):稳定
3.9.1 管道
管道:
实现原理: 内核借助环形队列机制,使用内核缓冲区实现。
特质: 1. 伪文件
2. 管道中的数据只能一次读取。
3. 数据在管道中,只能单向流动。
局限性:1. 自己写,不能自己读。
2. 数据不可以反复读。
3. 半双工通信。
4. 血缘关系进程间可用。
pipe函数: 创建,并打开管道。
int pipe(int fd[2]);
参数: fd[0]: 读端。
fd[1]: 写端。
返回值: 成功: 0
失败: -1 errno
管道的读写行为:
读管道:
1. 管道有数据,read返回实际读到的字节数。
2. 管道无数据: 1)无写端,read返回0 (类似读到文件尾)
2)有写端,read阻塞等待。
写管道:
1. 无读端, 异常终止。 (SIGPIPE导致的)
2. 有读端: 1) 管道已满, 阻塞等待
2) 管道未满, 返回写出的字节个数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int ret;
int fd[2];
pid_t pid;
char *str = "hello pipe\n";
char buf[1024];
ret = pipe(fd);
if (ret == -1)
sys_err("pipe error");
pid = fork();
if (pid > 0) {
close(fd[0]); // 关闭读段
//sleep(3);
write(fd[1], str, strlen(str));
close(fd[1]);
} else if (pid == 0) {
close(fd[1]); // 子进程关闭写段
ret = read(fd[0], buf, sizeof(buf));
printf("child read ret = %d\n", ret);
write(STDOUT_FILENO, buf, ret);
close(fd[0]);
}
return 0;
}
3.9.2 父子进程lswc-l练习
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <string.h>
4. #include <unistd.h>
5. #include <errno.h>
6. #include <pthread.h>
7.
8. void sys_err(const char *str)
9. {
10. perror(str);
11. exit(1);
12. }
13. int main(int argc, char *argv[])
14. {
15. int fd[2];
16. int ret;
17. pid_t pid;
18.
19. ret = pipe(fd); // 父进程先创建一个管道,持有管道的读端和写端
20. if (ret == -1) {
21. sys_err("pipe error");
22. }
23.
24. pid = fork(); // 子进程同样持有管道的读和写端
25. if (pid == -1) {
26. sys_err("fork error");
27. }
28. else if (pid > 0) { // 父进程 读, 关闭写端
29. close(fd[1]);
30. dup2(fd[0], STDIN_FILENO); // 重定向 stdin 到 管道的 读端
31. execlp("wc", "wc", "-l", NULL); // 执行 wc -l 程序
32. sys_err("exclp wc error");
33. }
34. else if (pid == 0) {
35. close(fd[0]);
36. dup2(fd[1], STDOUT_FILENO); // 重定向 stdout 到 管道写端
37. execlp("ls", "ls", NULL); // 子进程执行 ls 命令
38. sys_err("exclp ls error");
39. }
40.
41. return 0;
42. }
3.9.3 FIFO命名管道
fifo管道:可以用于无血缘关系的进程间通信。
命名管道: mkfifo
无血缘关系进程间通信:
读端,open fifo O_RDONLY
写端,open fifo O_WRONLY
fifo操作起来像文件
下面的代码创建一个fifo:
fifo实现非血缘关系进程间通信
下面这个例子,一个写fifo,一个读fifo,操作起来就像文件一样的:
下面测试多个写管道,一个读管道,就是多开两个fifo.w,就一个fifo.r,这是可以的,懒得做了,就这样吧。
测试一个写端多个读端的时候,由于数据一旦被读走就没了,所以多个读端的并集才是写端的写入数据。
3.9.4 mmap函数
存储映射I/O(Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用read和write函数的情况下,使地址指针完成I/O操作。
使用这种方法,首先应该通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 创建共享内存映射
参数:
addr: 指定映射区的首地址。通常传NULL,表示让系统自动分配
length:共享内存映射区的大小。(<= 文件的实际大小)
prot: 共享内存映射区的读写属性。PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE
fd: 用于创建共享内存映射区的那个文件的 文件描述符。
offset:默认0,表示映射文件全部。偏移位置。需是 4k 的整数倍。
返回值:
成功:映射区的首地址。
失败:MAP_FAILED (void*(-1)), errno
flags里面的shared意思是修改会反映到磁盘上,private表示修改不反映到磁盘上。
int munmap(void *addr, size_t length); 释放映射区。
addr:mmap 的返回值
length:大小
3.9.5 信号
一、信号共性:
简单、不能携带大量信息、满足条件才发送。
二、信号的特质:
信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。
所有信号的产生及处理全部都是由【内核】完成的。
三、信号相关的概念:
产生信号:
1. 按键产生
2. 系统调用产生
3. 软件条件产生
4. 硬件异常产生
5. 命令产生
概念:
未决:产生与递达之间状态。
递达:产生并且送达到进程。直接被内核处理掉。
信号处理方式: 执行默认处理动作、忽略、捕捉(自定义)
阻塞信号集(信号屏蔽字): 本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
未决信号集:本质:位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理。
四、信号的四要素:
信号使用之前,应先确定其4要素,而后再用!!!
编号、名称、对应事件、默认处理动作。
五、Linux常规信号
之前我们说了信号的处理方式有:系统默认处理动作、忽略和捕捉。
在系统执行默认处理动作时又有五种方式:
- Term:终止进程
- Ign:忽略信号(默认即时对该种信号忽略操作)
- Core:终止进程,生成core文件(查验进程死亡原因,用于gdb调试)
- Stop:停止(暂停)进程
- Cont:继续运行进程
Linux中常见的信号如下:
1)SIGHUP:当用于退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程。
2)SIGINT:当用户按下了<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:另一个用户自定义信号,程序员可以在程序中定义并使用该信号,默认处理动作为终止进程。
13)SIGPIPE:Broken pipe向一个没有读端的管道写数据,默认处理动作为终止进程。
14)SIGALRM:定时器超时,超时的时间有系统调用alarm设置,默认处理动作为终止进程。
15 )SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止,通常用来表示程序正常退出。是不带参数的kill默认发送的信号,默认处理动作为终止进程。
16)SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容,默认处理动作为终止进程。
17)SIGCHLD:子进程状态发生变化时,父进程会收到这个信号,默认处理动作为忽略这个信号。
18)SIGCONT:如果进程已停止,信号使其继续运行,默认处理动作为继续/忽略这个信号。
19)SIGSTOP:停止进程的执行,信号不能被忽略、处理和阻塞,默认处理动作为暂停进程。
20)SIGTSTP:停止终端交互进程的运行,按下<Ctrl+z>组合键时发出这个信号,默认处理动作为暂停进程。
四、线程相关
4.1 线程概念
进程:有独立的 进程地址空间。有独立的pcb。 分配资源的最小单位。
线程:有独立的pcb。没有独立的进程地址空间。 最小的执行单位。
ps -Lf 进程id ---> 线程号。LWP --》cpu 执行的最小单位。
4.2 线程共享和非共享
独享: 栈空间(内核栈、用户栈)
共享: ./text./data ./rodataa ./bsss heap ---> 共享【全局变量】(errno)
4.3 线程控制原语
4.3.1 pthread_self和pthread_create
pthread_t pthread_self(void); 获取线程id。 线程id是在进程地址空间内部,用来标识线程身份的id号。
返回值:本线程id
检查出错返回: 线程中。
fprintf(stderr, "xxx error: %s\n", strerror(ret));
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void *), void *arg); 创建子线程。
参1:传出参数,表新创建的子线程 id
参2:线程属性。传NULL表使用默认属性。
参3:子线程回调函数。创建成功,ptherad_create函数返回时,该函数会被自动调用。
参4:参3的参数。没有的话,传NULL
返回值: 成功:0
失败:error
下面的例子,循环创建多个子线程:
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <string.h>
4. #include <unistd.h>
5. #include <errno.h>
6. #include <pthread.h>
7.
8. void sys_err(const char *str){
9. perror(str);
10. exit(1);
11. }
12.
13. void *tfn(void *arg){
14. int i = (int)arg;
15. sleep(i);
16. printf("--I'm %dth thread: pid = %d, tid = %lu\n",i+1, getpid(), pthread_self());
17.
18. return NULL;
19. }
20.
21. int main(int argc, char *argv[]){
22. int i;
23. int ret;
24. pthread_t tid;
25.
26. for(i=0;i<5;i++){
27. ret = pthread_create(&tid, NULL, tfn, (void *)i);
28. if (ret != 0) {
29. sys_err("pthread_create error");
30. }
31. }
32. sleep(i);
33. printf("I'm main, pid = %d, tid = %lu\n", getpid(), pthread_self());
34.
35. return 0;
36. }
编译运行,结果如下:
编译时会出现类型强转的警告,指针4字节转int的8字节,不过不存在精度损失,忽略就行。
在上面的代码中,如果将i取地址后再传入线程创建函数里,就是说当前传的是:(void *)i 改成: (void *)&i 相应的,修改回调函数:int i = *((int *)arg) 运行代码,会出现如下结果:
如果多次运行都只有主线程的输出,将主线程等待时长从i改为大于6的数即可。因为子线程等待时间i是不定的,但都小于等于6秒,由于抢cpu时没抢过主线程,导致没有子线程的输出。
错误原因在于,子线程如果用引用传递i,会去读取主线程里的i值,而主线程里的i是动态变化的,不固定。所以,应该采用值传递,不用引用传递。
直接看个代码,在子线程里更改全局变量,看主线程里的该变量有啥变化:
编译运行,结果如下:
可以看到,子线程里更改全局变量后,主线程里也跟着发生变化。
4.3.2 pthread_exit退出
void pthread_exit(void *retval); 退出当前线程。
retval:退出值。 无退出值时,NULL
exit(); 退出当前进程。
return: 返回到调用者那里去。
pthread_exit(): 退出当前线程。
4.3.3 pthread_join
int pthread_join(pthread_t thread, void **retval); 阻塞 回收线程。
thread: 待回收的线程id
retval:传出参数。 回收的那个线程的退出值。
线程异常借助,值为 -1。
返回值: 成功:0
失败:errno
4.3.4 pthread_cancel函数
int pthread_cancel(pthread_t thread); 杀死一个线程。 需要到达取消点(保存点)
thread: 待杀死的线程id
返回值: 成功:0
失败:errno
如果,子线程没有到达取消点, 那么 pthread_cancel 无效。
我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();
成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。
4.4 进程和线程控制原语对比
线程控制原语 进程控制原语
pthread_create() fork();
pthread_self() getpid();
pthread_exit() exit(); / return
pthread_join() wait()/waitpid()
pthread_cancel() kill()
pthread_detach()
4.5 线程同步
线程同步:
协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。
数据混乱的原因:
1. 资源共享(独享资源则不会)
2. 调度随机(意味着数据访问会出现竞争)
3. 线程间缺乏必要同步机制
锁的使用:
建议锁!对公共数据进行保护。所有线程【应该】在访问公共数据前先拿锁再访问。但,锁本身不具备强制性。
主要应用函数:
pthread_mutex_init 函数
pthread_mutex_destory 函数
pthread_mutex_lock 函数
pthread_mutex_trylock 函数
pthread_mutex_unlock 函数
以上5个函数的返回值都是:成功返回0,失败返回错误号
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待
pthread_mutex_t mutex;变量mutex只有两种取值:0,1
4.6 借助互斥锁实现线程同步
下面一个小例子,数据共享导致的混乱:
1. #include <stdio.h>
2. #include <string.h>
3. #include <pthread.h>
4. #include <stdlib.h>
5. #include <unistd.h>
6.
7. void *tfn(void *arg)
8. {
9. srand(time(NULL));
10.
11. while (1) {
12.
13. printf("hello ");
14. sleep(rand() % 3); /*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/
15. printf("world\n");
16. sleep(rand() % 3);
17. }
18.
19. return NULL;
20. }
21.
22. int main(void)
23. {
24. pthread_t tid;
25. srand(time(NULL));
26.
27. pthread_create(&tid, NULL, tfn, NULL);
28. while (1) {
29.
30. printf("HELLO ");
31. sleep(rand() % 3);
32. printf("WORLD\n");
33. sleep(rand() % 3);
34.
35. }
36. pthread_join(tid, NULL);
37.
38. return 0;
39. }
编译运行,结果如下:
如图,输出结果是主线程和子线程交叉的。
修改上面的代码,使用锁实现互斥访问共享区:
1. #include <stdio.h>
2. #include <string.h>
3. #include <pthread.h>
4. #include <stdlib.h>
5. #include <unistd.h>
6.
7. pthread_mutex_t mutex; // 定义一把互斥锁
8.
9. void *tfn(void *arg)
10. {
11. srand(time(NULL));
12.
13. while (1) {
14. pthread_mutex_lock(&mutex); // 加锁
15. printf("hello ");
16. sleep(rand() % 3); // 模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
17. printf("world\n");
18. pthread_mutex_unlock(&mutex); // 解锁
19. sleep(rand() % 3);
20. }
21.
22. return NULL;
23. }
24.
25. int main(void)
26. {
27. pthread_t tid;
28. srand(time(NULL));
29. int ret = pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
30. if(ret != 0){
31. fprintf(stderr, "mutex init error:%s\n", strerror(ret));
32. exit(1);
33. }
34.
35. pthread_create(&tid, NULL, tfn, NULL);
36. while (1) {
37. pthread_mutex_lock(&mutex); // 加锁
38. printf("HELLO ");
39. sleep(rand() % 3);
40. printf("WORLD\n");
41. pthread_mutex_unlock(&mutex); // 解锁
42. sleep(rand() % 3);
43. }
44. pthread_join(tid, NULL);
45.
46. pthread_mutex_destory(&mutex); // 销毁互斥锁
47.
48. return 0;
49. }
编译运行,结果如下:
可以看到,主线程和子线程在访问共享区时就没有交叉输出的情况了。
互斥锁使用注意事项:
尽量保证锁的粒度, 越小越好。(访问共享数据前,加锁。访问结束【立即】解锁。)
互斥锁,本质是结构体。 我们可以看成整数。 初值为 1。(pthread_mutex_init() 函数调用成功。)
加锁: --操作, 阻塞线程。
解锁: ++操作, 唤醒阻塞在锁上的线程。
try锁:尝试加锁,成功--。失败,返回。同时设置错误号 EBUSY
4.7 死锁
【死锁】:
是使用锁不恰当导致的现象:
1. 对一个锁反复lock。
2. 两个线程,各自持有一把锁,请求另一把。
4.8 读写锁
读写锁:
锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。
读共享,写独占。
写锁优先级高。
相较于互斥量而言,当读线程多的时候,提高访问效率
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
pthread_rwlock_rdlock(&rwlock); try
pthread_rwlock_wrlock(&rwlock); try
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);
一个读写锁的例子,核心还是读共享,写独占。写锁优先级高。
1. /* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */
2.
3. #include <stdio.h>
4. #include <unistd.h>
5. #include <pthread.h>
6.
7. int counter; //全局资源
8. pthread_rwlock_t rwlock;
9.
10. void *th_write(void *arg)
11. {
12. int t;
13. int i = (int)arg;
14.
15. while (1) {
16. t = counter; // 保存写之前的值
17. usleep(1000);
18.
19. pthread_rwlock_wrlock(&rwlock);
20. printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
21. pthread_rwlock_unlock(&rwlock);
22.
23. usleep(9000); // 给 r 锁提供机会
24. }
25. return NULL;
26. }
27.
28. void *th_read(void *arg)
29. {
30. int i = (int)arg;
31.
32. while (1) {
33. pthread_rwlock_rdlock(&rwlock);
34. printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
35. pthread_rwlock_unlock(&rwlock);
36.
37. usleep(2000); // 给写锁提供机会
38. }
39. return NULL;
40. }
41.
42. int main(void)
43. {
44. int i;
45. pthread_t tid[8];
46.
47. pthread_rwlock_init(&rwlock, NULL);
48.
49. for (i = 0; i < 3; i++)
50. pthread_create(&tid[i], NULL, th_write, (void *)i);
51.
52. for (i = 0; i < 5; i++)
53. pthread_create(&tid[i+3], NULL, th_read, (void *)i);
54.
55. for (i = 0; i < 8; i++)
56. pthread_join(tid[i], NULL);
57.
58. pthread_rwlock_destroy(&rwlock); //释放读写琐
59.
60. return 0;
61. }