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

AWK学习

in Unix脚本语言

AWK是一种强大的文本处理工具,其处理文本的效率极高,其输出可以来自标注输入、其他命令的输出或一个或多个文件,熟练使用awk将会对工作效率有很大的提升。

awk调用方式

awk调用方式包括三种:

一、命令行调用

1
awk [-F seperator] 'commond' input-file

commond的组成又可以包括多个模式和动作的组合,即命令行调用又可以写为:

1
akw [-F seperator] 'parrtern1 {Action1} pattern2 {Action2}' input-file
  • seperator:分隔符。分隔符为可选参数,可以为任意字符串,若不指定,默认分隔符为空格。
  • commond:awk命令
  • input_file: 待处理的文本文件

二、脚本调用

1
awk -f awk-script-file input-file

将awk命令写入一个文件中,然后使用-f参数指定该文件运行

三、shell脚本插入awk命令

在shell中插入awk命令对文件进行处理,直接执行shell命令。

模式与动作

任何awk语句都由**模式**和**动作**组成,**模式部分决定Action语句何时触发以及触发事件,动作决定对当前被匹配的数据进行的操作**,Action中由多个awk处理语句组成。
1
awk [-F seperator] 'parrtern1 {Action1} pattern2 {Action2}' input-file

注意问题

  • awk语句必须被单引号或双引号包含,防止awk语句被当做shell命令解析。
  • 确保awk命令中所有引号都成对出现
  • 确保用花括号括起来动作语句,用圆括号括起来条件语句

  • 模式与动作一一对应,只有pattern匹配成功,对应的action(用{}括起来表示一个action)才会执行。

  • 模式可以为任何条件语句复合语句正则表达式,也可以为awk保留字BEGIN,END

  • 模式尽量不要加双引号,否则某些情况下可能会失效,如”$1>30”。

  • action一定要用{}括起来,一个{}括起来的动作属于一个action,不同{}括起来的动作属于不同action,其对应不同的pattern。

  • BEGIN和END为特殊的模式,BEGIN模式使用在任何文本浏览之前,常用来做一些初始化设置或打印头部信息等,END模式使用在完成文本浏览动作之后,常用来处理一些收尾打印等工作。注意BEGIN和END语句都仅且执行一次

awk基本用法

假定一下为一个学校中抽样的几个学生的成绩(score.txt),下面四列分别为学号,名字,年级,分数:

1
2
3
4
13331264 tom grade4 94
13342010 marry grade4 90
13315012 jemmy grade1 85
13323089 jane grade2 80

域标识

awk从输入文件中**每次读取一行**,当遇到分割符时将其分割为域,这些域被标记为\$1,\$2,\$3...,直到读到文件结尾或文件不存在,$0表示当前记录(即当前行)。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 不加pattern
awk '{print $1,$4}' score.txt
输出结果:
13331264 94
13342010 90
13315012 85
13323089 80
# pattern为$4>=90,过滤分数超过90分的用户才处理
awk '$4>=90 {print $1,$4}' score.txt
输出结果:
13331264 94
13342010 90
# 命令有两个parttern action组,第一个pattern为BEGIN,第二个patter为$4>=90
awk 'BEGIN {print "学号 成绩"} $4>=90 {print $1,$4}' score.txt
输出结果:
学号 成绩
13331264 94
13342010 90

条件控制语句

关系与正则运算符

符号 描述
< 小于
<= 小于等于
> 大于
>= 大于等于
== 等于
~ 匹配正则表达式(二元 符号前面为被匹配的字符串,后面为模式串,一般模式传用/pattern/括起来)
!~ 不匹配正则表达式

逻辑运算符

逻辑运算符与C语言中完全一致。

符号 描述
&& 且,两个条件同时满足才为真
\ \ 或,只要有一个条件为真即为真
! 将结果取反(一元运算符)

关键字

关键字 描述
break 用于while或for循环,退出循环
continue 终止当前循环,执行下一个循环
next 导致读入下一个输入行,并返回到脚本的顶部。这可以避免对当前输入行执行其他的操作过程。
exit 退出执行,直接跳转到END执行,若没有END,终止脚本

if条件控制语句

