1- 信号的概念


1.1 信号概念与机制

  1. 信号共性:

简单、不能携带大量信息、满足条件才发送。

  1. 信号的特质:

信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。

所有信号的产生及处理全部都是由【内核】完成的。

1.2 信号相关概念

产生信号

  • 按键产生:如 Ctrl + cCtrl + zCtrl + \

  • 系统调用产生:如 killraiseabort

  • 软件条件产生:如 定时器 alarm(sleep)

  • 硬件异常产生:如 非法访问内存( 段错误 )、除0 ( 浮点数除外 )、内存对齐出错 ( 总线错误 )

  • 命令产生:如 kill命令

递达

信号递送并且到达进程

未决

产生到递达之间的状态。主要由于阻塞(屏蔽)导致该状态。

信号的处理方式

  • 执行默认处理动作

  • 忽略(丢弃)

  • 捕捉(调用用户函数进行处理)

阻塞信号集(信号屏蔽字)

将某些信号加入集合,对他们设置屏蔽,当屏蔽 X 信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)

本质 ---- 位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决状态。

未决信号集

  1. 信号产生,未决信号集中描述该信号的位立刻反转位为 1 , 表示信号处于未决状态。当信号被处理对应位翻转回 0 。这一时刻往往非常短暂。

  2. 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。再屏蔽解除前,信号一直处于未决状态。

本质 ---- 位图。用来记录信号的处理状态。该信号集中的信号,表示已经产生,但尚未被处理

1.3 信号四要素和常规信号

kill -l:查看当前系统中的常规信号

kill-l.png

1 - 31 是常规信号,每个信号都有对应的默认事件和处理动作

34 - 64 是实时信号,没有对应的默认事件和处理动作

信号编号

信号名称

描述

1

SIGHUP

终端挂起或控制进程终止

2

SIGINT

中断信号,通常由终端上的Ctrl+C生成

3

SIGQUIT

退出信号,通常由终端上的Ctrl+\生成

4

SIGILL

非法指令信号

5

SIGTRAP

跟踪/断点陷阱信号

6

SIGABRT

中止信号,由调用abort函数生成

7

SIGBUS

总线错误信号

8

SIGFPE

浮点异常信号

9

SIGKILL

强制终止信号,无法被捕获或忽略

10

SIGUSR1

用户自定义信号1

11

SIGSEGV

段错误信号

12

SIGUSR2

用户自定义信号2

13

SIGPIPE

管道破裂信号

14

SIGALRM

定时器信号,由调用alarmsetitimer函数生成

15

SIGTERM

终止信号,通常用于请求进程正常终止

16

SIGSTKFLT

协处理器栈错误信号

17

SIGCHLD

子进程状态改变信号

18

SIGCONT

继续信号,用于恢复被停止的进程

19

SIGSTOP

停止信号,用于暂停进程的执行

20

SIGTSTP

终端停止信号,通常由终端上的Ctrl+Z生成

21

SIGTTIN

后台进程请求输入信号

22

SIGTTOU

后台进程请求输出信号

23

SIGURG

紧急条件信号

24

SIGXCPU

超过CPU时间限制信号

25

SIGXFSZ

超过文件大小限制信号

26

SIGVTALRM

虚拟定时器信号,由调用setitimer函数生成

27

SIGPROF

用于性能分析的定时器信号

28

SIGWINCH

窗口大小改变信号

29

SIGIO

异步I/O事件信号

30

SIGPWR

电源故障信号

31

SIGSYS

非法系统调用信号

信号四要素:

  • 编号

  • 名称

  • 对应时间

  • 默认处理动作

信号使用之前,应先确定其四要素,然后再使用!!!

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

  • 失败:-1errono

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命令查看程序执行时间

结果: 实际时间 = 用户时间 + 内核时间 + 等待时间

image-20231017151715260.png

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;
}

结果:

image-20231017200859772.png

在输入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:表示待捕捉的信号编号。可以是预定义的信号常量(如 SIGINTSIGTERM 等),也可以是自定义的信号编号。

  • 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;
}

