由localtime引发的函数可重入问题

本文探讨了C/C++中time和localtime函数的工作原理及其线程安全性问题,并介绍了如何编写可重入函数,避免数据竞争导致的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、先看一个例子

#include<time.h>
#include <windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
	time_t now;
	struct tm *tmStart, *tmEnd;
	if(time(&now)==-1)//函数time返回月历值(格式 struct tm)。若参数是0,函数返回该值,否则将转换的结果保存在参数中。失败返回-1
		return -1;
	tmStart = localtime(&now);//把从1970-1-1零点零分到当前时间系统所偏移的秒数时间,转换为本地tm结构体的日历时间
	Sleep(7);//linux下Sleep改为sleep
	if(time(&now)==-1)
		return -1;
	tmEnd = localtime(&now);
	//按照预先的想法,tmStart比tmEnd的时间近似1897秒,可是结果并不是我们想要的!!!
	printf("tmStart时间为:\t%s", asctime(tmStart));
	printf("tmEnd时间为:\t%s", asctime(tmEnd));
	getchar();
	return 0;
}
结果是:

(1)在windows、visual studio2010中的结果


(2)在linux、eclipse下的测试结果:


在linux下man localtime截取到的内容是:


       
注意到:
 (1)The  four  functions  asctime(),  ctime(),  gmtime() and localtime() return a pointer to static data and   hence are not thread-safe.
  这几个函数非线程安全的,线程安全的版本是在各函数后面加_r;
 (2)The asctime(), ctime(), gmtime(), and localtime() functions shall return values in   one of two static objects: a broken-down time structure 
     and an array of type char.即返回值保存在静态对象中。
二、不可重入函数
从以上例子的现象可以看出,tmStart指向的内容在后面的localtime中被改变了。也就是使用这种方法,前面的时间丢失了。
同样的问题若出在多线程中程序,自然出现不可预料的结果。
在实时系统中不可重入函数被认为是不安全的函数。
满足下列条件的函数多数是不可重入的:
(1) 函数体内使用了静态的数据结构;
(2)函数体内调用了malloc()或者free()函数;
(3)函数体内调用了标准I/O函数。
 对于这种函数在处理时需要注意:当然最好使用线程安全的函数!
(1)对于这个例子,可以把tmStart的结果拷贝出来。使的该指针不在与localtime关联。
         如:struct tm tmStart_pre;
                memcpy(&tmStart_pre,tmStart,sizeof(struct tm));

(2)通过关中断、信号量(即P、V操作)等手段对其加以保护。

三、编写可重入的函数
(1)受保护的全局变量和静态变量
(2)尽量使用局部变量(例如寄存器、堆栈中的变量);
(3) 在和硬件发生交互的时候,切记执行类似disinterrupt()之类的操作,就是关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做“进入/退出核心”。
(4) 不能调用其它任何不可重入的函数。
(5) 谨慎使用堆栈。

四、与之类似的函数inet_ntoa

void main()
{
	struct in_addr b1,b2;
	b1.s_addr = inet_addr("192.168.1.5");//将一个点分十进制的IP转换成一个长整数型数
	b2.s_addr = inet_addr("192.168.1.87");
	char*s1 = inet_ntoa(b1);//将一个in_addr格式的IP转换成一个互联网标准点分格式的字符串
	char*s2 = inet_ntoa(b2);
	//当上两句执行完毕之后,s1和s2指向相同的地址,这跟localtime类似
}
man inet_ntoa可以看到对它的解释:
The  inet_ntoa() function converts the Internet host address in, given in network byte order, to a string in IPv4 dotted-decimal notation.  The string is returned in a stati-cally allocated buffer, which subsequent calls will overwrite.



