系统编程

一、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. }  

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值