锁机制是多线程编程中最常用的同步机制,用来对多线程间共享的临界区进行保护。
1. 互斥锁:pthread_mutex,属于sleep-waiting类型的锁
pthread_mutex_t *mutex;
int pthread_mutex_int(mutex, attr) //以动态方式创建互斥锁,参数attr指定了新建互斥锁mutex的属性
int pthread_mutex_lock(mutex) //加锁
int pthread_mutex_unlock(mutex) //解锁
locak_irq_disable()、local_irq_enable()、local_irq_save()和local_irq_restore()为中断处理函数,主要是在要进入临界时禁止中断加在出临界区时使能中断。
locak_irq_disable()、local_irq_enable()配对使用,而local_irq_save()和local_irq_restore()配对使用。
locak_irq_disable()和local_irq_save()都可以禁止中断,但不同的是后者可以保存中断状态
local_irq_restore()在使能中断的同时还恢复了由local_irq_save()所保存的中断状态
2. 自旋锁:pin lock,属于busy-wait类型的锁
自旋锁与互斥锁的区别:自旋锁不会引起调用者睡眠,如果自旋锁已经被另的执行单元保持,调用者就一直循环在那里,看是否该自旋锁的保持者已经释放了锁,“自旋”一词而就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。
自旋锁的不足之处:
(1) 自旋锁一直占用CPU,它在未获得锁的情况下,一直运行-自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低
(2) 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数时也可能造成死锁,如copy_to_user()、copy_from_user()、kmalloc()等
因此要慎用自旋锁,自旋锁有在内核可抢占式或SMP的情况下才真正需要。
关于pthread_mutex_t的初始化
class MyLock
{
private:
pthread_mutex_t m_lock;
public:
MyLock()
{
// m_lock = PTHREAD_MUTEX_INITIALIZER; //使用这种方法初始化时GCC编译无法通过
pthread_mutex_init(&m_lock,NULL);
}
~MyLock()
{
}
void Lock()
{
pthread_mutex_lock(&m_lock);
}
void unLock()
{
pthread_mutex_unlock(&m_lock);
}
};
Unix网络编程卷一有讲:在静态分配时,比如全局变量,我们必须将它初始化为PTHREAD_MUTEX_INITIALIZER,而如果在共享内存中分配时,必须用pthread_mutex_init来初始化。
在pthread.h头文件中PTHREAD_MUTEX_INITIALIZER是这样定义的:
# define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, 0, { 0, 0 } } }
如果不对其进行初始化,对于某些系统来说如Solaris,静态分配就是初始化为零,所以没什么问题。但并非所有的系统都是这样的,如Digtal Unix将初始化常值定义为非0。
Linux/Unix编程中的线程安全问题
在目前的计算机科学中,线程是操作系统调度的最小单元,进程是资源分配的最小单元。在大多数操作系统中,一个进程可以同时派生出多个线程。这些线程独立执行,共享进程的资源。在单处理器系统中,多线程通过分时复用技术来技术,处理器在不同的线程间切换,从而更高效地利用系统 CPU资源。在多处理器和多核系统中,线程实际上可以同时运行,每个处理器或者核可以运行一个线程,系统的运算能力相对于单线程或者单进程大幅增强。
多线程技术让多个处理器机器,多核机器和集群系统运行更快。因为多线程模型与生俱来的优势可以使这些机器或者系统实现真实地的并发执行。但多线程在带来便利的同时,也引入一些问题。线程主要由控制流程和资源使用两部分构成,因此一个不得不面对的问题就是对共享资源的访问。为了确保资源得到正确的使用,开发人员在设计编写程序时需要考虑避免竞争条件和死锁,需要更多地考虑使用线程互斥变量。
线程安全 (Thread-safe) 的函数就是一个在代码层面解决上述问题比较好的方法,也成为多线程编程中的一个关键技术。如果在多线程并发执行的情况下,一个函数可以安全地被多个线程并发调用,可以说这个函数是线程安全的。反之,则称之为“非线程安全”函数。注意:在单线程环境下,没有“线程安全”和“非线程安全”的概念。因此,一个线程安全的函数允许任意地被任意的线程调用,程序开发人员可以把主要的精力在自己的程序逻辑上,在调用时不需要考虑锁和资源访问控制,这在很大程度上会降低软件的死锁故障和资源并发访问冲突的机率。所以,开发人员应尽可能编写和调用线程安全函数。
判断一个函数是否线程安全不是一件很容易的事情。但是读者可以通过下面这几条确定一个函数是线程不安全的。
- a, 函数中访问全局变量和堆。
- b, 函数中分配,重新分配释放全局资源。
- c, 函数中通过句柄和指针的不直接访问。
- d, 函数中使用了其他线程不安全的函数或者变量。
因此在编写线程安全函数时,要注意两点:
- 1, 减少对临界资源的依赖,尽量避免访问全局变量,静态变量或其它共享资源,如果必须要使用共享资源,所有使用到的地方必须要进行互斥锁 (Mutex) 保护;
- 2, 线程安全的函数所调用到的函数也应该是线程安全的,如果所调用的函数不是线程安全的,那么这些函数也必须被互斥锁 (Mutex) 保护;
举个例子(参考 例子 1),下面的这个函数 sum()是线程安全的,因为函数不依赖任何全局变量。
int sum(int i, int j) {
return (i+j);
}
但如果按下面的方法修改,sum()就不再是线程安全的,因为它调用的函数 inc_sum_counter()不是线程安全的,该函数访问了未加锁保护的全局变量 sum_invoke_counter。这样的代码在单线程环境下不会有任何问题,但如果调用者是在多线程环境中,因为 sum()有可能被并发调用,所以全局变量 sum_invoke_counter很有可能被并发修改,从而导致计数出错。
static int sum_invoke_counter = 0;
void inc_sum_counter(int i, int j) {
sum_invoke_counter++;
}
int sum(int i, int j) {
inc_sum_counter();
return (i+j);
}
我们可通过对全局变量 sum_invoke_counter添加锁保护,使得 inc_sum_counter()成为一个线程安全的函数。
static int sum_invoke_counter = 0;
static pthread_mutex_t sum_invoke_counter_lock = PTHREAD_MUTEX_INITIALIZER;
void inc_sum_counter(int i, int j) {
pthread_mutex_lock( &sum_invoke_counter_lock );
sum_invoke_counter++;
pthread_mutex_unlock( &sum_invoke_counter_lock );
}
int sum(int i, int j) {
inc_sum_counter();
return (i+j);
}
现在 , sum()和 inc_sum_counter()都成为了线程安全函数。在多线程环境下,sum()可以被并发的调用,但所有访问inc_sum_counter()线程都会在互斥锁 sum_invoke_counter_lock上排队,任何一个时刻都只允许一个线程修改sum_invoke_counter,所以 inc_sum_counter()就是现成安全的。
除了线程安全还有一个很重要的概念就是 可重入(Re-entrant),所谓可重入,即:当一个函数在被一个线程调用时,可以允许被其他线程再调用。显而易见,如果一个函数是可重入的,那么它肯定是线程安全的。但反之未然,一个函数是线程安全的,却未必是可重入的。程序开发人员应该尽量编写可重入的函数。
一个函数想要成为可重入的函数,必须满足下列要求:
- a) 不能使用静态或者全局的非常量数据
- b) 不能够返回地址给静态或者全局的非常量数据
- c) 函数使用的数据由调用者提供
- d) 不能够依赖于单一资源的锁
- e) 不能够调用非可重入的函数
对比前面的要求,例子 1的 sum()函数是可重入的,因此也是线程安全的。例子 3中的 inc_sum_counter()函数虽然是线程安全的,但是由于使用了静态变量和锁,所以它是不可重入的。因为 例子 3中的 sum()使用了不可重入函数inc_sum_counter(), 它也是不可重入的。
如果把一个非线程安全的函数作为线程安全对待,那么结果可能是无法预料的,例如 下面这段代码是对 basename()的错误用法:
#include <unistd.h>
#include <stdio.h>
#include <stdarg.h>
#include <pthread.h>
#include <string.h>
#include <libgen.h>
void printf_sa(char *fmt, ...) {
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
va_list args;
va_start(args, fmt);
pthread_mutex_lock(&lock);
vprintf(fmt, args);
pthread_mutex_unlock(&lock);
va_end(args);
}
void* basename_test(void *arg) {
pthread_t self = pthread_self();
char *base = basename((char*)arg);
printf_sa("TI-%u: base: %s/n", self, base);
}
int main(int argc, char *argv) {
int i = 0;
pthread_t tids[2];
char msg[1024];
strcpy(msg, "/tmp/test");
pthread_create(&tids[0], NULL, basename_test, msg);
msg[7] -= 32;
pthread_create(&tids[1], NULL, basename_test, msg);
pthread_join(tids[0], NULL);
pthread_join(tids[1], NULL);
return 0;
}
这段代码的意思是在两个并发的线程中同时执行函数 basename(),然后打印出对于路径 "/tmp/test"和"/tmp/teSt"的结果。
编译 ( 注意:如编译器提示 pthread_create函数不能找到可能需要连接库 pthread,请需添加 -lpthread选项 ) 执行这段代码,你会发现大部分情况下屏幕上都会打印类似出如下结果 :
TI-3086846864: base: teSt
TI-3076357008: base: teSt
实际上我们期待的值应该 :
TI-3086846864: base: test
TI-3076357008: base: teSt
虽然只是一个字母的差别,但这其实涉及到函数 basename()的线程安全特征。造成这个问题的原因是函数 basename()的返回值指向了输入字符串的一个片段,所以当输入字符串发生变化以后,basename()的返回值也发生了变化。
因为 basename()的函数声明只提供了一个参数,所以该函数不得不通过修改输入参数或使用静态变量的方式来将结果返回给用户。
参考 Linux帮助手册,dirname()和 basename()可能会修改传入的参数字符串,所以调用者应该传入参数字符串的拷贝。并且,这两个函数的返回值可能指向静态分配的内存,所以其返回值的内容有可能被随后的 dirname()或 basename()调用修改。
因为多线程技术和线程安全概念出现得相对较晚,所以 POSIX规范中收纳的一些函数并不符合线程安全要求。
下表是 UNIX环境高级编程列出 POSIX.1规范中的非线程安全的函数:
asctime ecvt gethostent getutxline putc_unlocked
basename encrypt getlogin gmtime putchar_unlocked
catgets endgrent getnetbyaddr hcreate putenv
crypt endpwent getnetbyname hdestroy pututxline
ctime endutxent getopt hsearch rand
dbm_clearerr fcvt getprotobyname inet_ntoa readdir
dbm_close ftw getprotobynumber L64a setenv
dbm_delete getcvt getprotobynumber lgamma setgrent
dbm_error getc_unlocked getprotoent lgammaf setkey
dbm_fetch getchar_unlocked getpwent lgammal setpwent
dbm_firstkey getdate getpwnam localeconv setutxent
dbm_nextkey getenv getpwuid lrand48 strerror
dbm_open getgrent getservbyname mrand48 strtok
dbm_store getgrgid getservbyport nftw ttyname
dirname getgrnam getservent nl_langinfo unsetenv
dlerror gethostbyaddr getutxent ptsname wcstombs
drand48 gethostbyname getutxid ptsname ectomb
目前大部分上述函数目前已经有了对应的线程安全版本的实现,例如:针对 getpwnam的 getpwnam_r(),( 这里的 _r表示可重入 (reentrant),如前所述,可重入的函数都是线程安全的)。在多线程软件开发中,如果需要使用到上所述函数,应优先使用它们对应的线程安全版本。而对于某些没有线程安全版本的函数,开发人员可按自己需要编写线程安全版本的实现。
在编写自己的线程安全版本函数之前,应首先仔细阅读 POSIX标准对函数的定义,以及通过充分的测试熟悉函数的输入和输出。理论上来说,所有的线程安全的版本函数应该与非线程安全版本函数在单线程环境下表现一致。
这里给出一个针对 basename()的线程安全版本的例子。
在熟悉了 basename() 函数的功能之后,下面是一个线程安全版本的实现。
/* thread-safe version of basename() */
char* basename_ta(char *path, char *buf, int buflen) {
#define DEFAULT_RESULT_DOT "."
#define DEFAULT_RESULT_SLASH "/"
/* 如果输入的路径长度小于 PATH_MAX,
* 则使用自动变量 i_fixed_bufer 作为内部缓冲区 ,
* 否则申请堆内存做为字符串存放缓冲区。
*/
char i_fixed_buf[PATH_MAX+1];
const int i_fixed_buf_len = sizeof(i_fixed_buf)/sizeof(char);
char *result = buf;
char *i_buf = NULL;
int i_buf_len = 0;
int adjusted_path_len = 0;
int path_len = 0;
int i, j;
char tmp = 0;
if (path == NULL) {
/* 如果输入为空指针,则直接返回当前目录 */
path = DEFAULT_RESULT_DOT;
}
/* 分配内部缓冲区用来存放输入字符串 */
path_len = strlen(path);
if ((path_len + 1) > i_fixed_buf_len) {
i_buf_len = (path_len + 1);
i_buf = (char*) malloc(i_buf_len * sizeof(char));
} else {
i_buf_len = i_fixed_buf_len;
i_buf = i_fixed_buf;
}
/* 拷贝字符串到缓冲区,以便接下来对字符串做预处理 */
strcpy(i_buf, path);
adjusted_path_len = path_len;
/* 预处理:删除路径未的路径符号 '/'; */
if (adjusted_path_len > 1) {
while (i_buf[adjusted_path_len-1] == '/') {
if (adjusted_path_len != 1) {
adjusted_path_len--;
} else {
break;
}
}
i_buf[adjusted_path_len] = '/0';
}
/* 预处理:折叠最后出现的连续 '/'; */
if (adjusted_path_len > 1) {
for (i = (adjusted_path_len -1), j = 0; i >= 0; i--) {
if (j == 0) {
if (i_buf[i] == '/')
j = i;
} else {
if (i_buf[i] != '/') {
i++;
break;
}
}
}
if (j != 0 && i < j) {
/* 折叠多余的路径符号 '/';
*/
strcpy(i_buf+i, i_buf+j);
}
adjusted_path_len -= (j - i);
}
/* 预处理:寻找最后一个路径符号 '/' */
for (i = 0, j = -1; i < adjusted_path_len; i++) {
if (i_buf[i] == '/')
j = i;
}
/* 查找 basename */
if (j >= 0) {
/* found one '/' */
if (adjusted_path_len == 1) { /* 输入的是跟路径 ("/"),则返回根路径 */
if (2 > buflen) {
return NULL;
} else {
strcpy(result, DEFAULT_RESULT_SLASH);
}
} else {
if ((adjusted_path_len - j) > buflen) { /* 缓冲区不够,返回空指针 */
result = NULL;
} else {
strcpy(result, (i_buf+j+1));
}
}
} else {
/* no '/' found */
if (adjusted_path_len == 0) {
if (2 > buflen) { /* 如果传入的参数为空字符串 ("") */
return NULL; /* 直接返回当前目录 (".") */
} else {
strcpy(result, DEFAULT_RESULT_DOT);
}
} else {
if ((adjusted_path_len+1) > buflen) {
result = NULL; /* 缓冲区不够,返回空指针 */
} else {
strcpy(result, i_buf); /* 拷贝整个字符串做为返回值 */
}
}
}
if (i_buf_len != i_fixed_buf_len) { /* 释放缓冲区 */
free(i_buf);
i_buf = NULL;
}
return result;
}
这个线程安全版本的函数将处理结果存储在外部分配的内存中,所以函数内部并无对全局资源的再依赖。因此,这个函数可安全地被多个线程所使用。
Linux可重入函数与不可重入函数
2011-01-24 20:27 中国IT实验室 佚名
关键字:Linux
主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。
编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。
说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。
示例:假设Exam是int型全局变量,函数Squre_Exam返回Exam平方值。那么如下函数不具有可重入性。
unsigned int example( int para )
{
unsigned int temp;
Exam = para; // (**)
temp = Square_Exam( );
return temp;
}
此函数若被多个进程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使Exam赋与另一个不同的para值,所以当控制重新回到“temp = Square_Exam( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。
unsigned int example( int para ) {
unsigned int temp;
[申请信号量操作] //(1)
Exam = para;
temp = Square_Exam( );
[释放信号量操作]
return temp;
}
(1)若申请不到“信号量”,说明另外的进程正处于给Exam赋值并计算其平方过程中(即正在使用此信号),本进程必须等待其释放信号后,才可继续执行。若申请到信号,则可继续执行,但其它进程必须等待本进程释放信号量后,才能再使用本信号。
保证函数的可重入性的方法:
在写函数时候尽量使用局部变量(例如寄存器、堆栈中的变量),对于要使用的全局变量要加以保护(如采取关中断、信号量等方法),这样构成的函数就一定是一个可重入的函数。
VxWorks中采取的可重入的技术有:
* 动态堆栈变量(各子函数有自己独立的堆栈空间)
* 受保护的全局变量和静态变量
* 任务变量
--------------------------------------------------
在实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果这个函数不幸被设计成为不可重入的函数的话,那么不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。那么什么是可重入函数呢?所谓可重入函数是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。不可重入函数在实时系统设计中被视为不安全函数。满足下列条件的函数多数是不可重入的:
1) 函数体内使用了静态的数据结构;
2) 函数体内调用了malloc()或者free()函数;
3) 函数体内调用了标准I/O函数。
下面举例加以说明。
A. 可重入函数
void strcpy(char *lpszDest, char *lpszSrc)
{
while(*lpszDest++=*lpszSrc++);
*dest=0;
}
B. 不可重入函数1
charcTemp;//全局变量
void SwapChar1(char *lpcX, char *lpcY)
{
cTemp=*lpcX;
*lpcX=*lpcY;
lpcY=cTemp;//访问了全局变量
}
C. 不可重入函数2
void SwapChar2(char *lpcX,char *lpcY)
{
static char cTemp;//静态局部变量
cTemp=*lpcX;
*lpcX=*lpcY;
lpcY=cTemp;//使用了静态局部变量
}
问题1,如何编写可重入的函数?
答:在函数体内不访问那些全局变量,不使用静态局部变量,坚持只使用局部变量,写出的函数就将是可重入的。如果必须访问全局变量,记住利用互斥信号量来保护全局变量。
问题2,如何将一个不可重入的函数改写成可重入的函数?
答:把一个不可重入函数变成可重入的唯一方法是用可重入规则来重写它。其实很简单,只要遵守了几条很容易理解的规则,那么写出来的函数就是可重入的。
1) 不要使用全局变量。因为别的代码很可能覆盖这些变量值。
2) 在和硬件发生交互的时候,切记执行类似disinterrupt()之类的操作,就是关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做“进入/退出核心”。
3) 不能调用其它任何不可重入的函数。
4) 谨慎使用堆栈。最好先在使用前先OS_ENTER_KERNAL。
堆栈操作涉及内存分配,稍不留神就会造成益出导致覆盖其他任务的数据,所以,请谨慎使用堆栈!最好别用!很多黑客程序就利用了这一点以便系统执行非法代码从而轻松获得系统控制权。还有一些规则,总之,时刻记住一句话:保证中断是安全的!
实例问题:曾经设计过如下一个函数,在代码检视的时候被提醒有bug,因为这个函数是不可重入的,为什么?
unsigned int sum_int( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; // 注意,是static类型
for (index = 1; index <= base; index++)
sum += index;
return sum;
}
分析:所谓的函数是可重入的(也可以说是可预测的),即只要输入数据相同就应产生相同的输出。这个函数之所以是不可预测的,就是因为函数中使用了static变量,因为static变量的特征,这样的函数被称为:带“内部存储器”功能的的函数。因此如果需要一个可重入的函数,一定要避免函数中使用static变量,这种函数中的static变量,使用原则是,能不用尽量不用。
将上面的函数修改为可重入的函数,只要将声明sum变量中的static关键字去掉,变量sum即变为一个auto类型的变量,函数即变为一个可重入的函数。
当然,有些时候,在函数中是必须要使用static变量的,比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针。