在awk中使用if判断条件时,必须将if后面的条件用()括起来,与C语言类似。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 过滤学号中有1333字符串的成绩记录
awk '{if($1~/1333/) print $0}' score.txt
输出:
13331264 tom grade4 94
# 过滤学号中不含1333字符串的成绩记录
awk '{if($1!~/1333/) print $0}' score.txt
输出:
13342010 marry grade4 90
13315012 jemmy grade1 85
13323089 jane grade2 80
# 过滤成绩大于85分的成绩记录
awk '{if($4>85) print $0}' score.txt
输出:
13331264 tom grade4 94
13342010 marry grade4 90
# 过滤成绩大于90小于100的成绩记录
awk '{if($4>90&&$4<100) print $0}' score.txt
输出:
13331264 tom grade4 94

当然我们使用模式代替if条件判断,这个可以达到相同的效果

1
2
3
4
5
# 使用条件语句
awk '{if($4>85) print $0}' score.txt
# 使用模式过滤行
awk '$4>85 {print $0}' score.txt

for循环控制语句

语法

1
2
3
4
5
6
7
8
9
10
11
# 格式一:
for (变量 in 数组)
{
do_something;
}
# 格式二(与C语言相同)
for (变量;条件;表达式)
{
do_something;
}

使用示例

1
2
3
4
5
# ENVIRON为awk内置的环境变量,下面会说到。其为一个数组,该作用为打印环境变量中的所有键值对
awk 'BEGIN {for(k in ENVIRON) {print k"="ENVIRON[k];}}'
# 打印0-9
awk 'BEGIN {for(i=0;i<10;i++) {print i}}'

while循环控制语句

语法:

1
2
3
4
while (条件表达式)
{
do_something;
}

使用示例

1
2
# 打印0-9
awk 'BEGIN {i=0; while (i<10){print i;i++}}'

do while循环控制语句

语法:

1
2
3
4
5
do
{
do_something
}
while (条件表达式)

使用示例

1
2
#打印0-9
awk 'BEGIN {i=0;do{print i; i++;} while (i<10)}'

awk运算

算术运算符

符号 描述
+ - * / % 加/减/乘/除/取余
++ 自增1
自减1
+ 一元加操作符,将操作数乘以1
- 一元减操作符,将操作数乘以-1
^ 求幂。如2^2=4

赋值运算符

符号 描述
= 赋值
+=、-=、*=、/=、%=、^= 将左右操作数进行对应操作,然后赋值给左操作数(与C语言完全一致)

其他运算符

符号 描述
$ 字段引用(引用域)
? : 条件表达式(与C语言一致)
空格 字符串连接符
in 判断数组中是否存在某个键值

运算符优先级:
crc1

awk数组

awk数组是一种关联数组,其下标既可以是数字,也可以是字符串。
  • 无需定义,数组在使用时被定义
  • 数组元素的初始值为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
# 数组可直接使用,且无需定义
awk 'BEGIN {a["123"]=2;print a["123"]}'
输出:
2
# 可使用for循环对数组中的元素进行循环读取
awk 'BEGIN {a[1]=2;a[2]=3;a[3]=4; for(k in a) print k"="a[k];}'
输出:
1=2
2=3
3=4
# 可以通过if 判断某个key是否在数组中
awk 'BEGIN {a[1]=2;a[2]=3;a[3]=4; print 5 in a; print 1 in a}'
输出:
0
1
# 删除数组中的元素,使用delete arr['key']
awk 'BEGIN {a[1]=2;a[2]=3;a[3]=4; delete a[1];for(k in a) print k"="a[k];}'
输出:
2=3
3=4
# 多维数组的下标分隔符默认为“\034”,可通过设定SUBSEP修改多为数组的下标分隔符
awk 'BEGIN {a[1,2]=10; for(k in a) print k"="a[k];}'
输出:
12=10
awk 'BEGIN {SUBSEP=":";a[1,2]=10; for(k in a) print k"="a[k];}'
输出:
1:2=10

awk内置变量

变量 描述
ARGC 命令行参数个数,awk后参数个数
ARGV 命令行参数数组,数组下标从0开始
ENVIRON 系统环境变量数组
FILENAME 输入文件的名字
FNR 浏览文件的记录数(文件中的记录数,若多个文件不会累加)
NR 已读记录数(已读的记录数,若多文件会离家)
NF 浏览记录域的个数(即每行分割的域的最大值)
FS 设置域分割符,常用于BEGIN中设置域分割符
RS 设置记录分隔符,原记录分隔符默认为换行,即一行一行读取,可使用该参数控制其不按照行读取
OFS 设置输出域分隔符,原域默认分隔符为空格,可使用此分隔符修改
ORS 设置输出记录分隔符。原记录默认分隔符为换行,可使用此参数修改