<think>好的,用户现在问的是“什么情况下的函数不能重入”。首先,我需要回忆一下可重入函数和不可重入函数的基本概念。可重入函数是指在多线程或递归调用中,能够安全地被同时调用函数,不会因为共享数据或状态而导致问题。相反,不可重入函数则在这样的环境下可能引发数据不一致、崩溃或其他不可预知的行为。 接下来,我需要确定用户为什么会问这个问题。可能的场景包括用户正在学习多线程编程,遇到了线程安全问题,或者在调试时发现了某些函数调用导致的问题,想知道原因。用户可能是开发者,尤其是嵌入式系统或应用层多线程程序的开发者,因为在这些领域,函数的重入性是一个常见问题。 用户的需求可能不仅仅是了解不可重入的情况,还可能想知道如何避免或修复这些问题。因此,在回答时不仅要列出情况,还需要给出例子和解决方案,甚至比较不同方法的优缺点。 首先,我需要整理不可重入函数的常见情况。根据知识,不可重入函数通常涉及以下几个方面: 1. **使用静态或全局变量**:这类变量在多次调用中保持其值,如果在函数中被修改,可能导致数据竞争或不一致。 2. **调用可重入函数**:如果函数内部调用了其他不可重入函数,比如某些标准库函数,那么该函数本身也不可重入。 3. **操作共享资源未加锁**:如文件、硬件设备等,多个线程同时访问会导致问题。 4. **返回指向静态数据的指针**:比如某些函数返回静态缓冲区,多次调用会覆盖之前的结果。 5. **修改自身代码(自修改代码)**:虽然少见,但在某些低级编程中可能存在,导致并发执行时出错。 然后,每个点需要详细解释,给出例子。例如,静态变量的使用,可以举strtok的例子,因为它内部使用静态指针来维护分词的位置。在多线程环境中,同时使用strtok会导致混乱。 另外,需要区分可重入函数和线程安全函数。虽然相关,但并不是完全相同的概念。可重入函数强调在单线程多任务(如信号处理函数)中的安全性,而线程安全函数则涉及多线程环境。但两者有重叠部分,比如使用互斥锁的线程安全函数可能不是可重入的,因为锁可能导致死锁。 用户可能存在的深层需求是希望确保自己写的函数可重入的,或者在多线程环境中正确使用函数。因此,除了列举情况,还需要提供解决方案,比如使用局部变量、线程局部存储、互斥锁等,并指出各自的适用场景和限制。 还要注意一些常见的误区,例如认为所有使用静态变量的函数都是不可重入的,但实际上如果静态变量是只读的,或者每次调用都重新初始化,则可能不会导致问题。需要明确说明只有在静态变量被读写且状态持续的情况下才会引发问题。 另外,可能用户没有意识到某些标准库函数是不可重入的,比如printf使用全局的IO缓冲区,如果在信号处理函数中使用printf,可能导致死锁或其他问题。这时需要建议使用异步信号安全的函数。 最后,总结如何避免编写不可重入函数,以及如何将现有函数改造为可重入的。可能需要提到可重入函数的设计原则,如避免全局状态,使用参数传递数据,依赖调用者管理状态等。 在组织回答时,结构要清晰,分点列出原因,每个原因下给出例子和解决方法。这样用户能快速定位问题并找到解决方案。同时,语言要简洁,避免过于技术化的术语,用通俗易懂的中文解释概念。</think>### **不可重入函数(Non-Reentrant Function)的常见场景** 不可重入函数在多线程、递归或信号处理等场景中调用时,可能因共享状态或资源冲突导致数据不一致、崩溃等问题。以下是不可重入的典型情况: --- #### **1. 依赖静态变量或全局变量** - **问题**: 静态变量(`static`)或全局变量在多次调用间保留状态,若函数修改这些变量,并发调用时会互相干扰。 - **示例**: ```c int counter() { static int count = 0; // 静态变量 return ++count; } ``` 若两个线程同时调用 `counter()`,`count` 的递增可能不同步,导致结果错误。 - **解决方案**: 改用局部变量或通过参数传递状态(需调用者管理状态)。 --- #### **2. 调用其他不可重入函数** - **问题**: 若函数内部调用了不可重入函数(如某些标准库函数),自身也会不可重入。 - **示例**: ```c void log_message(const char* msg) { // printf 内部使用全局IO缓冲区,不可重入 printf("[LOG] %s\n", msg); } ``` 多线程同时调用 `log_message` 可能导致输出混乱或死锁。 - **解决方案**: 使用线程安全的替代函数(如 `printf` 的线程安全版本 `printf_r`),或对共享资源加锁。 --- #### **3. 操作共享资源未加锁** - **问题**: 函数访问文件、硬件寄存器、数据库连接等共享资源时,未通过锁机制保护,导致竞态条件。 - **示例**: ```c void write_to_file(const char* data) { FILE* fp = fopen("log.txt", "a"); fputs(data, fp); // 多线程同时写入可能破坏文件内容 fclose(fp); } ``` - **解决方案**: 使用互斥锁(如 `pthread_mutex`)或原子操作保护临界区。 --- #### **4. 返回指向静态缓冲区的指针** - **问题**: 某些函数返回静态分配的缓冲区地址,多次调用会覆盖之前的结果。 - **示例**: ```c char* get_timestamp() { static char buffer[64]; time_t now = time(NULL); strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", localtime(&now)); return buffer; // 多次调用返回同一内存地址 } ``` 若线程A读取 `get_timestamp()` 后,线程B调用函数,线程A的缓冲区内容会被覆盖。 - **解决方案**: 要求调用者提供缓冲区,或返回动态分配的内存(需调用者释放)。 --- #### **5. 修改自身代码(自修改代码)** - **问题**: 极少数函数可能在运行时修改自身代码(如某些低级优化或动态补丁),导致并发执行时代码不一致。 - **示例**: 汇编或内核级代码中直接修改指令指针(如热更新逻辑)。 - **解决方案**: 避免自修改代码,或使用原子操作确保代码段替换的完整性。 --- ### **如何编写可重入函数?** 1. **避免全局/静态变量**:用局部变量或参数传递状态。 2. **使用线程局部存储(TLS)**:如 `thread_local`(C11)或 `__thread`(GCC扩展)。 3. **隔离共享资源**:通过锁、信号量或消息队列同步访问。 4. **选择可重入函数**:如用 `strtok_r` 替代 `strtok`,`asctime_r` 替代 `asctime`。 --- ### **不可重入函数的典型例子** | **不可重入函数** | **可重入替代方案** | **原因** | |------------------------|--------------------------|---------------------------| | `strtok` | `strtok_r` | 内部使用静态指针保存状态 | | `gmtime`, `localtime` | `gmtime_r`, `localtime_r`| 返回静态结构体指针 | | `rand` | `rand_r` | 依赖全局种子 | | `printf`, `scanf` | 加锁或使用异步信号安全函数 | 共享标准IO缓冲区 | --- ### **总结** 不可重入函数的本质是 **依赖共享状态或资源**。 - **关键影响场景**:多线程、递归、信号处理函数、中断服务程序(ISR)。 - **设计原则**: - 无状态化(Stateless):通过参数传递所有依赖。 - 隔离性(Isolation):每个调用实例独立操作资源。 - 同步性(Synchronization):强制串行访问共享资源。 在并发编程中,优先选择可重入函数或通过同步机制封装不可重入函数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值