
Linux系统编程-信号与多线程
1- 信号的概念
1.1 信号概念与机制
信号共性:
简单、不能携带大量信息、满足条件才发送。
信号的特质:
信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。
所有信号的产生及处理全部都是由【内核】完成的。
1.2 信号相关概念
产生信号
按键产生:如
Ctrl + c
、Ctrl + z
、Ctrl + \
系统调用产生:如
kill
、raise
、abort
软件条件产生:如 定时器
alarm
(sleep)硬件异常产生:如 非法访问内存( 段错误 )、除0 ( 浮点数除外 )、内存对齐出错 ( 总线错误 )
命令产生:如
kill
命令
递达
信号递送并且到达进程
未决
产生到递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
信号的处理方式
执行默认处理动作
忽略(丢弃)
捕捉(调用用户函数进行处理)
阻塞信号集(信号屏蔽字)
将某些信号加入集合,对他们设置屏蔽,当屏蔽 X 信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
本质 ---- 位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决状态。
未决信号集
信号产生,未决信号集中描述该信号的位立刻反转位为 1 , 表示信号处于未决状态。当信号被处理对应位翻转回 0 。这一时刻往往非常短暂。
信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。再屏蔽解除前,信号一直处于未决状态。
本质 ---- 位图。用来记录信号的处理状态。该信号集中的信号,表示已经产生,但尚未被处理
1.3 信号四要素和常规信号
kill -l
:查看当前系统中的常规信号
1 - 31 是常规信号,每个信号都有对应的默认事件和处理动作
34 - 64 是实时信号,没有对应的默认事件和处理动作
信号四要素:
编号
名称
对应时间
默认处理动作
信号使用之前,应先确定其四要素,然后再使用!!!
2- 信号的产生
2.1 kill
函数和kill
命令
2.1.1 函数原型
用来给一个程序发送信号。
#include <signal.h>
int kill(pid_t pid, int signum)
2.1.2 参数
pid
:0
:发送信号给指定进程= 0
:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。< -1
: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员。= -1
:发送信号给,有权限发送的所有进程。
signum
:待发送的信号
返回值
成功:
0
失败:
-1
,errono
2.1.3 示例
子进程发送信号kill父进程
// 子进程发送信号kill父进程
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/wait.h>
#include <signal.h> // 信号头文件
void sys_err(const char *str) // 错误处理函数
{
perror(str);
exit(1);
}
int main(int argc, char* argv[])
{
pid_t pid = fork();
// father
if(pid > 0)
{
printf("parent, pid = %d \n", getpid());
printf("waiting... \n");
while(1)
{
printf("father happy\n");
sleep(1);
}
}
// son
else if( pid == 0)
{
printf("child pid = %d, ppid = %d \n", getpid(), getppid());
sleep(5);
kill(getppid(), SIGKILL);
}
return 0;
}
kill -9 -groupname
杀一个进程组
2.2 alarm
函数
每个进程都有唯一的定时器
2.2.1 函数原型
使用自然计时法。定时发送SIGALRM
给当前进程。精度:秒
unsigned int alarm(unsigned int seconds);
参数
seconds
:定时的秒数
返回值
返回上次定时剩余的秒数
特例:alarm(0)
; 取消闹钟。
2.2.2 time
命令
查看程序执行时间;
例如:time ./a.out
,执行程序并且使用time
命令查看程序执行时间
结果: 实际时间 = 用户时间 + 内核时间 + 等待时间
2.2.3 示例
使用alarm函数计时,打印变量i的值。
// 使用alarm函数计时,打印变量i的值。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/wait.h>
#include <signal.h> // 信号头文件
void sys_err(const char *str) // 错误处理函数
{
perror(str);
exit(1);
}
int main(int argc, char* argv[])
{
alarm(1);
for(int i = 0; ; i++)
{
printf("%d\n",i);
}
return 0;
}
2.3 setitimer
函数
设置闹钟,可以替代alarm
函数,精度微秒us
,可以实现周期定时
2.3.1 函数原型
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数
which
:ITIMER_REAL
: 采用自然计时。 ——>SIGALRM
计算自然时间ITIMER_VIRTUAL
: 采用用户空间计时 --->SIGVTALRM
只计算进程占用 CPU 的时间ITIMER_PROF
: 采用内核+用户空间计时 --->SIGPROF
计算占用CPU及执行系统调用的时间
new_value
:定时秒数old_value
:传出参数,上次定时剩余时间。
返回值
成功:
0
失败:
-1
,errno
其中的 itimerval
结构体
/* 结构体嵌套 */
struct itimerval
{
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
}
struct timeval
{
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
}
// it_interval; ---> 用于设定两个定时任务之间的间隔时间
// it_value; ---> 第一次定时秒数
可以理解为有2个定时器
一个用于第一个闹钟什么时候触发打印
一个用于之后间隔多少时间再次触发闹钟。
2.3.2 示例
使用setitimer
定时,向屏幕打印信息
程序启动后2秒,开始打印,然后每隔5秒打印一次
// 使用setitimer定时,向屏幕打印信息
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
void sys_err(const char *str) // 错误处理函数
{
perror(str);
exit(1);
}
void myfun(int signo)
{
printf("hello my dear! \n");
}
int main(void)
{
struct itimerval it, oldit;
signal(SIGALRM, myfun); // 注册 SIGALRM 信号的捕捉处理函数
// 相当于第一次定时
it.it_value.tv_sec = 2; // 秒
it.it_value.tv_usec = 0; // 微秒
// 相当于周期定时
it.it_interval.tv_sec = 5; // 秒
it.it_interval.tv_usec = 0; // 微秒
if(setitimer(ITIMER_REAL, &it, &oldit) == -1)
{
sys_err("setitimer error");
}
while(1);
return 0;
}
3- 信号集操作函数
3.1 信号集set
函数
自定义信号集。
#include <signal.h>
sigset_t set;
1. 清空信号集(全部置0)
返回值:
成功:
0
失败:
-1
sigemptyset(sigset_t *set);
2. 全部置1
返回值:
成功:
0
失败:
-1
sigfillset(sigset_t *set);
3. 将一个信号添加到集合中
返回值:
成功:
0
失败:
-1
sigaddset(sigset_t *set, int signum);
4. 将一个信号从集合中移除
返回值:
成功:
0
失败:
-1
sigdelset(sigset_t *set, int signum);
判断一个信号是否在集合中
返回值:
在:
0
不在:
-1
sigismember(const sigset_t *set,int signum);
3.2 sigprocmask
函数
设置信号屏蔽字和解除屏蔽
3.2.1 函数原型
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how
:SIG_BLOCK
: 设置阻塞(与)SIG_UNBLOCK
: 取消阻塞(取反位与)SIG_SETMASK
: 用自定义set
替换mask
。(不推荐)
set
: 自定义set
oldset
:旧有的mask
。
返回值:
成功:
0
失败:
-1
3.3 sigpending
函数
3.3.1 函数原型
读取 / 查看未决信号集
int sigpending(sigset_t *set);
参数:
set
: 传出的未决信号集。
返回值:
成功:
0
失败:
-1
3.4 信号集操作示例
利用自定义集合,来设置信号阻塞,输入被设置阻塞的信号,可以看到未决信号集发生变化
// 使用alarm函数计时,打印变量i的值。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
void sys_err(const char *str) // 错误处理函数
{
perror(str);
exit(1);
}
// 打印set函数
void printset(sigset_t *set)
{
int i;
for(i = 1; i<32; i++)
{
if(sigismember(set, i))
putchar('1');
else
putchar('0');
}
printf("\n");
}
int main(int argc, char* argv[])
{
sigset_t set, oldset, pedset;
int ret = 0;
sigemptyset(&set); // 全部置0
sigaddset(&set, SIGINT); // 将 SIGINT (2号信号位置)[Ctrl+C] 信号添加到信号集
// sigaddset(&set, SIGINT); // 将 SIGQUIT (3号信号位置)[Ctrl+\] 信号添加到信号集
sigaddset(&set, SIGKILL); // SIGKILL,9号信号无法被捕获或忽略
ret = sigprocmask(SIG_BLOCK, &set, &oldset); // 将 SIGINT 信号设置为屏蔽
if(ret == -1)
{
sys_err("sigprocmask error");
}
while(1)
{
ret = sigpending(&pedset); // 查看当前信号集
if(ret == -1)
{
sys_err("sigpending error");
}
printset( &pedset );
sleep(1);
}
return 0;
}
结果:
在输入Ctrl+C之后,进程捕捉到信号,但由于设置阻塞,没有处理,未决信号集2号位置变为1.
4- 信号捕捉
4.1 signal
函数
注册一个信号捕捉函数,ANS 标准设置,不同操作系统存在差异建议使用sigaction
函数
4.1.1 函数原型
typedef void (*sighandler_t)(int)
void (*signal(int signum, void (*handler)(int)))(int);
参数:
signum
:表示待捕捉的信号编号。可以是预定义的信号常量(如SIGINT
、SIGTERM
等),也可以是自定义的信号编号。handler
:表示要设置的信号处理程序的函数指针。它接受一个整数参数,该参数是信号的编号。
返回的函数指针有以下几种可能的取值:
SIG_DFL
:表示将信号的处理方式恢复为默认行为。SIG_IGN
:表示忽略该信号,即不做任何处理。其他函数指针:表示设置了自定义的信号处理程序。
4.1.2 示例
注册一个信号捕捉函数,在函数中打印。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
// 错误处理函数
void sys_err(const char *str)
{
perror(str);
exit(1);
}
// 信号捕捉函数
void sig_cath(int signo)
{
printf("catch you! %d \n", signo);
return;
}
// 主函数
int main(int argc, char* argv[])
{
signal(SIGINT, sig_cath); // 注册信号捕捉函数
while(1);
return 0;
}
结果:
4.2 sigaction
函数
注册一个信号捕捉函数
4.2.1 函数原型
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
signum
:表示待捕捉的信号编号。可以是预定义的信号常量(如SIGINT
、SIGTERM
等),也可以是自定义的信号编号。act
/oldact
:一个指向struct sigaction
结构的指针,用于指定新/旧的信号处理程序
struct sigaction
结构的具体内容如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
其中:
sa_handler
是一个函数指针,用于指定信号处理程序的地址;(设置回调函数)sa_sigaction
也是一个函数指针,用于指定信号处理程序的地址,但它可以接收额外的参数;()sa_mask
是一个信号集,用于指定在处理该信号时要阻塞的其他信号;sigemptyset(&act.sa_mask);
// 清空sa_mask屏蔽字, 只在sig_catch工作时有效sa_flags
是一组标志位,用于指定信号处理的行为和选项;(默认0屏蔽)sa_restorer
是一个函数指针,用于指定一个恢复函数,在某些平台上可能会使用。
在调用sigaction
函数时,可以通过设置act
参数来指定新的信号处理程序和相关参数。如果不需要保存旧的信号处理程序,可以将oldact
参数设置为NULL
。
返回值:
成功:
0
失败:
-1
,errno
4.2.2 示例
使用sigaction
捕捉两个信号
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
// 错误处理函数
void sys_err(const char *str)
{
perror(str);
exit(1);
}
// 信号捕捉函数
void sig_catch(int signo)
{
if(signo == SIGINT)
{
printf("catch you! SIGINT:%d \n", signo);
// sleep(10); // 此期间信号被屏蔽,不再响应
return;
}
if(signo == SIGQUIT)
{
printf("catch you! SIGQUIT:%d \n", signo);
return;
}
}
// 主函数
int main(int argc, char* argv[])
{
struct sigaction act, oldact;
act.sa_handler = sig_catch; // 设置回调函数
sigemptyset(&act.sa_mask); // 清空sa_mask屏蔽字, 只在sig_catch工作时有效
act.sa_flags = 0; // flag 默认值,0 表信号屏蔽
// 第一个信号
int ret = sigaction(SIGINT, &act, &oldact); //注册信号2捕捉函数
if(ret == -1)
{
sys_err("sigaction error");
}
// 第二个信号
ret = sigaction(SIGQUIT, &act, &oldact); //注册信号3捕捉函数
if(ret == -1)
{
sys_err("sigaction error");
}
while(1);
return 0;
}
结果:
4.3 信号捕捉的特性
捕捉函数执行期间,信号屏蔽字 由
mask
-->sa_mask
, 捕捉函数执行结束。 恢复回mask
捕捉函数执行期间,本信号自动被屏蔽(
sa_flgs = 0
).其他信号不屏蔽,如需屏蔽则调用sigsetadd
函数修改(sigaddset(&act.sa_mask, SIGQUIT); // 屏蔽其他信号
)捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!
4.4 借助信号捕捉回收子进程
4.4.1 SIGCHLD
信号产生的条件
子进程终止时
子进程接收到
SIGSTOP
子进程处于停止态,接收到
SIGCONT
后唤醒时
4.4.2 回收多个子进程
问题
一次回调只回收一个子进程,同时出现多个子进程死亡时,只会回收累积信号中的一个子进程。
有可能父进程还没注册完捕捉函数,子进程就死亡了
解决方法
首先是让子进程sleep,但这个不太科学。在fork之前注册也行,但这个也不是很科学。
最科学的方法是在
int i
之前设置屏蔽,等父进程注册完捕捉函数再解除屏蔽。这样即使子进程先死亡了,信号也因为被屏蔽而无法到达父进程。解除屏蔽过后,父进程就能处理累积起来的信号了。
代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
// 错误处理函数
void sys_err(const char *str)
{
perror(str);
exit(1);
}
// 信号捕捉函数
void catch_child(int signo)
{
pid_t wpid;
int status;
// 该方法可以一次处理累计起来的多个信号,避免信号遗漏,防止僵尸进程出现
while((wpid = waitpid(-1, &status, 0)) != -1)
{
if(WIFEXITED(status))
{
printf("----------------catch child! ID = %d, ret = %d \n", wpid,WEXITSTATUS(status));
}
}
return;
}
int main(int argc, char* argv[])
{
pid_t pid;
// 阻塞子进程发给父进程的 SIGCHLD 信号,防止子进程提前死亡导致父进程收不到子进程死亡消息
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
int i;
for(i = 0; i<5; i++)
{
if((pid = fork()) == 0) // 循环期间, 子进程不 fork
{
break;
}
}
if(5 == i) // 父进程, 从 表达式 2 跳出
{
struct sigaction act;
act.sa_handler = catch_child; // 设置回调函数
sigemptyset(&act.sa_mask); // 清空sa_mask屏蔽字
act.sa_flags = 0; // flag 默认值,本信号自动屏蔽
int ret = sigaction(SIGCHLD, &act, NULL); //注册信号捕捉函数
if(ret == -1)
{
sys_err("sigaction error");
}
// 解除阻塞,使父进程可以收到子进程发来或阻塞时产生的 SIGCHLD 信号
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("im parent! pid = %d \n", getpid());
while(1); // 模拟父进程后续执行逻辑
}
else // 子进程, 从 break 跳出
{
printf("im child! pid = %d \n", getpid());
return i;
}
return 0;
}
结果:
4.5 慢速系统调用
可能会使进程永久阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期),也可以设定系统调用是否重启。如
read
,write
,pause
…其他系统调用:
getpid
、getppid
、fork
...
可以修改sa_flag
参数来设置被信号中断后系统调用是否重启。
SA_INTERRURT
:不重启SA_RESTART
:重启
sa_flag
其他的一些参数:
SA_RESETHAND
:当处理信号时,将信号的处理函数重置为默认处理函数(SIG_DFL
)。SA_NODEFER
:在信号处理函数执行期间,不阻塞当前信号。这意味着可以在信号处理函数中接收同一信号的其他实例。SA_NOCLDSTOP
:当子进程停止时,不会产生SIGCHLD
信号。这样可以防止父进程在子进程停止时接收到SIGCHLD
信号。SA_NOCLDWAIT
:当子进程终止时,系统不会将其转换为僵尸进程。这样可以避免父进程需要调用wait()
或waitpid()
来等待子进程终止。SA_SIGINFO
:在信号处理函数中,使用扩展的信号处理函数形式(void handler(int signum, siginfo_t *info, void *context)
)。这样可以获取更多关于信号的信息,如发送进程的PID等。SA_ONSTACK
:在备用信号栈上执行信号处理函数。备用信号栈可以用于在正常栈溢出时仍然能够处理信号。SA_RESETHAND
:当处理信号时,将信号的处理函数重置为默认处理函数(SIG_DFL
)。
5- 进程
5.1 会话
5.1.1 进程和进程组和会话的关系
进程组:(别名:作业)
多个进程的集合,每个进程都属于一个进程组,简化对多个进程的管理,
waitpid
函数和kill
函数的参数中用到父进程创建子进程的时候默认父子进程属于同一进程组。进程组的ID第一个进程ID(组长进程),组长进程id进程组id,组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。
只要有一个进程存在,进程组就存在,生存期与组长进程是否终止无关
kill -SIGKILL -进程组id
杀掉整个进程组一个进程可以为自己或子进程设置进程组id
创建会话的6点注意事项
调用进程不能是进程组组长,该进程变成新会话首进程(平民)
该进程成为一个新进程组的组长进程
需要
root
权限(ubuntu
不需要)新会话丢弃原有的控制终端,该会话没有控制终端
该调用进程是组长进程,则出错返回
建立新会话时,先调用
fork
,父进程终止,子进程调用setsid
5.1.2 getsid
函数
获取当前进程所属的会话ID
pid_t getsid(pid_t pid);
参数:
pid
:进程的pid
返回值:
成功:当前进程的会话
id
失败:
-1
,errno
5.1.3 setsid
函数
创建一个会话,并以自己的ID
设置进程组ID
,同时也是新会话的ID
pid_t setsid(void);
返回值:
成功:调用进程的会话
id
失败:
-1
,errno
5.2 守护进程
即daemon
(精灵)进程。是Linux中的后台服务进程,脱离控制终端。一般不与用户直接交互。周期性地执行某种任务或等待处理某些发生的事件。
不受用户登录注销影响。通常采用以d
结尾的命名方式。
如:ftp服务器,nfs服务器等。
5.2.1 创建守护进程
如何创建守护进程:
调用setsid
函数创建一个新的Session
,并成为Session Leader
步骤:
fork
子进程,让父进程终止。子进程调用
setsid()
创建新会话通常根据需要,使用
chdir()
函数改变工作目录位置 , 防止目录被卸载。int chdir(const char *path);
通常根据需要,重设
umask
文件权限掩码,影响新文件的创建权限。 022 -- 755 0345 --- 432 r---wx-w- 422通常根据需要,关闭/重定向 文件描述符
守护进程 业务逻辑。
while()
5.2.2 示例
创建一个守护进程
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <signal.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 > 0) // 终止父进程
{
exit(0);
}
// 新会话
pid = setsid(); // 创建新会话,子进程是会长
if(pid == -1)
{
sys_err("setsid error");
}
// 工作目录
ret = chdir("/home/simon/code/session"); // 改变工作目录位置
if(ret == -1)
{
sys_err("chdir error");
}
// 访问权限
umask(0022); // 改变文件访问权限掩码
// 文件描述符的关闭与重定向
close(STDIN_FILENO); // 关闭文件描述符 0
fd = open("/dev/null", O_RDWR);
if(fd == -1)
{
sys_err("open error");
}
dup2(fd, STDOUT_FILENO); // 重定向 STDOUT和STDERR
dup2(fd, STDERR_FILENO);
// work
while(1) // 模拟守护进程循环任务
{
printf("working...\n");
sleep(1);
}
return 0;
}
结果:
查看进程列表如下:
ps -aux
ps -ajx
这个daemon
进程就不会受到用户登录注销影响。
要想终止,就必须用kill
命令
6- 线程
进程:有独立的进程地址空间。有独立的
pcb
。 分配资源的最小单位。线程:有独立的
pcb
。没有独立的进程地址空间。 最小单位的执行。
在CPU眼中,线程当作一个进程。
6.1 基本概念
6.1.1 ps -Lf
用于显示进程线程信息
UID PID PPID LWP C NLWP STIME TTY TIME CMD
root 1 0 1 0 1 09:45 ? 00:00:01 /sbin/init
root 2 0 2 0 1 09:45 ? 00:00:00 [kthreadd]
root 3 2 3 0 1 09:45 ? 00:00:00 [ksoftirqd/0]
每一行代表一个线程的信息,字段含义如下:
UID
:线程所属用户的IDPID
:进程的ID(即主线程的ID)PPID
:父进程的IDLWP
:线程号C
:CPU使用率NLWP
:进程的线程数STIME
:进程启动时间TTY
:终端设备TIME
:线程的累计CPU时间CMD
:进程的命令行
例如:
查看pid
为1234进程的线程信息
ps -Lf 1234
6.1.2 三级映射
将进程的虚拟地址空间映射到物理内存
Linux 使用分页机制来管理虚拟内存,将进程的虚拟地址空间划分为多个固定大小的页(通常为4KB)。为了有效地管理这些页,Linux 使用了多级页表的结构,其中三级映射是一种常见的页表结构。
三级映射的页表结构由三级页表组成,从高到低分别是目录表(Page Global Directory)、中间表(Page Middle Directory)和页表(Page Table)。每个级别的页表都包含一系列的表项,用于存储虚拟页和物理页之间的映射关系。
当进程访问虚拟地址时,Linux 内核会根据三级映射的页表结构来查找对应的物理页。首先,通过目录表找到中间表的地址,再通过中间表找到页表的地址,最后通过页表找到物理页的地址。这个过程称为页表查找或页表遍历。
使用三级映射的好处是可以将虚拟地址空间划分为更小的区域,每个区域都有对应的页表。这样可以减少整个页表的大小,节省内存空间。同时,三级映射也提供了更灵活的页表管理,可以根据需要动态分配和释放页表。
Linux 的页表结构可以根据系统的需求进行调整,三级映射只是其中一种常见的结构。在一些系统中,可能会使用更多或更少级别的页表来管理虚拟内存。这些结构的选择取决于硬件架构、内存需求和性能考虑等因素。
6.1.3 线程共享和非共享
<font color=blue>线程共享资源:</font>
文件描述符表
每种信号的处理方式
当前工作目录
用户 ID 和组 ID
内存地址空间(.text / .data / .bss / heap / 共享库)
线程非共享资源:
线程 ID
处理器现场和栈指针(内核栈)
独立的栈空间
errno
变量信号屏蔽字
调度优先级
线程的优缺点
优点:
提高程序并发性
开销小
数据通信、共享数据方便
缺点:
库函数,不稳定
调试、编写困难,gdb 不支持
对信号支持不好
优点突出,缺点不是硬伤。Linux下进程、线程区别不大。
6.2 创建线程
6.2.1 pthread_self
函数
获取线程 ID。 对应进程中的 getsid()
pthread_t pthread_self();
返回值:
本线程 ID
6.2.2 pthread_create
函数
获取线程 ID。 对应进程中的 getsid()
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void *), void *arg);
参数:
tid
:传出参数,表新创建的子线程 idattr
:线程属性。传 NULL 表使用默认属性。start_rountn
:子线程回调函数。创建成功,ptherad_create
函数返回时,该函数会被自动调用。arg
:回调函数的参数。没有的话,传 NULL
返回值:
成功:
0
失败:
errno
6.2.3 示例
循环创建多个子线程
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
// 子线程回调函数
void *tfunc(void *arg)
{
int i = *((int *)arg); // 此处强转无数据丢失,需要采用值传递
sleep(i);
printf("-----NO: %dth, thread: pid = %d, tid = %lu \n", i+1, getpid(), pthread_self());
return NULL;
}
// 主函数
int main(int argc, char* argv[])
{
// 定义变量
int i;
pthread_t tid;
// 循环创建
for(i = 0; i < 6; i++)
{
int ret = pthread_create(&tid, NULL, tfunc, (void*)&i);
if(ret != 0)
{
sys_err("pthread_create", ret);
}
}
sleep(i);
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
return 0;
}
结果:
易错分析:
如果我们在给回调函数传参的时候用引用传递呢?
比如把:
// 主函数中:
(void *)i -------> (void *)&i
// 子进程回调函数中
int i = (int *)arg; -------> int i = *((int *)arg);
那么结果就会变成:
错误原因在于,子线程如果用引用传递i
,会去读取【主线程】里的【i
值】,而主线程里的i
是动态变化的,不固定。所以,应该采用值传递,不用引用传递。
6.3 线程间全局变量共享
子线程里更改全局变量后,主线程里也跟着发生变化。
6.4 线程回收与退出
6.4.1 pthread_exit
退出
退出当前线程
void pthread_exit(void *retval);
参数:
retval
:退出值。 无退出值时,NULL
========
各种退出函数区别
exit();
退出当前进程。return
: 返回到调用者那里去。pthread_exit()
: 退出当前线程。
========
示例:
如果在【回调函数】里加一段代码:
if(i == 2)
exit(0);
看起来好像是退出了第三个子线程,然而运行时,发现后续的4,5也没了。这是因为,exit是退出进程。
修改一下,换成:
if(i == 2)
return NULL;
这样运行一下,发现后续线程不会凉凉,说明return是可以达到退出线程的目的。然而真正意义上,return是返回到函数调用者那里去,线程并没有退出。
再修改一下,再定义一个函数func
,直接返回那种
void *func(void)
{
return NULL;
}
......
if(i == 2)
func();
运行,发现1,2,3,4,5线程都还在,说明没有达到退出目的。
再次修改:
void *func(void)
{
pthread_exit(NULL);
return NULL;
}
.......
if(i == 2)
func();
编译运行,发现3没了,看起来很科学的样子。pthread_exit
表示将当前线程退出。放在函数里,还是直接调用,都可以。
pthread_exit
函数还可以用来退出主线程,使主线程的退出不影响子线程的执行。可以替换使用return 0
加sleep
的方法。
6.4.2 pthread_join
回收
阻塞等待线程退出,获取线程退出状态; ------ 对应于进程中的 waitpid()
函数
int pthread_join(pthread_t thread, void **retval);
参数:
thread
: 待回收的线程 idretval
:传出参数。 回收的那个线程的退出值。线程异常结束值为
-1
返回值:
成功:
0
失败:
errno
示例:
回收线程并获取子线程返回值
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
// 该结构体的数据在线程结束后通过join传出
struct thrd
{
int var;
char str[256];
};
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
// 子线程回调函数
void *tfunc(void *arg)
{
struct thrd *tval;
// 分配内存
tval = malloc(sizeof(tval));
tval->var = 100;
strcpy(tval->str, "hello thread");
return (void*)tval;
}
// 主函数
int main(int argc, char* argv[])
{
// 定义
pthread_t tid;
struct thrd *retval;
// 创建线程
int ret = pthread_create(&tid, NULL, tfunc, NULL);
if(ret != 0)
{
sys_err("pthread creat", ret);
}
// 阻塞回收线程
ret = pthread_join(tid, (void **)&retval);
if(ret != 0)
{
sys_err("pthread join", ret);
}
// 拿到线程传出的数据并打印
printf("child thread exit with var = %d, str = %s \n", retval->var, retval->str);
pthread_exit(NULL);
return 0;
}
结果:
6.4.3 pthread_cancel
函数
杀死一个线程。 需要到达取消点(保存点)
函数原型:
int pthread_cancel(pthread_t thread);
参数:
thread
: 待杀死的线程 id
返回值:
成功:
0
失败:
errno
tips: 如果子进程函数没有进入内核,需要手动设置取消点pthread_testcancel()
示例:
cancel
延迟5秒杀死进程
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
// 子线程回调函数
void *tfunc(void *arg)
{
while(1)
{
printf("thread: pid = %d, tid = %lu \n", getpid(), pthread_self());
sleep(1);
}
}
// 主函数
int main(int argc, char* argv[])
{
// 定义
pthread_t tid;
// 创建线程
int ret = pthread_create(&tid, NULL, tfunc, NULL);
if(ret != 0)
{
sys_err("pthread creat", ret);
}
sleep(5); // 延迟5秒
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
// 终止线程
ret = pthread_cancel(tid);
if(ret != 0)
{
sys_err("pthread cancel", ret);
}
else
{
printf("child pthread has been canceled \n");
}
// while(1);
pthread_exit(NULL);
return 0;
}
结果:
6.4.4 三种终止线程的方式
exit
pthread_exit()
pthread_cancel()
,使用到了取消点:pthread_testcancel()
示例:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
// 子线程回调函数1
void *tfunc1(void *arg)
{
printf("thread1 returning");
return (void *)111; // 和exit(111)一样;
}
// 子线程回调函数2
void *tfunc2(void *arg)
{
printf("thread2 returning");
pthread_exit((void *)222);
}
// 子线程回调函数3
void *tfunc3(void *arg)
{
int i = 1;
while(1)
{
printf("thread3 : im going to die in %d seconds\n", i++);
sleep(1);
pthread_testcancel(); // 自己添加取消点
}
return (void *)333;
}
// 主函数
int main(int argc, char* argv[])
{
// 定义
pthread_t tid;
void *tret = NULL;
// 线程1
int ret = pthread_create(&tid, NULL, tfunc1, NULL);
if(ret != 0)
{
sys_err("pthread creat", ret);
}
pthread_join(tid, &tret);
printf("thread 1 exit code = %d \n", (int)(intptr_t)tret); // 强制使用(int)转换会报警告,通过将指针转换为 intptr_t 类型,然后再将其转换为整数类型。
// intptr_t 是一个整数类型,它的大小足够大以容纳指针类型的值。
// 线程2
ret = pthread_create(&tid, NULL, tfunc2, NULL);
if(ret != 0)
{
sys_err("pthread creat", ret);
}
pthread_join(tid, &tret);
printf("thread 2 exit code = %d \n", (int)(intptr_t)tret);
// 线程3
ret = pthread_create(&tid, NULL, tfunc3, NULL);
if(ret != 0)
{
sys_err("pthread creat", ret);
}
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("thread 3 exit code = %d \n", (int)(intptr_t)tret);
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
// while(1);
pthread_exit(NULL);
return 0;
}
结果:
6.5 线程分离
6.5.1 pthread_detach
函数
实现线程分离
函数原型:
int pthread_detach(pthread_t thread);
参数:
thread
: 待分离的线程 id
返回值:
成功:
0
失败:
errno
示例:
使用detach
分离线程,分离后的线程会自动回收
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
// 子线程回调函数
void *tfunc(void *arg)
{
while(1)
{
printf("thread: pid = %d, tid = %lu \n", getpid(), pthread_self());
sleep(1);
}
}
// 主函数
int main(int argc, char* argv[])
{
// 定义
pthread_t tid;
// 创建线程
int ret = pthread_create(&tid, NULL, tfunc, NULL);
if(ret != 0)
{
sys_err("pthread creat", ret);
}
// 分离线程(类似于join回收子线程)
ret = pthread_detach(tid);
if(ret != 0)
{
sys_err("pthread detach", ret);
}
sleep(1);
// 回收线程(会失败)
ret = pthread_join(tid, NULL);
printf("join ret = %d \n", ret);
if(ret != 0)
{
sys_err("pthread join", ret);
}
pthread_exit(NULL);
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
return 0;
}
结果:
线程分离后,系统会自动回收资源,用pthread_join
去回收已经被系统回收的线程,那个线程号就是无效参数。
6.6 线程和进程的控制语句对比
6.7 线程属性设置分离线程
线程属性(Thread Attributes)使用 pthread_attr_t
结构体来表示。pthread_attr_t
结构体定义在 <pthread.h>
头文件中,可以使用 pthread_attr_init()
函数来初始化该结构体。
6.7.1 使用方法
以下是一些pthread_attr_t
函数的使用方法:
初始化线程属性,必须在使用
pthread_attr_t
结构体之前调用此函数int pthread_attr_init(pthread_attr_t *attr) // 成功: 0 // 失败: errno
销毁线程属性结构体。
pthread_attr_destroy(pthread_attr_t *attr) // 成功: 0 // 失败: errno
设置线程的分离状态。
pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate) // 参数detachstate: // PTHREAD_CREATE_JOINABLE : 非分离 // PTHREAD_CREATE_DETACHED : 分离
设置线程的栈大小。
pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param) // 设置线程的调度参数。param 参数是一个指向 struct sched_param 结构体的指针,可以设置线程的优先级和调度策略。
设置线程的调度策略。
pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit) // inherit 参数可以是 PTHREAD_INHERIT_SCHED(表示线程继承创建它的线程的调度属性)或 PTHREAD_EXPLICIT_SCHED(表示线程使用自己显式设置的调度属性)。
......
一旦设置了所需的属性,就可以将线程属性结构体作为参数传递给 pthread_create()
函数来创建线程。
6.7.2 示例
使用pthread_attr_t
设置线程属性,并验证
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <signal.h> // 信号头文件
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
// 子线程回调函数
void *tfunc(void *arg)
{
while(1)
{
printf("thread: pid = %d, tid = %lu \n", getpid(), pthread_self());
sleep(1);
}
}
// 主函数
int main(int argc, char* argv[])
{
// 定义
pthread_t tid;
pthread_attr_t attr;
// 初始化
int ret = pthread_attr_init(&attr);
if(ret != 0)
{
sys_err("pthread_attr_init", ret);
}
// 设置分离状态
ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if(ret != 0)
{
sys_err("pthread_attr_setdetachstate", ret);
}
// 创建线程
ret = pthread_create(&tid, &attr, tfunc, NULL); // 创建线程时候使用创建好的属性
if(ret != 0)
{
sys_err("pthread creat", ret);
}
// 销毁线程属性属性
ret = pthread_attr_destroy(&attr);
if(ret != 0)
{
sys_err("pthread_attr_destroy", ret);
}
sleep(1); // 等待子进程死亡
// 回收线程(会失败)说明线程是分离的
ret = pthread_join(tid, NULL);
printf("join ret = %d \n", ret);
if(ret != 0)
{
sys_err("pthread join", ret);
}
pthread_exit(NULL);
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
return 0;
}
结果:
如果线程是分离的,则调用join
函数阻塞回收线程会失败。
6.8 线程使用注意事项
主线程退出其他线程不退出,主线程应该调用
pthread_exit
避免僵尸线程
pthread_join
pthread_detach
pthread_create
指定分离属性被
join
线程可能在join
函数返回前就释放自己的所有内存资源,所以不应当返回被回收线程栈中的值
malloc
和mmap
申请的内存可以被其他线程释放应避免在多线程中调用
fork
,除非马上exec
,子线程中只有调用fork
的线程存在,其他线程在子进程中均pthread_exit
信号的复杂语义很难和多线程共存,在多线程中避免使用信号机制
7- 锁
7.1 线程同步---锁的使用注意事项
对公告区域的数据按序访问,防止数据混乱,产生于时间有关的错误。
数据混乱的原因:
资源共享(独享资源则不会)
调度随机(意味着数据访问会出现竞争)
线程间缺乏必要同步机制
锁可以对公共数据进行保护。所有线程应该在访问公共数据之前先拿锁再访问。但是锁本身不具备强制性,线程直接访问数据也是可以成功的。
7.2 互斥锁
pthread_mutex
是 POSIX 线程库中用于实现互斥锁(mutex)的类型。互斥锁用于保护临界区,确保在同一时间只有一个线程可以访问共享资源。
7.2.1 pthread_mutex_t
函数
以下是 pthread_mutex
的使用方法和一些常用函数:
a) 初始化和销毁互斥锁
互斥锁类型,通常通过声明
pthread_mutex_t
变量来创建互斥锁。pthread_mutex_t lock...
初始化互斥锁。必须在使用
pthread_mutexattr_t
对象之前调用此函数。pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)
参数:
mutex
:是一个指向pthread_mutex_t
的指针attr
:是一个指向互斥锁属性的指针(通常设置为NULL
表示使用默认属性)。
销毁互斥锁。
pthread_mutex_destroy(pthread_mutex_t *mutex)
b) 加锁和解锁
加锁互斥锁。如果互斥锁已被其他线程锁定,调用线程将被阻塞,直到互斥锁可用。会阻塞
pthread_mutex_lock(pthread_mutex_t *mutex)
尝试加锁互斥锁。如果互斥锁已被其他线程锁定,调用将立即返回,并返回一个非零值表示加锁失败。不会阻塞
pthread_mutex_trylock(pthread_mutex_t *mutex)
成功获取互斥锁时,返回值为 0。
获取互斥锁失败时,返回值为非零值,表示获取锁失败的错误码
EBUSY
。
解锁互斥锁。释放互斥锁,允许其他线程获得锁。
pthread_mutex_unlock(pthread_mutex_t *mutex)
c) 互斥锁属性
设置互斥锁的共享属性。
pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared)
pshared
参数:互斥锁仅在进程内共享:
PTHREAD_PROCESS_PRIVATE
互斥锁可以在不同进程之间共享:
PTHREAD_PROCESS_SHARED
设置互斥锁的类型。
pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type)
type
参数:默认值,标准互斥锁:
PTHREAD_MUTEX_NORMAL
带有错误检查的互斥锁:
PTHREAD_MUTEX_ERRORCHECK
递归互斥锁:
PTHREAD_MUTEX_RECURSIVE
根据实现定义的默认类型:
PTHREAD_MUTEX_DEFAULT
设置互斥锁的鲁棒性属性。
pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust)
robust
参数:PTHREAD_MUTEX_STALLED
:默认值, 当持有锁的线程终止时,其他线程将一直等待PTHREAD_MUTEX_ROBUST
: 当持有锁的线程终止时,其他线程将获得错误码EOWNERDEAD
,需要手动处理
将互斥锁属性对象作为参数传递给 pthread_mutex_init()
函数来初始化互斥锁
7.2.2 互斥锁示例
示例:主线程循环打印大写的 HELLO WORLD ,子线程循环打印小写的 hello world 。
未加互斥锁:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
// 子线程回调函数
void *tfunc(void *arg)
{
while(1)
{
printf("hello ");
sleep(rand()%3); /*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/
printf("world\n");
sleep(rand()%3);
}
return NULL;
}
// 主函数
int main(int argc, char* argv[])
{
// 定义
pthread_t tid;
srand(time(NULL));
// 创建线程
int ret = pthread_create(&tid, NULL, tfunc, NULL); // 创建线程时候使用创建好的属性
if(ret != 0)
{
sys_err("pthread creat", ret);
}
while(1)
{
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
sleep(rand()%3);
}
pthread_join(tid, NULL);
pthread_exit(NULL);
return 0;
}
结果:
主线程和子线程交叉输出,并且数据混乱。
使用互斥锁
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
pthread_mutex_t mutex; // 全局定义一把互斥锁
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
// 子线程回调函数
void *tfunc(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex); // 加锁
printf("hello ");
sleep(rand()%3); /*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/
printf("world\n");
pthread_mutex_unlock(&mutex); // 解锁
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)
{
sys_err("pthread_mutex_init", ret);
}
// 创建线程
ret = pthread_create(&tid, NULL, tfunc, NULL); // 创建线程时候使用创建好的属性
if(ret != 0)
{
sys_err("pthread creat", ret);
}
while(1)
{
pthread_mutex_lock(&mutex); // 加锁
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex); // 解锁
sleep(rand()%3);
}
pthread_join(tid, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
pthread_exit(NULL);
return 0;
}
结果:
达到了预期效果。
7.2.3 互斥锁的使用技巧
尽量保证锁的粒度, 越小越好。(访问共享数据前,加锁。访问结束【立即】解锁。)
互斥锁本质是结构体,但是可以看成整数,初始化后值为 1
加锁:-- 操作, 阻塞线程。
解锁:++ 操作, 唤醒阻塞在锁上的线程。
try 锁:尝试加锁,成功 --。失败,返回。同时设置错误号
EBUSY
7.3 读写锁
写独占,读共享
写锁优先级高
锁只有一把
7.3.1 概念和特性
读写锁(Read-Write Lock)是一种用于多线程环境下的锁机制,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。读写锁的状态和特性如下:
读锁状态(Shared Lock):
多个线程可以同时持有读锁。
当没有线程持有写锁时,可以获取读锁。
读锁是共享的,可以同时由多个线程持有。
读锁是非排他的,不会阻塞其他线程获取读锁。
写锁状态(Exclusive Lock):
只能有一个线程持有写锁。
当没有线程持有读锁或写锁时,可以获取写锁。
写锁是排他的,会阻塞其他线程获取读锁和写锁。
读写锁的特性包括:
读优先:当有线程持有读锁时,其他线程仍然可以获取读锁,但会阻塞写锁的获取。这样可以提高并发性,允许多个线程同时读取共享资源。
写优先:当有线程持有写锁时,其他线程无法获取读锁和写锁,必须等待写锁释放。这样可以确保写操作的原子性和一致性,避免读取到不一致的数据。
避免写饥饿:读写锁的设计避免了写线程长时间等待的情况,即使有大量的读线程存在,写线程也有机会获取写锁。
公平性:读写锁的实现可以是公平的或非公平的,公平性指的是等待时间较长的线程更有机会获取锁。具体实现取决于操作系统或线程库。
7.3.2 pthread_rwlock_t
函数
a) 初始化和销毁读写锁
初始化读写锁对象。
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr)
参数:
rwlock
是指向读写锁对象的指针。attr
是读写锁属性对象的指针,可以为NULL
。
销毁读写锁对象。
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
b) 加锁和解锁
加读锁。如果有其他线程持有写锁,则当前线程会被阻塞,直到写锁被释放。
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
加写锁。如果有其他线程持有读锁或写锁,则当前线程会被阻塞,直到所有的读锁和写锁都被释放。
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
尝试获取读锁,获取锁失败时会立即返回 不会阻塞
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
成功获取读锁时,返回值为 0。
获取读锁失败时,返回值为非零值,表示获取锁失败的错误码。
尝试获取写锁,获取锁失败时会立即返回 不会阻塞
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
成功获取写锁时,返回值为 0。
获取写锁失败时,返回值为非零值,表示获取锁失败的错误码。
解锁。释放当前线程持有的读锁或写锁。
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
c) 读写锁属性
初始化读写锁属性对象。
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr)
销毁读写锁属性对象。
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr)
设置读写锁的共享属性。
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared)
pshared
参数:PTHREAD_PROCESS_PRIVATE
:锁只能由同一进程内的线程共享PTHREAD_PROCESS_SHARED
:锁可由多个进程共享
获取读写锁的共享属性。
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared)
7.3.3 读写锁示例
创建多个线程不定时对共享资源进行读和写
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
int counter;
pthread_rwlock_t rwlock; // 全局定义一把读写锁
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
//** 三个线程不定时写同一全局资源 **//
void *th_write(void *arg)
{
int t;
intptr_t i = (intptr_t)arg;
while(1)
{
pthread_rwlock_wrlock(&rwlock); // 以写模式加锁,写独占
t = counter;
usleep(1000);
printf("=====write %d: %lu: counter = %d ++counter = %d \n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(10000);
}
return NULL;
}
//** 五个线程不定时读同一全局资源 **//
void *th_read(void *arg)
{
int t;
intptr_t i = (intptr_t)arg;
while(1)
{
pthread_rwlock_rdlock(&rwlock); // 读锁共享
t = counter;
printf("--------------------------- read : %d : %lu: %d \n", i, pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}
// 主函数
int main(int argc, char* argv[])
{
// 定义
int i;
pthread_t tid[8];
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 创建线程
for(i = 0; i < 3; i++)
pthread_create(&tid[i], NULL, th_write, (void *)(intptr_t)i);
for(i = 0; i < 5; i++)
pthread_create(&tid[i], NULL, th_read, (void *)(intptr_t)i);
// 回收线程
for(i = 0; i < 8; i++)
pthread_join(tid[i], NULL);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
pthread_exit(NULL);
return 0;
}
结果:核心:读共享,写独占,写锁优先级高
7.3.4 时间粒度对于读写锁的影响
时间粒度 指的是时间的最小单位,它决定了你可以设置的最小延时或时间间隔。
时间粒度较大,例如延时或时间间隔设置为较大的值,那么读写线程之间的竞争可能会减少,写线程更容易获得写锁。因为读线程需要等待一段较长的时间才能再次获取读锁,给写线程更多的机会获得写锁。
时间粒度较小,例如延时或时间间隔设置为较小的值,那么读写线程之间的竞争可能会增加,写线程更难获得写锁。因为读线程可以更频繁地获取读锁,减少了写线程获得写锁的机会。
在程序中需要设置合理的时间粒度,平衡读写线程之间的竞争,提高程序的性能和响应能力。
在上述例子中,分别使用 sleep()
加大时间粒度和使用 nanosleep
减小时间粒度,对比得到如下结果:
使用
sleep()
函数统一延时1秒读写线程基本上是机会均等的
使用
nanosleep
函数
读线程会大量获得CPU资源,而且是一个线程会连任的状态。
7.4 两种死锁
使用锁不恰当导致的现象。
线程试图对同一个锁反复
lock
线程 1 拥有 A 锁,请求获得 B 锁;线程 2 拥有 B 锁,请求获得 A 锁
8- 条件变量和信号量
8.1 条件变量
条件变量本身不是锁,但是通常结合锁(mutex)来使用。
8.1.1 pthread_cond_
函数
以下是 pthread_cond_
的使用方法和一些常用函数:
a)初始化和销毁条件变量:
初始化条件变量
pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr)
cond
:指向条件变量对象的指针。attr
:条件变量的属性,通常可以传入NULL
使用默认属性。
销毁条件变量
pthread_cond_destroy(pthread_cond_t *cond)
cond
:指向条件变量对象的指针。
b)等待和唤醒函数:
等待条件变量满足特定条件
pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
cond
:指向条件变量对象的指针。mutex
:指向互斥锁对象的指针。在调用函数之前,必须先获取该互斥锁。
等待条件变量满足特定条件,但是它允许在指定的时间范围内等待条件变量。
pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)
cond
:指向条件变量对象的指针。mutex
:指向互斥锁对象的指针。在调用函数之前,必须先获取该互斥锁。abstime
:指定的绝对时间,即等待的时间范围。如果超过指定时间仍未满足条件,线程将被唤醒。
发送条件变量满足的信号给等待的线程中的一个线程。
pthread_cond_signal(pthread_cond_t *cond)
cond
:指向条件变量对象的指针。发送条件变量满足的信号给等待的线程中的一个线程。
发送条件变量满足的信号给等待的所有线程
pthread_cond_broadcast(pthread_cond_t *cond)
cond
:指向条件变量对象的指针
以上6个函数返回值都是:
成功:
0
失败:
errno
8.1.2 初始化
初始化条件变量
pthread_cond_t cond;
动态初始化:
pthread_cond_init(&cond, NULL);
静态初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
8.1.3 wait
函数
pthread_cond_wait(&cond, &mutex);
作用:
阻塞等待条件变量满足
解锁已经加锁成功的信号量 (相当于
pthread_mutex_unlock(&mutex)
),前两步为一个原子操作当条件满足,函数返回时,解除阻塞并重新申请获取互斥锁。重新加锁信号量 (相当于,
pthread_mutex_lock(&mutex);
)
8.1.4 示例
生产者生产数据存储到公共区域,消费者在公共区域消费这些数据,利用pthread_cond_wait
阻塞消费者。
/*借助条件变量模拟 生产者 消费者 问题*/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
/*链表作为共享数据,需要被互斥量保护*/
struct msg
{
struct msg *next;
int num;
};
struct msg *head;
/*静态初始化 一个条件变量,一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; // 定义/初始化一个条件变量
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 定义/初始化一个互斥量
void *consumer(void *arg)
{
while(1)
{
struct msg *mp;
// 加锁,互斥量
pthread_mutex_lock(&lock);
if(head == NULL)
{
// 阻塞等待条件变量,解锁
pthread_cond_wait(&has_product, &lock);
}
// 消费者消费(头删法)
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock); // 解锁 互斥量
printf("--------------------consumer %d\n",mp->num);
free(mp);
sleep(rand()%3);
}
return NULL;
}
void *produser(void *arg)
{
while(1)
{
struct msg *mp = malloc(sizeof(struct msg));
// 加锁互斥量
pthread_mutex_lock(&lock);
// 模拟生产一个数据
mp->num = rand()%1000+1;
printf("-produce=====%d\n",mp->num);
// 写入公共区域(链表头插法)
mp->next = head;
head = mp;
// 解锁互斥量
pthread_mutex_unlock(&lock);
// 唤醒阻塞在条件变量has_product上面的线程
pthread_cond_signal(&has_product);
sleep(rand()%3);
}
return NULL;
}
int main(int argc, char *argv[])
{
int ret;
pthread_t pid, cid;
srand(time(NULL)); // 随机数种子
// 生产者
ret = pthread_create(&pid, NULL, produser, NULL);
if(ret != 0)
{
sys_err("pthread_creat_produser", ret);
}
// 消费者
ret = pthread_create(&cid, NULL, consumer, NULL);
if(ret != 0)
{
sys_err("pthread_creat_consuer", ret);
}
// 回收线程
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
结果:
8.1.5 示例2
对于上面的示例中,将一个消费者改为多个消费者。
Tips:需要使用while
来判断是否有数据
/*借助条件变量模拟 生产者 消费者 问题*/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
/*链表作为共享数据,需要被互斥量保护*/
struct msg
{
struct msg *next;
int num;
};
struct msg *head;
/*静态初始化 一个条件变量,一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; // 定义/初始化一个条件变量
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 定义/初始化一个互斥量
void *consumer(void *arg)
{
while(1)
{
struct msg *mp;
// 加锁,互斥量
pthread_mutex_lock(&lock);
while(head == NULL)
{
// 阻塞等待条件变量,解锁
pthread_cond_wait(&has_product, &lock);
}
// 消费者消费(头删法)
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock); // 解锁 互斥量
printf("--------------------consumer id : %lu data id : %d\n",pthread_self(),mp->num);
free(mp);
sleep(rand()%3);
}
return NULL;
}
void *produser(void *arg)
{
while(1)
{
struct msg *mp = malloc(sizeof(struct msg));
// 加锁互斥量
pthread_mutex_lock(&lock);
// 模拟生产一个数据
mp->num = rand()%1000+1;
printf("-produce=====%d\n",mp->num);
// 写入公共区域(链表头插法)
mp->next = head;
head = mp;
// 解锁互斥量
pthread_mutex_unlock(&lock);
// 唤醒阻塞在条件变量has_product上面的线程
pthread_cond_signal(&has_product);
sleep(rand()%3);
}
return NULL;
}
int main(int argc, char *argv[])
{
int ret;
pthread_t pid, cid1, cid2, cid3;
srand(time(NULL)); // 随机数种子
// 生产者
ret = pthread_create(&pid, NULL, produser, NULL);
if(ret != 0)
{
sys_err("pthread_creat_produser", ret);
}
// 消费者1
ret = pthread_create(&cid1, NULL, consumer, NULL);
if(ret != 0)
{
sys_err("pthread_creat_consuer", ret);
}
// 消费者2
ret = pthread_create(&cid2, NULL, consumer, NULL);
if(ret != 0)
{
sys_err("pthread_creat_consuer", ret);
}
// 消费者3
ret = pthread_create(&cid3, NULL, consumer, NULL);
if(ret != 0)
{
sys_err("pthread_creat_consuer", ret);
}
// 回收线程
pthread_join(pid, NULL);
pthread_join(cid1, NULL);
pthread_join(cid2, NULL);
pthread_join(cid3, NULL);
return 0;
}
结果:
8.2 信号量
应用于线程、进程间同步,控制同时访问某个资源的线程数量。
相当于 初始化值为 N 的互斥量。 N值,表示可以同时访问共享数据区的线程数。
8.2.1 sem_
函数
#include <semaphore.h>
头文件
a)初始化和销毁信号量:
sem_open()
创建或打开一个具名信号量sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value)
name
:信号量的名称。oflag
:标志位,用于指定信号量的创建和访问权限。mode
:权限模式,用于设置信号量的访问权限。value
:信号量的初始值。( 初始值:决定了可以同时访问的线程数 )
sem_init()
初始化一个匿名信号量int sem_init(sem_t *sem, int pshared, unsigned int value);
sem
:指向要初始化的信号量的指针。pshared
:指定信号量的共享方式,为 0 表示线程间共享,非 0 表示进程间共享。value
:信号量的初始值。
sem_close()
:关闭一个具名信号量。int sem_close(sem_t *sem);
sem
:指向要关闭的信号量的指针。
sem_unlink()
:删除一个具名信号量。int sem_unlink(const char *name);
name
:要删除的信号量的名称
sem_destroy()
:销毁一个匿名信号量。int sem_destroy(sem_t *sem);
sem
:指向要销毁的信号量的指针。
b)信号量的操作:
sem_wait()
:对信号量进行 P 操作,即将信号量的值减一,如果值小于等于0,则阻塞等待。相当于
lock
函数int sem_wait(sem_t *sem);
sem_trywait()
:对信号量进行非阻塞的 P 操作,即将信号量的值减一,如果值小于等于0,则立即返回。int sem_trywait(sem_t *sem);
sem_timedwait()
:对信号量进行定时的P操作,即将信号量的值减一,如果值小于等于0,则等待一定的时间后返回。int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
sem
:指向要操作的信号量的指针。abs_timeout
:等待的绝对超时时间(精度:秒和纳秒)
sem_post()
:对信号量进行 V 操作,即将信号量的值加一,如果有等待的线程或进程,则唤醒其中一个。相当于
unlock
函数int sem_post(sem_t *sem);
sem_getvalue()
:获取信号量的当前值。int sem_getvalue(sem_t *sem, int *sval);
以上函数返回值都是:
成功:
0
失败:
errno
8.2.2 示例
使用信号量实现生产者,消费者模型
/*借助条件变量模拟 生产者 消费者 问题*/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#define NUM 5 // 公共区域大小为5
// 错误处理函数
void sys_err(const char *str, int errnum)
{
fprintf(stderr, "%s error: %s\n", str, strerror(errnum));
exit(1);
}
// 全局定义
int queue[NUM]; //全局数组实现环形队列
sem_t product_num; // 产品数量的信号量
sem_t blank_num; // 空格数量的信号量
pthread_mutex_t lock; // 互斥锁
void *consumer(void *arg)
{
int i;
while(1)
{
// 产品数--,结果小于0则阻塞等待
sem_wait(&product_num);
// 加锁,保护共享资源
pthread_mutex_lock(&lock);
// 消费者消费数据
printf("-----------------consumer :%d\n",queue[i]);
queue[i] = 0;
// 解锁
pthread_mutex_unlock(&lock);
// 消费数据后空格数++,并唤醒阻塞在该信号量上的一个线程
sem_post(&blank_num);
i = (i+1) % NUM; // 借助下标实现环形
sleep(rand()%3);
}
return NULL;
}
void *produser(void *arg)
{
int i = 0;
while(1)
{
// 生产数据,空格数--,如果值小于0,则阻塞等待
sem_wait(&blank_num);
// 加锁,保护共享资源
pthread_mutex_lock(&lock);
// 模拟生产一个数据
queue[i] = rand()%1000+1;
printf("-produce=====%d\n",queue[i]);
// 解锁
pthread_mutex_unlock(&lock);
// 将产品数++,并唤醒阻塞在该信号量上的一个线程
sem_post(&product_num);
i = (i+1) % NUM;
sleep(rand()%3);
}
return NULL;
}
int main(int argc, char *argv[])
{
int ret;
pthread_t pid, cid;
srand(time(NULL)); // 随机数种子
// 初始化信号量
sem_init(&blank_num, 0, NUM);
sem_init(&product_num, 0, 0);
// 初始化互斥锁
pthread_mutex_init(&lock, NULL);
// 生产者
ret = pthread_create(&pid, NULL, produser, NULL);
if(ret != 0)
{
sys_err("pthread_creat_produser", ret);
}
// 消费者
ret = pthread_create(&cid, NULL, consumer, NULL);
if(ret != 0)
{
sys_err("pthread_creat_consuer", ret);
}
// 回收线程
pthread_join(pid, NULL);
pthread_join(cid, NULL);
// 销毁信号量和互斥锁
sem_destroy(&product_num);
sem_destroy(&blank_num);
pthread_mutex_destroy(&lock);
return 0;
}
结果:
- 感谢你赐予我前进的力量