使用示例:

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
# ARGC测试
awk 'BEGIN {print ARGC}' score.txt
输出:
2
# ARGV测试
awk 'BEGIN {print ARGC; print ARGV[0];print ARGV[1]}' score.txt
输出:
2
awk
score.txt
# ENVIRON测试
awk 'BEGIN {for(k in ENVIRON) {print k"="ENVIRON[k];}}'
# FILENAME测试
awk 'BEGIN {i=0} {if(i==0){print FILENAME;i++}}' score.txt
输出:
score.txt
# FNR测试
awk ' END {print FNR}' score.txt
输出:
1
2
3
4
#NR 测试
awk '{print NR}' score.txt
输出:
1
2
3
4
# NF测试
awk ' END {print NF}' score.txt
# FS测试(以下两种方式效果一致)
awk 'BEGIN {FS="\t"} {print NR}' score.txt
awk -F'\t' '{print NR}' score.txt
# OFS测试
awk 'BEGIN {OFS="|"} {print $1,$2}' score.txt
输出:
13331264|tom
13342010|marry
13315012|jemmy
13323089|jane
# ORS测试
awk 'BEGIN {ORS="|"} {print $1,$2}' score.txt
输出:
13331264 tom|13342010 marry|13315012 jemmy|13323089 jane|
# RS测试
awk 'BEGIN {RS="1333"} {print $1,$2}' score.txt
输出:
1264 tom

FNR与NR区别

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
cat a.txt
111
222
111
333
444
cat b.txt
111
555
666
awk '{print FNR}' a.txt b.txt
1
2
3
4
5
1
2
3
awk '{print NR}' a.txt b.txt
1
2
3
4
5
6
7
8

awk内置函数

计算相关函数:

函数 描述
cos(expr) 计算余弦值,参数为弧度
sin(expr) 计算正弦值,参数为弧度
int(expr) 取整
log(expr) 计算expr的自然对数
sqrt(expr) 计算expr的平方根
1
2
3
4
5
6
7
8
9
10
11
12
13
# 测试cos
awk 'BEGIN {print cos(60*3.1415936/180)}'
输出:
0.5
# 测试int
awk 'BEGIN {print int(20.5)}'
输出:
20
# 测试log
awk 'BEGIN {print log(10)}'
输出:
2.30259

注意:awk字符串下标从1开始不是从0开始

字符串相关函数:

函数 描述
sub(src,des) 将0中src第一次出现的子串替换为des
sub(src,des,str) 将字符串str中第一次出现的src替换为des。
gsub(src,des) 将0中的src全部替换为des,若0中包含src,则返回1否则返回0
gsub(src,des,str) 将字符串str中的所有src替换为des,
index(str,substr) 返回str中字符串substr首次出现的位置,位置从1开始,若未找到则返回0
length(str) 返回str的长度
match(str, substr) 测试str中是否存在子串substr
split(str,result,sep) 将str以sep为分割符分割为数组,并存到result中
printf(format,…) 格式化输出,与C语言类似
substr(str,start) 返回从start开始一直到最后的子字符串,与C++类似
substr(str,start,n) 返回从start开始长度为n的子字符串,与C++类似

常用printf format

format 说明
%c ascii字符
%d 整数
%e 浮点数,科学计数法
%f 浮点数(如1.234)
%o 八进制数
%x 十六进制数
%s 字符串

