i007.cc

i007.cc

优先队列-降维打击

malloc的线程安全与signal使用malloc的陷阱(内核态与用户态、系统调用与C库调用、可重入与不可重入)

前言:为什么写这篇博客?

原因:为什么malloc本身是线程安全函数(man手册也说明了),但是signal中使用却会产生错误与陷阱,线程是调用malloc并执行,signal也是执行malloc,可是为什么是不安全的?

背景知识:这4个部分留给初学者自己查资料,内容太多,这里简单叙述下,这5块搞清楚就能理解了,而且对于linux学习这几部分是必不可少的,现在可能感觉用不上,等真正做项目你才会发现这些概念有多重要。

(1)内核态与用户态

(2)系统调用与C库调用

(3)可重入与不可重入

(4)内核对信号的处理方法

(5)系统调用函数内部的锁

(6)同步与异步

1、系统调用函数的线程安全与可重入性

(1)在man手册中,与系统调用有关的函数都会说明该函数是否线程安全,所以这也是我们写代码需要关注的,而线程安全与函数是否可重入有很大关系,函数可重入一定是线程安全的,线程安全不一定是可重入函数,比如maloc使用递归锁实现了线程安全,但它是不可重入函数,所以不可重入函数可以通过内核锁实现线程安全(锁是系统调用,所以工作在内核态,也叫内核锁),还有很多函数也是这样实现线程安全的。

2、内核处理信号内函数机制与线程处理函数机制的区别

2.1 信号

信号详细介绍该博客https://blog.csdn.net/edonlii/article/details/8472382

1、基本概念

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。

收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。

2、内核对信号的基本处理方法(重点理解这部分,不懂的去百度,直到理解)

(1)内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理,比如线程中系统调用malloc申请内存,所以从用户态进入了内核态,现在信号发生,这时操作系统会从内核态跳转到用户态执行signal函数(signal函数本身是应用层的一行代码需要被运行,属于用户态,当执行到绑定的函数时,函数内部有系统调用函数,然后进入内核态)。进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数,而且执行用户定义的函数的方法很巧妙(这个函数时signal函数绑定的那个处理信号函数,比如里面出现malloc系统调用),内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方(线程中调用malloc所在内核执行处)。这样做的原因是用户定义的处理函数不应该在内核态下执行,所以一般在信号处理函数中,最好仅仅用来打印一条信息,然后使用longjmp或者exit退出。

综上所述,如果信号被捕获,执行点从内核态返回用户态,在返回时,如果发现待执行进程存在被触发的signal,那么在离开内核态之后(也就是将CPU切换到用户模式),执行用户进程为该signal绑定的signal处理函数,从这一点上看,signal处理函数是在用户进程上下文中执行的。当执行完signal处理函数之后,再返回到用户进程被中断或者system call(软中断或者指令陷阱)打断的地方。

(2)如果signal处理函数使用系统调用,比如malloc,free

信号处理函数中只能调用可重入函数,而不能调用不可重入函数。进程捕捉到信号并对其进行处理时,正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理函数中的指令。如果从信号处理程序返回,则继续执行在捕捉到信号时正在执行的正常指令序列(这类似于发生硬件中断时所做的)。但在信号处理函数中,不能判断捕捉到信号时线程执行到何处。

信号处理函数默认情况下是在进程的主线程调用的,这种情况下使用不可重入函数,有可能会造成不可预知的错误。比如调用了malloc函数,为了保证malloc是线程安全的,所以内部使用了锁,根据malloc中锁的不同处理方式,分别可能会导致以下情况的发生:

1)  如果是普通锁,在主线程中malloc函数获取锁之后被signal中断,在signal处理函数中继续调用malloc,因为主线程中的malloc已经获取到了锁,signal处理函数只能等待锁释放,而主线程中的malloc函数正在等待signal处理函数返回后继续执行,这样就造成了锁死;

2) 如果是递归锁,那么signal处理函数中的malloc函数获取锁后进行内存分配,因为上次的malloc操作还没完,可能成会造成内存数据混乱。

就定时而言,可不直接使用singal alarm,而使用posix定时器,通过通知线程的方式,将定时处理函数放到单独的线程中来处理。

3、内核线程调度系统对malloc的处理方法

多线程之前使用malloc是安全的,虽然它不可重入,但是用锁实现了

1)  如果是普通锁,A线程获取堆栈锁后,B线程必须等待A线程执行完成后,释放锁,然后B线程malloc才能申请内存。

2)  如果是递归锁,内核调度系统在线程之间调度时,如果线程A的malloc函数一旦开始了申请,它就不会交出CPU,而是等malloc完成后,才会根据情况是否交出CPU,如果没有特别重要的处理,调度器就会跳转到线程B中执行,如果B线程执行到malloc,同理。(调度器如何工作等我搞明白再修正这里的介绍,这里第二点只是猜测)

3、同步与异步(Sync与Async)

同步:简单来说,同步就是必须一件一件事做,等前一件做完了才能做下一件事。就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。

异步:异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

在许多C语言实现中,信号是真正意义上的“异步”。从理论上说,一个信号可能在C程序执行期间的任何时刻上发生。

发表回复