结果:

image-20231020113852422.png

4.2 sigaction函数

注册一个信号捕捉函数

4.2.1 函数原型
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:

  • signum:表示待捕捉的信号编号。可以是预定义的信号常量(如 SIGINTSIGTERM 等),也可以是自定义的信号编号。

  • 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

  • 失败:-1errno

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;
}

结果:

image-20231020150938753.png

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 回收多个子进程
  1. 问题

    • 一次回调只回收一个子进程,同时出现多个子进程死亡时,只会回收累积信号中的一个子进程。

    • 有可能父进程还没注册完捕捉函数,子进程就死亡了

  2. 解决方法

    • 首先是让子进程sleep,但这个不太科学。在fork之前注册也行,但这个也不是很科学。

    • 最科学的方法是在int i之前设置屏蔽,等父进程注册完捕捉函数再解除屏蔽。这样即使子进程先死亡了,信号也因为被屏蔽而无法到达父进程。解除屏蔽过后,父进程就能处理累积起来的信号了。

  3. 代码

#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;
}

结果:

image-20231020163911681.png

4.5 慢速系统调用

  1. 可能会使进程永久阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期),也可以设定系统调用是否重启。如read, write, pause

  2. 其他系统调用:getpidgetppidfork...

可以修改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 进程和进程组和会话的关系

afd541ce4bf44ef72f19fe7648045184

进程组:(别名:作业)

  • 多个进程的集合,每个进程都属于一个进程组,简化对多个进程的管理,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

  • 失败:-1errno

5.1.3 setsid函数

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

pid_t setsid(void);  

返回值:

  • 成功:调用进程的会话id

  • 失败:-1errno

5.2 守护进程

daemon(精灵)进程。是Linux中的后台服务进程,脱离控制终端。一般不与用户直接交互。周期性地执行某种任务或等待处理某些发生的事件。

不受用户登录注销影响。通常采用以d结尾的命名方式。

如:ftp服务器,nfs服务器等。

5.2.1 创建守护进程

如何创建守护进程:

调用setsid函数创建一个新的Session,并成为Session Leader

步骤:

  1. fork子进程,让父进程终止。

  2. 子进程调用 setsid() 创建新会话

  3. 通常根据需要,使用chdir()函数改变工作目录位置 , 防止目录被卸载。

    int chdir(const char *path);
  4. 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限。 022 -- 755 0345 --- 432 r---wx-w- 422

  5. 通常根据需要,关闭/重定向 文件描述符

ec50e06e0f8064ebf05e5e72f17b2ed0.session_

  1. 守护进程 业务逻辑。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;
}

结果:

image-20231022225036940.png

查看进程列表如下:

ps -aux

image-20231022225212371.png

ps -ajx

image-20231022225254457.png

这个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:线程所属用户的ID

  • PID:进程的ID(即主线程的ID)

  • PPID:父进程的ID

  • LWP:线程号

  • 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 的页表结构可以根据系统的需求进行调整,三级映射只是其中一种常见的结构。在一些系统中,可能会使用更多或更少级别的页表来管理虚拟内存。这些结构的选择取决于硬件架构、内存需求和性能考虑等因素。

3e29cf084b998f778a746af3afcc10a5sanjiyingshe

6.1.3 线程共享和非共享
  1. <font color=blue>线程共享资源:</font>

  • 文件描述符表

  • 每种信号的处理方式

  • 当前工作目录

  • 用户 ID 和组 ID

  • 内存地址空间(.text / .data / .bss / heap / 共享库)

  1. 线程非共享资源:

  • 线程 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:传出参数,表新创建的子线程 id

  • attr:线程属性。传 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;
}

结果:

image-20231023165745775.png

易错分析:

如果我们在给回调函数传参的时候用引用传递呢?

比如把:

// 主函数中:
(void *)i  ------->  (void *)&i
// 子进程回调函数中
int i = (int *)arg;  ------->   int i = *((int *)arg);

那么结果就会变成:

image-20231023170241667.png

