mktime函数性能分析

mktime是一个将break-down时间(struct tm)转化为日历时间(time_t)的转换函数。它的转换与struct tm中的 tm_wday、tm_yday无关,当进行转换时,mktime会通过struct tm的其他成员重新矫正该值。若struct tm中的成员是非法的的,mktime将会自动校正,如2018-12-32 00:00:00,矫正后为2019-01-01 00:00:00。若给定struct tm不能转换为日历时间,则mktime返回-1。—-man mktime

1. 背景

背景:最近工作中遇到一个奇怪的问题,在将原先在32位机器上编译的程序放在64位机器上重新编译之后,然后放到IDC机器运行,发现性能降了100倍左右。在经过性能分析和查阅相关资料后发现,是由于mktime使用不当导致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <string>
#include <sys/time.h>
#include <unistd.h>
using namespace std;
#define PrintTime(id) struct timeval __now1105##id;\
gettimeofday(&__now1105##id, 0);
#define PrintTimeDone(id) struct timeval __now21105##id; \
gettimeofday(&__now21105##id, 0); \
printf("timer_%s spend time:%d us\n",#id,(__now21105##id.tv_sec-__now1105##id.tv_sec)* 1000000 + (__now21105##id.tv_usec-__now1105##id.tv_usec));
static void get_time(const std::string& time_str)
{
struct tm temp_tm;
strptime(time_str.c_str(), "%Y-%m-%d %H:%M:%S", &temp_tm);
PrintTime(mktime);
time_t temp = mktime(&temp_tm);
PrintTimeDone(mktime);
}
int main()
{
for (int i = 0; i < 10; i++)
get_time("2018-12-27 00:00:00");
return 0;
}

32位机器(i686)编译运行结果:

1
2
3
4
5
6
7
8
9
10
timer_mktime spend time:51 us
timer_mktime spend time:4 us
timer_mktime spend time:2 us
timer_mktime spend time:3 us
timer_mktime spend time:3 us
timer_mktime spend time:3 us
timer_mktime spend time:3 us
timer_mktime spend time:2 us
timer_mktime spend time:2 us
timer_mktime spend time:2 us

64位机器(x86_64 )编译运行结果:

1
2
3
4
5
6
7
8
9
10
timer_mktime spend time:181 us
timer_mktime spend time:156 us
timer_mktime spend time:138 us
timer_mktime spend time:138 us
timer_mktime spend time:137 us
timer_mktime spend time:145 us
timer_mktime spend time:143 us
timer_mktime spend time:138 us
timer_mktime spend time:138 us
timer_mktime spend time:145 us

造成上述问题的原因究竟是什么呢?

2. 源码分析mktime性能

2.1 mktime源码:

mktime.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
/* Convert *TP to a time_t value. */
time_t
mktime (struct tm *tp)
{
#ifdef _LIBC
/* POSIX.1 8.1.1 requires that whenever mktime() is called, the
time zone names contained in the external variable 'tzname' shall
be set as if the tzset() function had been called. */
__tzset ();
#endif
return __mktime_internal (tp, __localtime_r, &localtime_offset);
}
/* Convert *TP to a time_t value, inverting
the monotonic and mostly-unit-linear conversion function CONVERT.
Use *OFFSET to keep track of a guess at the offset of the result,
compared to what the result would be for UTC without leap seconds.
If *OFFSET's guess is correct, only one CONVERT call is needed.
This function is external because it is used also by timegm.c. */
time_t
__mktime_internal (struct tm *tp,
struct tm *(*convert) (const time_t *, struct tm *),
time_t *offset)
{
time_t t, gt, t0, t1, t2;
struct tm tm;
/* The maximum number of probes (calls to CONVERT) should be enough
to handle any combinations of time zone rule changes, solar time,
leap seconds, and oscillations around a spring-forward gap.
POSIX.1 prohibits leap seconds, but some hosts have them anyway. */
int remaining_probes = 6;
/* Time requested. Copy it in case CONVERT modifies *TP; this can
occur if TP is localtime's returned value and CONVERT is localtime. */
int sec = tp->tm_sec;
int min = tp->tm_min;
int hour = tp->tm_hour;
int mday = tp->tm_mday;
int mon = tp->tm_mon;
int year_requested = tp->tm_year;
int isdst = tp->tm_isdst;
/* 1 if the previous probe was DST. */
int dst2;
/* Ensure that mon is in range, and set year accordingly. */
int mon_remainder = mon % 12;
int negative_mon_remainder = mon_remainder < 0;
int mon_years = mon / 12 - negative_mon_remainder;
long_int lyear_requested = year_requested;
long_int year = lyear_requested + mon_years;
/* The other values need not be in range:
the remaining code handles minor overflows correctly,
assuming int and time_t arithmetic wraps around.
Major overflows are caught at the end. */
/* Calculate day of year from year, month, and day of month.
The result need not be in range. */
int mon_yday = ((__mon_yday[leapyear (year)]
[mon_remainder + 12 * negative_mon_remainder])
- 1);
long_int lmday = mday;
long_int yday = mon_yday + lmday;
time_t guessed_offset = *offset;
int sec_requested = sec;
if (LEAP_SECONDS_POSSIBLE)
{
/* Handle out-of-range seconds specially,
since ydhms_tm_diff assumes every minute has 60 seconds. */
if (sec < 0)
sec = 0;
if (59 < sec)
sec = 59;
}
/* Invert CONVERT by probing. First assume the same offset as last
time. */
t0 = ydhms_diff (year, yday, hour, min, sec,
EPOCH_YEAR - TM_YEAR_BASE, 0, 0, 0, - guessed_offset);
if (TIME_T_MAX / INT_MAX / 366 / 24 / 60 / 60 < 3)
{
/* time_t isn't large enough to rule out overflows, so check
for major overflows. A gross check suffices, since if t0
has overflowed, it is off by a multiple of TIME_T_MAX -
TIME_T_MIN + 1. So ignore any component of the difference
that is bounded by a small value. */
/* Approximate log base 2 of the number of time units per
biennium. A biennium is 2 years; use this unit instead of
years to avoid integer overflow. For example, 2 average
Gregorian years are 2 * 365.2425 * 24 * 60 * 60 seconds,
which is 63113904 seconds, and rint (log2 (63113904)) is
26. */
int ALOG2_SECONDS_PER_BIENNIUM = 26;
int ALOG2_MINUTES_PER_BIENNIUM = 20;
int ALOG2_HOURS_PER_BIENNIUM = 14;
int ALOG2_DAYS_PER_BIENNIUM = 10;
int LOG2_YEARS_PER_BIENNIUM = 1;
int approx_requested_biennia =
(SHR (year_requested, LOG2_YEARS_PER_BIENNIUM)
- SHR (EPOCH_YEAR - TM_YEAR_BASE, LOG2_YEARS_PER_BIENNIUM)
+ SHR (mday, ALOG2_DAYS_PER_BIENNIUM)
+ SHR (hour, ALOG2_HOURS_PER_BIENNIUM)
+ SHR (min, ALOG2_MINUTES_PER_BIENNIUM)
+ (LEAP_SECONDS_POSSIBLE
? 0
: SHR (sec, ALOG2_SECONDS_PER_BIENNIUM)));
int approx_biennia = SHR (t0, ALOG2_SECONDS_PER_BIENNIUM);
int diff = approx_biennia - approx_requested_biennia;
int approx_abs_diff = diff < 0 ? -1 - diff : diff;
/* IRIX 4.0.5 cc miscalculates TIME_T_MIN / 3: it erroneously
gives a positive value of 715827882. Setting a variable
first then doing math on it seems to work.
(ghazi@caip.rutgers.edu) */
time_t time_t_max = TIME_T_MAX;
time_t time_t_min = TIME_T_MIN;
time_t overflow_threshold =
(time_t_max / 3 - time_t_min / 3) >> ALOG2_SECONDS_PER_BIENNIUM;
if (overflow_threshold < approx_abs_diff)
{
/* Overflow occurred. Try repairing it; this might work if
the time zone offset is enough to undo the overflow. */
time_t repaired_t0 = -1 - t0;
approx_biennia = SHR (repaired_t0, ALOG2_SECONDS_PER_BIENNIUM);
diff = approx_biennia - approx_requested_biennia;
approx_abs_diff = diff < 0 ? -1 - diff : diff;
if (overflow_threshold < approx_abs_diff)
return -1;
guessed_offset += repaired_t0 - t0;
t0 = repaired_t0;
}
}
/* Repeatedly use the error to improve the guess. */
for (t = t1 = t2 = t0, dst2 = 0;
(gt = guess_time_tm (year, yday, hour, min, sec, &t,
ranged_convert (convert, &t, &tm)),
t != gt);
t1 = t2, t2 = t, t = gt, dst2 = tm.tm_isdst != 0)
if (t == t1 && t != t2
&& (tm.tm_isdst < 0
|| (isdst < 0
? dst2 <= (tm.tm_isdst != 0)
: (isdst != 0) != (tm.tm_isdst != 0))))
/* We can't possibly find a match, as we are oscillating
between two values. The requested time probably falls
within a spring-forward gap of size GT - T. Follow the common
practice in this case, which is to return a time that is GT - T
away from the requested time, preferring a time whose
tm_isdst differs from the requested value. (If no tm_isdst
was requested and only one of the two values has a nonzero
tm_isdst, prefer that value.) In practice, this is more
useful than returning -1. */
goto offset_found;
else if (--remaining_probes == 0)
return -1;
/* We have a match. Check whether tm.tm_isdst has the requested
value, if any. */
if (isdst_differ (isdst, tm.tm_isdst))
{
/* tm.tm_isdst has the wrong value. Look for a neighboring
time with the right value, and use its UTC offset.
Heuristic: probe the adjacent timestamps in both directions,
looking for the desired isdst. This should work for all real
time zone histories in the tz database. */
/* Distance between probes when looking for a DST boundary. In
tzdata2003a, the shortest period of DST is 601200 seconds
(e.g., America/Recife starting 2000-10-08 01:00), and the
shortest period of non-DST surrounded by DST is 694800
seconds (Africa/Tunis starting 1943-04-17 01:00). Use the
minimum of these two values, so we don't miss these short
periods when probing. */
int stride = 601200;
/* The longest period of DST in tzdata2003a is 536454000 seconds
(e.g., America/Jujuy starting 1946-10-01 01:00). The longest
period of non-DST is much longer, but it makes no real sense
to search for more than a year of non-DST, so use the DST
max. */
int duration_max = 536454000;
/* Search in both directions, so the maximum distance is half
the duration; add the stride to avoid off-by-1 problems. */
int delta_bound = duration_max / 2 + stride;
int delta, direction;
for (delta = stride; delta < delta_bound; delta += stride)
for (direction = -1; direction <= 1; direction += 2)
if (time_t_int_add_ok (t, delta * direction))
{
time_t ot = t + delta * direction;
struct tm otm;
ranged_convert (convert, &ot, &otm);
if (! isdst_differ (isdst, otm.tm_isdst))
{
/* We found the desired tm_isdst.
Extrapolate back to the desired time. */
t = guess_time_tm (year, yday, hour, min, sec, &ot, &otm);
ranged_convert (convert, &t, &tm);
goto offset_found;
}
}
}
offset_found:
*offset = guessed_offset + t - t0;
if (LEAP_SECONDS_POSSIBLE && sec_requested != tm.tm_sec)
{
/* Adjust time to reflect the tm_sec requested, not the normalized value.
Also, repair any damage from a false match due to a leap second. */
int sec_adjustment = (sec == 0 && tm.tm_sec == 60) - sec;
if (! time_t_int_add_ok (t, sec_requested))
return -1;
t1 = t + sec_requested;
if (! time_t_int_add_ok (t1, sec_adjustment))
return -1;
t2 = t1 + sec_adjustment;
if (! convert (&t2, &tm))
return -1;
t = t2;
}
*tp = tm;
return t;
}

tzset.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
void
__tzset (void)
{
__libc_lock_lock (tzset_lock);
tzset_internal (1, 1);
if (!__use_tzfile)
{
/* Set `tzname'. */
__tzname[0] = (char *) tz_rules[0].name;
__tzname[1] = (char *) tz_rules[1].name;
}
__libc_lock_unlock (tzset_lock);
}
weak_alias (__tzset, tzset)
/* Interpret the TZ envariable. */
static void
internal_function
tzset_internal (int always, int explicit)
{
static int is_initialized;
const char *tz;
if (is_initialized && !always)
return;
is_initialized = 1;
/* Examine the TZ environment variable. */
tz = getenv ("TZ");
if (tz == NULL && !explicit)
/* Use the site-wide default. This is a file name which means we
would not see changes to the file if we compare only the file
name for change. We want to notice file changes if tzset() has
been called explicitly. Leave TZ as NULL in this case. */
tz = TZDEFAULT;
if (tz && *tz == '\0')
/* User specified the empty string; use UTC explicitly. */
tz = "Universal";
/* A leading colon means "implementation defined syntax".
We ignore the colon and always use the same algorithm:
try a data file, and if none exists parse the 1003.1 syntax. */
if (tz && *tz == ':')
++tz;
/* Check whether the value changed since the last run. */
if (old_tz != NULL && tz != NULL && strcmp (tz, old_tz) == 0)
/* No change, simply return. */
return;
if (tz == NULL)
/* No user specification; use the site-wide default. */
tz = TZDEFAULT;
tz_rules[0].name = NULL;
tz_rules[1].name = NULL;
/* Save the value of `tz'. */
free (old_tz);
old_tz = tz ? __strdup (tz) : NULL;
/* Try to read a data file. */
__tzfile_read (tz, 0, NULL);
if (__use_tzfile)
return;
/* No data file found. Default to UTC if nothing specified. */
if (tz == NULL || *tz == '\0'
|| (TZDEFAULT != NULL && strcmp (tz, TZDEFAULT) == 0))
{
memset (tz_rules, '\0', sizeof tz_rules);
tz_rules[0].name = tz_rules[1].name = "UTC";
if (J0 != 0)
tz_rules[0].type = tz_rules[1].type = J0;
tz_rules[0].change = tz_rules[1].change = (time_t) -1;
update_vars ();
return;
}
__tzset_parse_tz (tz);
}

2.2 源码分析结论

从mktime的源码实现中可以看出,mktime的大致执行流程如下:

  • 首先通过调用__tzset()对时区进行设置
    • 若TZ环境变量为NULL,尝试调用__tzfile_read读取文件中的时区信息
    • 只需要首次设置,若改变或为NULL重新设置
  • 然后调用__mktime_internal进行时间转换
    • 检查系统的tm_isdst(是否为夏令时)和传入的struct tm的tm_isdst,若两个不一致(isdst_differ),则进行矫正tm_isdst

从上源码可以得出结论:影响mktime性能的两个因素主要包括两方面,一是TZ设置是否为NULL,二是传入的struct tm参数的tm_isdst成员与系统的是否一致。

3. 实验分析mktime性能

3.1 TZ对mktime的性能影响

3.1.1 实验程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void test_TZ_effect(int count)
{
struct tm temp_tm;
temp_tm.tm_isdst = 0;
std::string time_str = "2019-01-02 00:00:00";
strptime(time_str.c_str(), "%Y-%m-%d %H:%M:%S", &temp_tm);
setenv("TZ", "Asia/Shanghai", 0);
PrintTime(TZ_SET);
for (int i = 0; i < count; i++)
{
mktime(&temp_tm);
}
PrintTimeDone(TZ_SET);
unsetenv("TZ");
PrintTime(TZ_UNSET);
for (int i = 0; i < count; i++)
{
mktime(&temp_tm);
}
PrintTimeDone(TZ_UNSET);
}

3.1.2 实验结果:

1
2
3
4
5
6
7
8
./a.out 10000
32位机器实验结果:
timer_TZ_SET spend time:3945 us
timer_TZ_UNSET spend time:14332 us
64位机器实验结果:
timer_TZ_SET spend time:2566 us
timer_TZ_UNSET spend time:17459 us

3.1.3 实验结果分析

从源码分析可以知道,当TZ为NULL时,将会尝试读取文件,故不设置TZ时性能较低,约为设置的1/6

3.2 tm_isdst对mktime性能影响

3.2.1 实验程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void test_TZ_effect(int count)
{
struct tm temp_tm;
std::string time_str = "2019-01-02 00:00:00";
strptime(time_str.c_str(), "%Y-%m-%d %H:%M:%S", &temp_tm);
setenv("TZ", "Asia/Shanghai", 0);
PrintTime(DST_INNER_0);
for (int i = 0; i < count; i++)
{
temp_tm.tm_isdst = 0;
mktime(&temp_tm);
}
PrintTimeDone(DST_INNER_0);
PrintTime(DST_INNER_1);
for (int i = 0; i < count; i++)
{
temp_tm.tm_isdst = 1;
mktime(&temp_tm);
}
PrintTimeDone(DST_INNER_1);
PrintTime(DST_INNER_NAG1);
for (int i = 0; i < count; i++)
{
temp_tm.tm_isdst = -1;
mktime(&temp_tm);
}
PrintTimeDone(DST_INNER_NAG1);
/************************************/
PrintTime(DST_OUTTER_0);
temp_tm.tm_isdst = 0;
for (int i = 0; i < count; i++)
{
mktime(&temp_tm);
}
PrintTimeDone(DST_OUTTER_0);
PrintTime(DST_OUTTER_1);
temp_tm.tm_isdst = 1;
for (int i = 0; i < count; i++)
{
mktime(&temp_tm);
}
PrintTimeDone(DST_OUTTER_1);
PrintTime(DST_OUTTER_NAG1);
temp_tm.tm_isdst = -1;
for (int i = 0; i < count; i++)
{
mktime(&temp_tm);
}
PrintTimeDone(DST_OUTTER_NAG1);
}

3.2.2 实验结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ./a.out 10000
32位实验结果
imer_DST_INNER_0 spend time:3957 us
timer_DST_INNER_1 spend time:1986811 us
timer_DST_INNER_NAG1 spend time:3871 us
timer_DST_OUTTER_0 spend time:3889 us
timer_DST_OUTTER_1 spend time:4042 us
timer_DST_OUTTER_NAG1 spend time:3958 us
64位实验结果
timer_DST_INNER_0 spend time:2622 us
timer_DST_INNER_1 spend time:1408451 us
timer_DST_INNER_NAG1 spend time:2588 us
timer_DST_OUTTER_0 spend time:2534 us
timer_DST_OUTTER_1 spend time:2698 us
timer_DST_OUTTER_NAG1 spend time:2621 us

3.2.3 实验结果分析

从上面可以看出,当tm_isdst与系统夏令时设置不一致的时候,性能为一致时的1/500左右。为什么将tm_isdst设置放在外面的时候性能又高了很多呢?从源码可以看出,当mktime对isdst进行矫正后,将正确的isdst存到了传入的结构体中,所以第二次进来时,isdst与系统一致,不需要矫正。

特别注意:当tm_isdst初始化为-1的时候,mktime设置是否为夏令时时间时失手编译器的设计影响,所以传入isdst=-1可能会得到错误的结果,即非夏令时按照夏令时计算或者夏令时按照非夏令时计算。

4. 背景中出现的问题以及mktime使用注意事项

4.1 背景中问题原因

通过实验程序以及原程序片段分析,运行过程中,在32位机器上的tm_isdst默认值为0,在64位机器上tm_isdst的默认值为1(与系统和内存环境有关,可能出现不同结果,我的机器上上述问题),导致在32位机器上运行时间较短,而且第一次运行时需要设置TZ环境变量,故第一次运行时间为
51us,后面运行时间较短。而64为机器一致运行时间为150us左右。

4.2 mktime使用注意事项

  • 使用前建议设置TZ环境变量,在大量调用操作过程中,有助于将该部分性能提高到5~6倍.
  • mktime函数中传入的tm的tm_isdst参数必须设置为0或1,若设置为-1可能会得到错误的结果,或者使用默认值导致性能大大降低,国内一般设置为0,因为中国不采用夏令时。

5. 参考链接:

https://github.molgen.mpg.de/git-mirror/glibc/blob/20003c49884422da7ffbc459cdeee768a6fee07b/time/mktime.c
https://github.molgen.mpg.de/git-mirror/glibc/blob/20003c49884422da7ffbc459cdeee768a6fee07b/time/tzset.c
https://blog.csdn.net/aquester/article/details/54669264

Comment and share

pthread使用barrier栅栏方式同步

Linux中提供了多种同步机制,其中使用barrier(栅栏)是多线程之间进行同步的方法之一。

基本原理

假设多个线程约定一个栅栏,只有当所有的线程都达到这个栅栏时,栅栏才会放行,否则到达此处的线程将被阻塞。

使用场景

程序启动的时候,需要建立一个独立的线程去做一些特殊的工作。比如这个线程需要初始化一些全局配置等等。而主线程启动后,必须等待这个子线程拿到配置信息后,才能继续工作。所以这里就存在一个问题要解决,主线程如何等待这个子线程完成工作后,才继续往下执行呢?

栅栏相关API

1
2
3
4
5
6
7
8
9
10
11
12
#include <pthread.h>
//初始化栅栏,栅栏需要等待到count个线程,才会全部一起放行。
int pthread_barrier_init(pthread_barrier_t *restrict,
const pthread_barrierattr_t *restrict, unsigned count);
//报道函数,当一个线程到达栅栏的时候,就报道。
//所有的线程都报道后,栅栏自然会放行。
int pthread_barrier_wait(pthread_barrier_t *barrier);
栅栏完成历史使命后,当然是功成身退。
int pthread_barrier_destroy(pthread_barrier_t *barrier);

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
pthread_barrier_t barrier;
void* initor(void* args) {
printf("---------------thread init work(%d)--------------\n", time(NULL));
//模拟初始化工作。
sleep(10);
//到达栅栏
pthread_barrier_wait(&barrier);
printf("--------------thread start work(%d)--------------\n", time(NULL));
sleep(10);
printf("--------------thread stop work(%d)--------------\n", time(NULL));
return NULL;
}
int main(int argc, char* argv[]) {
//初始化栅栏,该栅栏等待两个线程到达时放行
pthread_barrier_init(&barrier, NULL, 2);
printf("**************main thread barrier init done****************\n");
pthread_t pid;
pthread_create(&pid, NULL, &initor, NULL);
printf("**************main waiting(%d)********************\n", time(NULL));
//主线程到达,被阻塞,当初始化线程到达栅栏时才放行。
pthread_barrier_wait(&barrier);
pthread_barrier_destroy(&barrier);
printf("***************main start to work(%d)****************\n", time(NULL));
sleep(30);
pthread_join(pid, NULL);
printf("***************thread complete(%d)***************\n", time(NULL));
return 0;
}

Comment and share

Unix信号之sigaction函数

sigaction函数是除signal函数之外的另一个可以设置信号处理的函数。sigaction用法比signal函数复杂,但是可以对信号处理进行更加精准更灵活的控制。sigaction是POSIX的信号接口,而signal函数是标准C的信号接口,所以sigaction函数的可移植性更强。

sigaction函数说明

函数功能

检查或修改与指定信号相关联的处理动作,可以同时检查和修改。

函数原型

1
2
3
4
5
6
7
8
struct sigaction{
void (*sa_handler)(int); //信号处理函数地址
sigset_t sa_mask; //信号掩码集,当调用信号处理函数时,程序将阻塞sa_mask中的信号
int sa_flag; //位掩码,指定用于控制信号处理过程中的各种选项。
void (*sa_sigaction)(int,siginfo_t*,void*); //暂不用
};
int sigaction(int signo,const struct sigaction*restrict act,struct sigaction*restrict oact);

函数参数

  • signo : 指定操作的信号

  • act : 新修改的sigaction

  • oldact : 保存该函数原有的sigaction。

上述struct sigaction中sa_flag取值说明(常用的有以下两个):

  • SA_NODEFER:当信号处理函数正在进行时,不堵塞对于信号处理函数自身信号功能。
  • SA_RESETHAND:当用户注册的信号处理函数被执行过一次后,该信号的处理函数被设为系统默认的处理函数

函数返回值

  • 0 : 返回0表示设置成功
  • -1 : 返回-1表示设置失败

函数示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
void sighandler(int sig) {
printf("this is in the sig handler\n");
for (int i = 0; i < 5; i++) {
printf("num:%d\n", i);
}
}
int main() {
struct sigaction act;
struct sigaction oldact;
act.sa_handler = sighandler;
act.sa_flags = SA_NODEFER ;//| SA_RESETHAND;
sigaction(SIGINT , &act,&oldact);
printf ("this is the main function\n");
pid_t pid = getpid();
printf("%d", pid);
kill(pid, SIGINT);
}

Comment and share

判断主机字节序大端规则或小端规则方法

大端规则与小端规则

在计算机存储中存储字节的顺序有两种分别为大端规则和小端规则。

  • 小端规则(littel endian):低序字节存储到内存较低的位置,即起始位置。
  • 大端规则(big endian):低序字节存储到内存较高的位置,即高序字节存储到起始位置。

有一个32位数字为:0x01020304

小端规则的机器上,其存储如下:

低地址 -> -> 高地址
0x04 0x03 0x02 0x01

大端规则机器上,其存储如下:

低地址 -> -> 高地址
0x01 0x02 0x03 0x04

判断当前机器字节序的方法

判断当前机器为大端规则还是小端规则,其本质是对于一个变量,判断其各字节的存储顺序

方法一:使用union判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
int main() {
union {
short a;
char c[2];
} u;
u.a = 0x0102;
if (u.c[0] == 2 && u.c[1] == 1) {
std::cout << "little" << std::endl;
} else if (u.c[0] == 1 && u.c[1] == 2) {
std::cout << "big" << std::endl;
} else {
std::cout << "unkown" << std::endl;
}
return 0;
}

方法二:直接将字节取出,判断顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
int main() {
short s = 0x0102;
char* a = (char*)(&s);
std::cout << (*a) << std::endl;
char b = 0x02, c = 0x01;
if (((*a) | b) == b) {
std::cout << "little" << std::endl;
} else if (((*a) | c) == b) {
std::cout << "big" << std::endl;
} else {
std::cout << "unknow" << std::endl;
}
return 0;
}

Comment and share

pack与aligned的区别

GCC支持用__attribute__为变量、类型、函数以及标签指定特殊属性。其中可以使用attribute的aligned属性控制变量或类型的内存对其规则,之前一篇文章已经提到pack可以改变结构体中各成员之间的内存对其规则。https://langzi989.github.io/2017/10/02/C语言内存对其相关/

其中#pragma pack()和__attribute__((aligned))区别很大。

aligned内存对其详解

使用场景:

  • 变量
  • 类型

功能说明:

  • 当aligned作用于变量时,其作用是告诉编译器为变量分配内存的时候,要分配在指定对其的内存上.作用于变量之上不会改变变量的大小。
    • 例如:int a __attribute__((aligned(16)));该变量a的内存起始地址为16的倍数。
  • 当aligned作用于类型时,其作用是告诉编译器该类型声明的所有变量都要分配在指定对齐的内存上。当该属性作用于结构体声明时可能会改变结构体的大小。
1
2
3
4
5
6
7
8
struct Test{
char a[3];
}__attribute__((aligned(8)));
int main() {
//8
std::cout << sizeof(Test);
}

如上所示,当align作用于结构体定义时会改变结构体的大小。结构体最终大小为aligned指定大小的整数倍。

aligned与pack的区别

从上面可以看出,aligned和pack的主要区别如下:

  • pack作用于结构体或类的定义,而aligned既可以作用于结构体或类的定义,也可以作用于变量的声明。
  • pack的作用是改变结构体或类中成员变量的布局规则,而aligned只是建议编译器对指定变量或指定类型的变量分配内存时的规则。
  • pack可以压缩变量所占内存的空间
  • align可以指定变量在内存的对其规则,而pack不可以。
  • 若某一个结构体的默认pack为n,pack指定的对齐规则m大于n,则该pack忽略。若aligned指定的对齐规则s大于n,则此时结构体的大小一定为s的整数倍。
  • aligned和pack指定规则时都必须为2的n次幂。

参考链接:http://blog.shengbin.me/posts/gcc-attribute-aligned-and-packed

Comment and share

fcntl实现对文件加锁功能

之前有一篇文章详细介绍了fcntl的用法,这一节将说明使用fcntl实现对文件加锁的功能,

fcntl函数原型

fcntl函数如下,具体用法可参考上面的文章。

1
2
#include <fcntl.h>
int fcntl(int fd, int cmd, .../*int args or lock args*/);

使用fcntl对文件加锁

当fcntl中的cmd为F_GETLK,F_SETLK,F_SELFKW时为对文件进行锁操作,此时arg参数为flock。注意:使用fcntl对文件加锁,加锁效果类似于自旋锁,只有写写互斥和读写互斥,读读并不互斥。

cmd取值及其操作

  • F_GETLK : 获取当前锁得状态
  • F_SETLK : 给当前文件上锁(非阻塞)。
  • F_SETLKW : 给当前文件上锁(阻塞,若当前文件正在被锁住,该函数一直阻塞)。

flock结构体定义如下:

1
2
3
4
5
6
7
8
struct flock {
short int l_type;
short int l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
};

下面对flock中的参数一一解释:

  • l_type:此参数表示所得类型。其可能的取值包括一下三个:
    • F_RDLCK : 读锁
    • F_WRLCK : 写锁
    • F_UNLCK : 无锁状态
  • l_start : 此参数锁区域的开始位置的偏移量
  • l_whence:此参数决定锁开始的位置。其可选参数为:
    • SEEK_SET:当前位置为文件的开头
    • SEEK_CUR:当前位置为文件指针的位置
    • SEEK_END:当前位置为文件末尾
  • l_len : 锁定文件的长度

若要锁定整个文件,通常的方法为将l_start设为0,l_whence设为SEEK_SET,l_len设为0.

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void lock_set(int fd, int type) {
struct flock lock;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
while (1) {
lock.l_type = type;
if ((fcntl(fd, F_SETLK, &lock)) == 0) {
if (lock.l_type == F_RDLCK)
printf("read lock set by %d\n", getpid());
else if(lock.l_type == F_WRLCK)
printf("write lock set by %d\n", getpid());
else if (lock.l_type == F_UNLCK)
printf("release lock by %d\n", getpid());
return;
}
//检查文件是否可以上锁
fcntl(fd, F_GETLK, &lock);
//判断不能上锁的原因
if (lock.l_type != F_UNLCK) {
if (lock.l_type == F_RDLCK)
printf("read lock has been already set by %d\n", getpid());
else if (lock.l_type == F_WRLCK)
printf("write lock has been already set by %d\n", getpid());
getchar();
}
}
}
int main() {
int fd;
fd = open("data", O_RDWR | O_CREAT, 0666);
if (fd < 0) {
perror("open failed");
return -1;
}
lock_set(fd, F_WRLCK);
getchar();
lock_set(fd, F_UNLCK);
getchar();
close(fd);
return 0;
}

Comment and share

Unix系统调用hook函数以及使用dl库实现

参考链接:http://www.it165.net/os/html/201407/8847.html

系统调用属于一种软中断机制(内中断陷阱),它有操作系统提供的功能入口(sys_call)以及CPU提供的硬件支持(int 3 trap)共同完成。
hook系统调用:为当用户代码调用系统调用的时候,我们通过某种手段入侵该系统调用,使得系统调用中除了执行原有的操作,还可以完成我们需要其完成的一些自定义特性,也可以理解为我们为这个函数做了一个钩子。这样我们就可以实现如当一个文件发生写操作的时候,通知所有关注此文件的进程或线程等等。

通过动态链接库挟持系统调用

在linux操作系统的动态链接库的世界中,LD_PRELOAD就是这样一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。loader在进行动态链接的时候,会将有相同符号名的符号覆盖成LD_PRELOAD指定的so文件中的符号。换句话说,可以用我们自己的so库中的函数替换原来库里有的函数,从而达到hook的目的。

上述hook系统调用的方法可以使用dlfcn.h库中的一系列函数实现。
example,此函数hook了fcntl系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <stdarg.h>
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
typedef int (*fcntl_pfn_t)(int fildes, int cmd, ...);
int fcntl(int fildes, int cmd, ...)
{
fcntl_pfn_t g_sys_fcntl_func = (fcntl_pfn_t)dlsym(RTLD_NEXT,"fcntl");
va_list arg_list;
va_start( arg_list,cmd );
int ret = -1;
switch( cmd )
{
case F_DUPFD:
{
int param = va_arg(arg_list,int);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
case F_GETFD:
{
ret = g_sys_fcntl_func( fildes,cmd );
break;
}
case F_SETFD:
{
int param = va_arg(arg_list,int);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
case F_GETFL:
{
ret = g_sys_fcntl_func( fildes,cmd );
break;
}
case F_SETFL:
{
int param = va_arg(arg_list,int);
int flag = param;
flag |= O_NONBLOCK;
ret = g_sys_fcntl_func( fildes,cmd,flag );
break;
}
case F_GETOWN:
{
ret = g_sys_fcntl_func( fildes,cmd );
break;
}
case F_SETOWN:
{
int param = va_arg(arg_list,int);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
case F_GETLK:
{
struct flock *param = va_arg(arg_list,struct flock *);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
case F_SETLK:
{
struct flock *param = va_arg(arg_list,struct flock *);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
case F_SETLKW:
{
struct flock *param = va_arg(arg_list,struct flock *);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
}
va_end( arg_list );
return ret;
}
int main() {
int old = fcntl(STDIN_FILENO, F_GETFL);
printf ("old:%d\n", old);
fcntl(STDIN_FILENO, F_SETFL, old);
int _new = fcntl(STDIN_FILENO, F_GETFL);
printf ("new:%d\n", _new);
}

Comment and share

Linux中dlfcn库相关学习

在linux中静态链接库和动态链接库是进程之间代码共享的两种方式。Linux在库中提供了加载和处理动态连接库的系统调用,使用非常方便。具体用法如下:

dlfcn库中函数说明

dlfcn库中主要包括四个函数:

1
2
3
4
5
6
7
8
9
#include <dlfcn.h>
void* dlopen(const char*, int flag);
char* dlerror();
void* dlsym(void* handler, char* symbol);
int dlclose(void* handler);

  • dlopen : 打开一个动态连接库,并返回一个类型为void*的handler,flag为打开模式,可选的模式有两种
    • RTLD_LAZY 暂缓决定,等有需要时再解出符号
    • RTLD_NOW 立即决定,返回前解除所有未决定的符号。
  • dlerror : 返回dl操作的错误,若没有出现错误,则返回NUlL,否则打印错误信息
  • dlsym : 查找动态链接库中的符号symbol,并返回该符号所在的地址
  • dlclose : 关闭动态链接库句柄

使用实例

动态链接库cal.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//cal.cpp
extern "C" {
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int div(int a, int b) {
return a / b;
}
}

生成动态链接库libcal.so

1
g++ -shared -fPIC cal.cpp libcal.so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//main.cpp
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#define LIB_LIBRARY_PATH_1 "./libcal.so"
typedef int (*CAC_FUNC)(int ,int);
int main() {
void* handler = NULL;
char* error = NULL;
CAC_FUNC cac_func = NULL;
handler = dlopen(LIB_LIBRARY_PATH_1, RTLD_LAZY);
if (!handler) {
fprintf(stderr, "err:%s\n", dlerror());
exit(1);
}
dlerror();
//此处取对应函数地址,
*(void **) (&cac_func) = dlsym(handler, "add");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "err:%s", error);
exit(1);
}
printf("add:%d\n", cac_func(1,2));
cac_func = (CAC_FUNC)dlsym(handler, "sub");
printf("sub:%d\n", cac_func(1,2));
cac_func = (CAC_FUNC)dlsym(handler, "mul");
printf("mul:%d\n", cac_func(1,2));
cac_func = (CAC_FUNC)dlsym(handler, "div");
printf("div:%d\n", cac_func(1,2));
dlclose(handler);
return 0;
}

编译函数main:

1
g++ main.cpp -rdynamic -ldl

执行结果:

1
2
3
4
add:3
sub:-1
mul:2
div:0

注意问题

特别注意,若使用c++编译动态链接库,一定要在需要使用的符号处添加extern “C”,否则会出现符号找不到的问题。即undefined symbol

Comment and share

C++协程及其原理

协程的几种实现方式及原理

协程又可以称为用户线程,微线程,可以将其理解为单个进程或线程中的多个用户态线程,这些微线程在用户态进程控制和调度.协程的实现方式有很多种,包括

  1. 使用glibc中的ucontext库实现
  2. 利用汇编代码切换上下文
  3. 利用C语言语法中的switch-case的奇淫技巧实现(protothreads)
  4. 利用C语言的setjmp和longjmp实现

实际上,无论是上述那种方式实现协程,其原理是相同的,都是通过保存和恢复寄存器的状态,来进行各协程上下文的保存和切换。

协程较于函数和线程的优点

  • 相比于函数:协程避免了传统的函数调用栈,几乎可以无限地递归
  • 相比与线程:协程没有内核态的上下文切换,近乎可以无限并发。协程在用户态进程显式的调度,可以把异步操作转换为同步操作,也意味着不需要加锁,避免了加锁过程中不必要的开销。

进程,线程以及协程的设计都是为了并发任务可以更好的利用CPU资源,他们之间最大的区别在于CPU资源的使用上:

  • 进程和线程的任务调度是由内核控制的,是抢占式的;
  • 协程的任务调度是在用户态完成,需要代码里显式地将CPU交给其他协程,是协作式的

由于我们可以在用户态调度协程任务,所以我们可以把一组相互依赖的任务设计为协程。这样,当一个协程任务完成之后,可以手动的进行任务切换,把当前任务挂起(yield),切换到另一个协程区工作.由于我们可以控制程序主动让出资源,很多情况下将不需要对资源进行加锁。

Comment and share

ucontext族函数详解

the ucontext_t type is a structure type suitable for holding the context for the user thread of execution. A thread’s context include stack,saved registersm a list of block signals

上述为ncontext_t结构体的定义,ucontext机制是GNU C库提供的一组创建,保存,切换用户态执行上下文的API,从上面的描述可以看出ucontext_t结构体使得用户在程序中保存当前的上下文成为可能。我们也可以利用此实现用户级线程,即协程。

ucontext_t以及ucontext族函数

ucontext_t结构体

ucontext_t结构体定义,一个ucontext_t至少包括以下四个成员,可能依据不同系统包括其他不同的成员。

1
2
3
4
5
6
7
8
#include <ucontext.h>
typedef struct ucontext_t {
struct ucontext_t* uc_link;
sigset_t uc_sigmask;
stack_t uc_stack;
mcontext_t uc_mcontext;
...
};

类成员解释:

  • uc_link:为当前context执行结束之后要执行的下一个context,若uc_link为空,执行完当前context之后退出程序。
  • uc_sigmask : 执行当前上下文过程中需要屏蔽的信号列表,即信号掩码
  • uc_stack : 为当前context运行的栈信息。
  • uc_mcontext : 保存具体的程序执行上下文,如PC值,堆栈指针以及寄存器值等信息。它的实现依赖于底层,是平台硬件相关的。此实现不透明。

ucontext族函数

ucontext族函数主要包括以下四个:

1
2
3
4
5
#include <ucontext.h>
void makecontext(ucontext_t* ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t* olducp, ucontext_t* newucp);
int getcontext(ucontext_t* ucp);
int setcontext(const ucontext_t* ucp);

  • makecontext:初始化一个ucontext_t,func参数指明了该context的入口函数,argc为入口参数的个数,每个参数的类型必须是int类型。另外在makecontext之前,一般需要显示的初始化栈信息以及信号掩码集同时也需要初始化uc_link,以便程序退出上下文后继续执行。
  • swapcontext:原子操作,该函数的工作是保存当前上下文并将上下文切换到新的上下文运行。
  • getcontext:将当前的执行上下文保存在ucp中,以便后续恢复上下文
  • setcontext : 将当前程序切换到新的context,在执行正确的情况下该函数直接切换到新的执行状态,不会返回。

注意:setcontext执行成功不返回,getcontext执行成功返回0,若执行失败都返回-1。若uc_link为NULL,执行完新的上下文之后程序结束。

简单实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <ucontext.h>
#include <iostream>
#include <unistd.h>
void newContextFun() {
std::cout << "this is the new context" << std::endl;
}
int main() {
char stack[10*1204];
//get current context
ucontext_t curContext;
getcontext(&curContext);
//modify the current context
ucontext_t newContext = curContext;
newContext.uc_stack.ss_sp = stack;
newContext.uc_stack.ss_size = sizeof(stack);
newContext.uc_stack.ss_flags = 0;
newContext.uc_link = &curContext;
//register the new context
makecontext(&newContext, (void(*)(void))newContextFun, 0);
swapcontext(&curContext, &newContext);
printf("main\n");
return 0;
}

Comment and share

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China