更多format详见(http://wiki.jikexueyuan.com/project/awk/pretty-printing.html)

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
#sub测试
awk 'BEGIN {s="aaabbbaaaccc";sub("aaa","1",s);print s}'
输出:
1bbbaaaccc
# gsub(r,s)测试
awk '{gsub("t","s");print $0}' ./score.txt
输出:
13331264 som grade4 94
13342010 marry grade4 90
13315012 jemmy grade1 85
13323089 jane grade2 80
# gsub(r,s,t)测试
awk '{gsub("133", "45",$1);print $0}' ./score.txt
输出:
4531264 tom grade4 94
4542010 marry grade4 90
4515012 jemmy grade1 85
4523089 jane grade2 80
# index(s,t)测试
awk '{r = index($2,"m");print r}' ./score.txt
输出:
3
1
3
0
# 测试length
awk '{print length($2)}' ./score.txt
输出:
3
5
5
4
# 测试match
awk '{print match($2,"to")}' ./score.txt
输出:
1
0
0
0
# split测试
awk 'BEGIN {print split("this is a test",result, " "); for(k in result) {print k":"result[k]}}'
# substr测试
awk 'BEGIN {s="aaabbbccc";print substr(s,2)}'
输出:
aabbbccc

AWK几个例子

文件去重并统计相同记录出现次数(保留记录原来的顺序)

1
2
3
4
5
6
7
8
9
10
11
test.txt
111
222
111
333
444
# !的优先级高于++,读到一条记录,首先判断记录是否存在于arr中,若不存在,添加到数组中并将该记录数出现次数+1,否则打印
awk '!arr[$0]++' test.txt
# 统计每条记录出现的次数
awk '{!arr[$0]++} END {for (k in arr) print k,arr[k]}' test.txt

文件内容合并

1
2
3
4
5
6
test.txt.1
111
555
666
awk '{if(!arr[$0]) {print $0; arr[$0]++}}' test.txt test.txt.1

找出文件A中存在且文件B中不存在的记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A:
111
222
333
444
B:
333
444
#计算a.txt-(a.txt并a.txt)
awk 'NR==FNR {a[$0]=1} NR>FNR {if (!a[$0]) print $0}' b.txt a.txt
输出:
111
222

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

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

汇编基础之寄存器及汇编指令

参考链接:
http://www.cnblogs.com/technology/archive/2010/05/16/1736782.html
http://anonymalias.github.io/2017/01/09/ucontext-theory/

最近在学习协程方面的知识,在协程的一种实现方式中有一种是用汇编实现的,所以这里再把汇编知识复习一下。

寄存器

寄存器按照其用途可分为以下4类:

  • 数据寄存器
  • 指针及变址寄存器
  • 段寄存器
  • 控制寄存器

数据寄存器

数据寄存器包括4个16位的寄存器(AX,BX,CX,DX)或者8个8位的寄存器(AH,AL,BH,BL,CH,CL,DH,DL),这些寄存器都是用来暂时存放操作数,运算结果或者其他信息,但同时又具有某些专门的用途。

  • AX(累加寄存器):算术运算中的主要寄存器,在乘除运算中用来指定除数和被除数,也是乘除运算后积和商的默认存储单元。,另外IO指令均使用该寄存器与IO设备传送信息
  • BX(基址寄存器):指令寻址时常用作基址寄存器,存入偏移量或者偏移量的构成部分。
  • CX(计数寄存器):在循环指令操作或者串处理指令中隐含计数
  • DX(数据寄存器):在双字节运算中,与AX构成32位操作数,DX为高16位。在某些IO指令中,DX被用来存放端口地址

指针及变址寄存器

这些寄存器都是16位的寄存器,用来存放16为的操作数或者中间结果,但是更常见的是存放偏移量或者位移量

  • SP(堆栈指针寄存器):指向栈顶的位置,与SS寄存器一起组成栈顶数据的物理地址
  • BP(基址指针寄存器):系统默认其指向堆栈中的某一单元,即提供栈中该单元偏移量,加段前缀后,BP可作为非堆栈段的地址指针。一般用于识别栈帧的起始位置。
  • SI(源变址寄存器):与DS联用, 指示数据段中某操作的偏移量. 在做串处理时, SI指示源操作数地址, 并有自动增量或自动减量的功能. 变址寻址时, SI与某一位移量共同构成操作数的偏移量
  • DI(目的变址寄存器):与DS联用, 指示数据段中某操作数的偏移量, 或与某一位移量共同构成操作数的偏移量. 串处理操作时, DI指示附加段中目的地址, 并有自动增量或减量的功能

段寄存器

  • CS(代码段):存放当前程序的指令代码
  • DS(数据段):存放程序所涉及的源数据以及结果
  • SS(堆栈段):以先进后出原则的数据区
  • ES(附加段):辅助数据区,存放串或者其他数据

控制寄存器

  • IP(指令寄存器): 存放下一条要执行的指令的偏移量
  • FR(控制标志位):
    • CF(进位标识位):进行加减运算时, 如果最高二进制位产生进位或错位, CF则为1, 否则为0. 程序设计中, 常用条件转移指令JC, JNC指令据此标志位实现转移
    • PF(奇偶标志位):操作结果中二进制位1的个数为偶数是, PF为1, 某则为0
    • AF(辅助进位标志位):运算时半字节产生进位或借位时, AF为1, 某则为0. 主要用于BCD码的调整
    • ZF(零标志位):运算结果为0时, ZF为1, 否则为0
    • SF(符号标志位):当运算结果的最高位为1时, SF为1, 否则为0. 最高位表示符号数的正和负
    • TF(跟踪标志位):用于调试程序时进入单步方式工作. TF=1时, 每条指令执行完后产生一个内部中断, 让用户检查指令运行后寄存器, 存储器和各标志位的内容. TF=0时, CPU工作正常, 不产生内部中断
    • IF(中断允许标志位):IF=1同时中断屏蔽寄存器的相应位为0, 允许系统响应可屏蔽中断, 反之, 不接收外部发出的中断请求
    • DF(方向位标志位):用于控制串操作时地址指针位移方向. 当DF=1时, 指针向高地址方向移动
    • OF(溢出标志位):算术运算时结果超出系统所能表示的数的范围. 溢出时, OF=1

注意:上述为16位处理其器中的寄存器的名字,在32和64位系统中,寄存器中名字有相应的变化,具体看下面(32位在前面加E,64在前面加R,64位系统的中含有16个64位的通用寄存器):

i386常用的16个寄存器

  • EAX、EBX、ECX、EDX这四个寄存器,主要就是用来暂时存放计算过程中所用的操作数、结果或其它信息。
  • ESP为堆栈指针寄存,它和堆栈段寄存器SS共同决定了当前的栈指针,每当执行push,pull操作时,或者因为某些原因(如中断),CPU自动将数据入栈时,就会使用该指针来找到堆栈栈顶(或栈底)的位置,然后执行压栈、出栈的操作。系统管理软件,如操作系统会根据其分配的栈空间地址来设定这两个寄存器的值。
  • EBP称为基址指针寄存器,它和ESP都可以与堆栈段寄存器SS联用来确定堆栈中的某一存储单元的地址,ESP用来指示段顶的偏移地址,而EBP可作为堆栈区中的一个基地址以便访问堆栈中的信息。
  • ESI(源变址寄存器)和EDI(目的变址寄存器)一般与数据段寄存器DS联用,用来确定数据段中某一存储单元的地址。这两个变址寄存器有自动增量和自动减量的功能,可以很方便地用于变址。在串处理指令中,ESI和EDI作为隐含的源变址和目的变址寄存器时,ESI和DS联用,EDI和附加段ES联用,分别达到在数据段和附加段中寻址的目的。
  • EIP指令指针寄存器,它用来存放代码段中的偏移地址。在程序运行的过程中,它始终指向下一条指令的首地址。它与段寄存器CS联用确定下一条指令的物理地址。当这一地址送到存储器后,控制器可以取得下一条要执行的指令,而控制器一旦取得这条指令就马上修改EIP的内容,使它始终指向下一条指令的首地址。那些跳转指令,就是通过修改EIP的值来达到相应的目的的
  • FLAGS标志寄存器,又称PSW(program status word),即程序状态寄存器。这一个是存放条件标志码、控制标志和系统标志的寄存器。
  • 段寄存器:一共六个,分别是CS代码段,DS数据段,ES附加段,SS堆栈段,FS以及GS这两个还是附加段。
    EFLAGS寄存器中的IF位表示是否允许中断,为1允许,否则不允许。
  • TR寄存器:用来指向当前任务的TSS段
  • IDTR寄存器:用来指向当前IDT(中断表述符表或者说是中断向量表),因为在保护模式下,IDT的起始地址可以在任何位置,而不仅限于地址0。
  • GDT和LDT : 前者是全局描述符表,位置由GDTR寄存器确定,后者是局部描述符表,位置由LDTR寄存器确定,具体使用哪一个,取决于段选择码中的TI位。

汇编指令

汇编指令格式

汇编指令的格式如下:

1
[标号:] 指令助记符[[目的操作数][,源操作数]][;注释]

  • 指令助记符:如MOV,ADD之类标识传送,加法。不区分大小写
  • 目的操作数:作用有两个,1.参与指令操作2,暂时存储操作结果
  • 源操作数:主要提供原始数据或操作对象。面向所有寻址方式
  • 注释:用分号隔开

汇编指令中常见的符号:

  • imme:立即数
  • DST:目的操作数
  • SRC:源操作数
  • mem:存储器操作数
  • OPR:操作数
  • reg:通用寄存器
  • EA:偏移地址
  • Sreg:段寄存器
  • Port:端口地址
  • Lable:标号

汇编指令可以分成六类:

  • 数据传送指令
  • 算数运算指
  • 逻辑运算与移位指令
  • 串操作指令
  • 程序控制指令
  • 处理器控制指令

    数据传送指令

    数据传送指令

  • MOV DST,SRC(传送指令):把源操作数的内容送入目的操作数
    • 立即数做源操作数时,立即数的长度必须小于等于目的操作数的长度
    • 操作数分别为reg,reg或reg,sreg或sreg,sreg或reg,sreg时,两者的长度必须保持一致
    • 立即数不能作为目的操作数
    • CS和IP寄存器不能做目的操作数,不允许用立即数为段寄存器赋值
    • 不能将一个段寄存器的内容直接送到另一个段寄存器中, 可借助通用寄存器或PUSH, POP指令实现这一要求
  • PUSH SRC(压栈指令): 将一个字数据压入当前栈顶, 位移量disp=-2的地址单元. 数据进栈时, 栈指针SP首先向低地址方向移动两个字节位置, 接着数据进栈, 形成新的栈顶
  • POP DST(出栈指令):弹出栈顶元素, 后将栈顶指针向栈底方向移动一个字
  • XCHG OPR1, OPR2(交换指令):交换指令: 将这两个操作数交换

地址传送指令

  • LEA DST, SRC(装载有效指令):该指令将源操作数的偏移量OA装载到目的操作数中
  • LDS DST, SRC(装载数据段指针指令):将当前数据段中的一个双字数据装入到一个通用寄存器SI(双字数据的低字)和数据段寄存器DS(双字数据的高字)中
  • LES DST,SRC(装载附加段指针指令):将附加数据段中的一个32位地址数据指针(附加段指针)送到DI(低字)和ES(高字)寄存器中

标志传送指令

  • LAHF(标志寄存器送AH指令): 将标志寄存器的低字节送入AH中
  • SAHF(AH送标志寄存器指令): 将AH寄存器内容送标志寄存器FR的低字节
  • PUSHF(标志进栈指令): 标志寄存器进栈
  • POPF (标志出栈指令): 标志寄存器出栈

累加器专用传送指令

  • IN AL, Port:从端口读入数据, 存放在AL中
  • OUT PORT,AL:传送AL中的数据到端口
  • XLAT OPR或XLAT:用于将AL中当前内容转换为一种代码

算术运算指令

加法指令

  • ADD DST, SRC:DST+SRC的和存放到DST中去
  • ADC DST, SRC:带进位加法指令, DST+SRC+CF
  • INC DST:增1指令

减法指令

  • SUB DST, RSC:DST-SRC, 存放到DST中
  • SBB DST, SRC:带借位减法指令, DST-SRC-CF
  • DEC DST :减1指令
  • NEG DST:求补指令, 求补码
  • CMP OPR1, OPR2:比较指令

乘法指令

  • MUL SRC:无符号数乘指令, AL*SRC, 结果放入AX中
  • IMUL SRC:有符号数乘指令, AL*SRC, 结果放入AX中

除法指令

  • DIV SRC :无符号数除指令, AX/SRC, 商放入AL中, 余数放在AH中
  • IDIV SRC:符号数除指令, AX/SRC, 上放入AL中, 余数放在AH中
  • CBW, CWD:都是符号扩展指令. 将AL的符号扩到AX中; 将AX的符号扩到DX

逻辑运算与移位指令

逻辑运算指令

  • NOT OPR:逻辑非指令
  • AND OPR:逻辑与指令
  • OR OPR:逻辑或指令
  • XOR OPR :逻辑异或指令

移位指令:

  • SHL DST, CNT:逻辑左移
  • SHR DST, CNT:逻辑右移
  • SAL DST, CNT:算术左移
  • SAR DST, CNT:算术右移

循环移位指令

  • ROL DST, CNT:循环左移
  • ROR DST, CNT:循环右移
  • RCL DST, CNT:带进位循环左移
  • RCR DST, CNT:带进位循环右移

串操作指令

  • MOVS:串传送指令
  • CMPS:串比较指令
  • SCAS:串扫描指令
  • LODS:装入串指令
  • STOS:存储串指令

控制转移指令

转移指令:

  • JMP:无条件转移指令
  • JX:条件转移指令(JC/JNC, JZ/JNZ, JE/JNE, JS/JNS, JO/JNO, JP/JNP…)

循环指令

  • LOOP 标号:该指令执行时, 技术寄存器CXX首先减1, 然后判断CX, 若为0, 跳出循环

条件循环指令

  • LOOPZ/LOOPE, LOOPNZ/LOOPNE:前者用于找到第一个不为0的事件, 后者用于找到第一个为0的事件

子程序调用指令

  • CALL 子程序名:段内直接调用
  • RET

中断指令

  • INT N(中断类型号):软中断指令
  • IRET:中断返回指令

处理器控制指令

标志处理指令:

  • CLC:进位标志CF置0
  • CMC:进位标志CF求反
  • STC:进位标志值1
  • CLD:方向标志置0
  • STD:方向标志置1
  • CLI:中断允许标志置0
  • STI:中断允许标志置1

其他处理器控制指令:

  • NOP:空操作
  • HLT:停机
  • WAIT:等待
  • ESC:换码
  • LOCK:封锁

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

System V IPC 机制和key_t本质类型

System V三种IPC机制

System V IPC包括三种不同的通信机制

  • 消息队列:消息队列类似于管道,但是又有很大的差别。第一,消息队列是有边界的,所以消息队列的通信机制是通过消息进行传递,而管道是通过字节流进行通信。第二,每条消息包括一个完整的整形字段,消息队列可以通过类型来选择消息。
  • 信号量:用于同步两个进程。
  • 共享内存:共享内存允许两个进程共享同一个内存段。即一块内存被映射到不同进程的虚拟内存中。

key_t本质

System V系统调用都有一个相关的get系统调用,它与文件io中的open函数类似,不同的是它的参数是一个整数,给定一个整数之后,系统会做以下操作:

  • 使用给定key创建一个新的IPC对象,并将IPC对象标识key_t返回给调用者.
  • 若该key对应的IPC对象存在,直接将标识返回给调用者,相当于使用key换取标识,而不做创建操作.

上述IPC对象的标识类型为key_t。key_t的本质是什么?我们可以通过grep命令追踪到

在文件/usr/include/sys/ipc.h中:

1
2
3
4
#ifndef __key_t_defined
typedef __key_t key_t;
# define __key_t_defined
#endif

在文件/usr/include/bits/types.h中:

1
__STD_TYPE __KEY_T_TYPE __key_t; /* Type of an IPC key. */

在文件/usr/include/bits/typesizes.h中:

1
#define __KEY_T_TYPE __S32_TYPE

在文件中:

1
#define __S32_TYPE int

故从上面的追踪,可以看出key_t的本质为int类型.

获取key_t方法

获取ket_t的方法三种

  • 随机选取一个整数值作为key值,多个进程共享一个key值。注意此时不能使用已经存在的key
  • 在创建IPC对象的get方法中用IPC_PRIVATE作为key值,这样会导致每个调用都会创建一个新的IPC对象
  • 使用ftok函数生成一个key,这个方法产生的key接近唯一。

IPC_PRIVATE

IPC_PRIVATE本质上为0的宏定义.使用方法是直接使用IPC_PRIVATE作为key生成一个IPC对象。这种做法保证每次产生的都是全新的IPC对象。
这种做法适用于在父进程fork前创建IPC对象,从而达到子进程继承IPC对象的目的。

ftok生成key

函数原型:

1
2
#include <sys/ipc.h>
key_t ftok(char* pathname, int proj);

函数功能:

此函数的功能是根据pathname和proj的值生成key,该算法只使用proj的最低的8个有效位,应用程序必须确保pathname引用一个可以应用stat()的既有文件。否则该函数会返回-1。

在linux中,key_t一般是32位的值。它通过取proj参数的最低8个有效位,包含该文件所属的文件系统的设备的设备号的最低8个有效位以及pathname所引用文件的inode号的最低16个有效位组合而成,这样保证了唯一性。所以从上面可以看出,对于同意个文件的不同路径(inode值相同),proj值相同,此时产生的key是相同的。 

Comment and share

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China