错误原因在于,子线程如果用引用传递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 0sleep的方法。

6.4.2 pthread_join 回收

阻塞等待线程退出,获取线程退出状态; ------ 对应于进程中的 waitpid() 函数

int pthread_join(pthread_t thread, void **retval);	

参数:

  • thread: 待回收的线程 id

  • retval:传出参数。 回收的那个线程的退出值。

  • 线程异常结束值为 -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;
}

结果:

image-20231023195835715.png

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;
}

结果:

image-20231023210056503.png

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;
}

结果:

image-20231023211448160.png

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去回收已经被系统回收的线程,那个线程号就是无效参数。

image-20231023214315536.png

6.6 线程和进程的控制语句对比

行为

线程

进程

创建

pthread_create()

fork()

获取 id

pthread_self()

getpid()

退出

pthread_exit()

exit();

回收

pthread_join()

wait()/waitpid()

杀死

pthread_cancel()

kill()

分离

pthread_detach()

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函数阻塞回收线程会失败。

image-20231023221746996.png

6.8 线程使用注意事项

  • 主线程退出其他线程不退出,主线程应该调用pthread_exit

  • 避免僵尸线程

    • pthread_join

    • pthread_detach

    • pthread_create指定分离属性

    • join线程可能在join函数返回前就释放自己的所有内存资源,所以不应当返回被回收线程栈中的值

  • mallocmmap申请的内存可以被其他线程释放

  • 应避免在多线程中调用fork,除非马上exec,子线程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit

  • 信号的复杂语义很难和多线程共存,在多线程中避免使用信号机制

7- 锁


7.1 线程同步---锁的使用注意事项

对公告区域的数据按序访问,防止数据混乱,产生于时间有关的错误。

数据混乱的原因:

  • 资源共享(独享资源则不会)

  • 调度随机(意味着数据访问会出现竞争)

  • 线程间缺乏必要同步机制

bank_eg.png

锁可以对公共数据进行保护。所有线程应该在访问公共数据之前先拿锁再访问。但是锁本身不具备强制性,线程直接访问数据也是可以成功的。

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;
}

结果:

image-20231024102828993.png

主线程和子线程交叉输出,并且数据混乱。

使用互斥锁

#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;
}

结果:

image-20231024110121590.png

达到了预期效果。

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;
}

结果:核心:读共享,写独占,写锁优先级高

image-20231024210843925.png

7.3.4 时间粒度对于读写锁的影响

时间粒度 指的是时间的最小单位,它决定了你可以设置的最小延时或时间间隔。

  • 时间粒度较大,例如延时或时间间隔设置为较大的值,那么读写线程之间的竞争可能会减少,写线程更容易获得写锁。因为读线程需要等待一段较长的时间才能再次获取读锁,给写线程更多的机会获得写锁。

  • 时间粒度较小,例如延时或时间间隔设置为较小的值,那么读写线程之间的竞争可能会增加,写线程更难获得写锁。因为读线程可以更频繁地获取读锁,减少了写线程获得写锁的机会。

在程序中需要设置合理的时间粒度,平衡读写线程之间的竞争,提高程序的性能和响应能力。

在上述例子中,分别使用 sleep() 加大时间粒度和使用 nanosleep 减小时间粒度,对比得到如下结果:

  • 使用 sleep() 函数统一延时1秒

    读写线程基本上是机会均等的

image-20231024211653894.png

  • 使用 nanosleep 函数

读线程会大量获得CPU资源,而且是一个线程会连任的状态。

image-20231024212253046.png

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);

8d00cbd857bbef7420a4504cfb5e41f9.wiat

8.1.4 示例

生产者生产数据存储到公共区域,消费者在公共区域消费这些数据,利用pthread_cond_wait 阻塞消费者。

a912f1da9e2f84f3f30a16e5cf5b1731.shengchanxiaofeimoxing_

/*借助条件变量模拟 生产者 消费者 问题*/
#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;
}

结果:

image-20231027111451119.png

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;
}

结果:

image-20231027161045562.png

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;
}

结果: