muduo 库解析之三:CurrentThread

__thread

线程局部存储 Thread Local Storage(tls),是一种机制,通过这一机制分配的变量,每个当前线程有一个该变量的实例。

在用户层,用一个新的存储类型关键字:__thread 表示这一扩展。

__thread 使用规则:

  • 如果一个线程局部存储变量有一个初始化器,它必须是常量表达式。
  • __thread 限定符可以单独使用,也可带有 externstatic 限定符,但不能带有其它存储类型的限定符。
  • 当一个线程终止时,任何该线程中的线程局部存储变量都不再有效。
  • 不能修饰 class 类型,因为无法自动调用构造函数和析构函数。
  • 可以用于修饰全局变量,函数内的静态变量,不能修饰函数的局部变量或者class的普通成员变量。
#include <stdio.h>
#include <pthread.h>

static __thread int var = 5;

void *worker1()
{
    ++var;
    printf("worker1,var is: %d,address is: %p\n", var, &var);
}

void *worker2()
{
    sleep(3);
    ++var;
    printf("worker2,var is: %d,address is: %p\n", var, &var);
}

int main()
{
    pthread_t t1, t2;
    pthread_create(&t1, NULL, worker1, NULL);
    pthread_create(&t2, NULL, worker2, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    return 0;
}

输出:

worker1,var is: 6,address is: 0x7f21d75236fc
worker2,var is: 6,address is: 0x7f21d6d226fc

可以看出每个线程中变量地址不相同,互相之间没有影响。

__builtin_expect

gcc(version >= 2.96) 指令,作用是允许程序员将最有可能执行的分支告诉编译器。这个指令的写法为:

__builtin_expect(EXP, N)

意思是:EXP==N 的概率很大,编译器会根据这个条件,预取指令,从而减少系统的取指次数。

一般的使用方法是将__builtin_expect指令封装宏:

#define Likely(x) __builtin_expect(!!(x),1) //@ x 很可能为真
#define UnLikely(x) __builtin_expect(!!(x),0) //@ x 很可能为假

例如:

#include <stdio.h>

#define Likely(x) __builtin_expect(!!(x),1) //@ x 很可能为真
#define UnLikely(x) __builtin_expect(!!(x),0) //@ x 很可能为假

int abs(int var)
{
	//@ var 很大可能性大于0 
    if(Likely(var > 0))
        return var;
    else
        return -var;
}
int main()
{
    printf("%d\n",abs(10));
    return 0;
}

输出程序的调用栈

#include <execinfo.h>
 
/* Store up to SIZE return address of the current program state in
   ARRAY and return the exact number of values stored.  */
int backtrace(void **array, int size);
 
/* Return names of functions from the backtrace list in ARRAY in a newly
   malloc()ed memory block.  */
char **backtrace_symbols(void *const *array, int size);
 
/* This function is similar to backtrace_symbols() but it writes the result
   immediately to a file.  */
void backtrace_symbols_fd(void *const *array, int size, int fd);
  • backtrace 的实现依赖于栈指针(fp寄存器),在 gcc 编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后多将不能正确得到程序栈信息。
  • backtrace_symbols 的实现需要符号名称的支持,在 gcc 编译过程中需要加入-rdynamic参数。
  • 内联函数没有栈帧,它在编译过程中被展开在调用的位置。
  • 尾调用优化将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。

测试

add.c

#include <stdio.h>

int add_one(int num)
{
    int ret = 0;
    int *p = NULL;

    *p = 1; //@  导致段错误
    ret = *p + num;
    return ret;
 }

 int increase(int num)
 {
     int ret = 0;
     ret = add_one(num);
     return ret;
 }

dump.c

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>  
#include <execinfo.h>

void dump()
{
    int nptrs,j;
    const int kBackTraceSize = 16;
    void *buffer[kBackTraceSize];
    char **strings = NULL;
    nptrs = backtrace(buffer,kBackTraceSize);
    printf("backtrace() returned %d addresses\n",nptrs);
    strings = backtrace_symbols(buffer,nptrs);
    if(strings == NULL)
    {
        perror("backtrace_symbols");
        exit(EXIT_FAILURE);
    }

    for (int j = 0; j < nptrs;++j)
        printf(" [%02d] %s \n",j,strings[j]);
    free(strings);
}

void signal_handler(int signo)
{
    printf("\n============>>>catch signal %d<<<=============\n",signo);
    printf("Dump stack start...\n");
    dump();
    printf("Dump stack end...\n");
    signal(signo,SIG_DFL); //@ 恢复信号默认处理
    raise(signo); //@ 重新发送信号
}

backtrace_test.c

#include <stdio.h>
#include <signal.h>

extern int increase(int num);
extern void dump();
extern void signal_handler(int signo);

int main()
{
    int value = 0;
    signal(SIGSEGV,signal_handler); //@ 为 SIGSEGV 安装信号处理函数
    value = increase(value);
    printf("value is:%d\n",value);
    return 0;
}

静态链接测试

gcc -g -rdynamic backtrace_test.c  add.c dump.c -o test
./test

输出:

============>>>catch signal 11<<<=============
Dump stack start...
backtrace() returned 8 addresses
 [00] ./test(dump+0xa5) [0x400c28] 
 [01] ./test(signal_handler+0x33) [0x400d0c] 
 [02] /lib/x86_64-linux-gnu/libc.so.6(+0x354c0) [0x7f77bfbb04c0] 
 [03] ./test(add_one+0x1a) [0x400b46] 
 [04] ./test(increase+0x1c) [0x400b7b] 
 [05] ./test(main+0x28) [0x400b0e] 
 [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f77bfb9b840] 
 [07] ./test(_start+0x29) [0x400a19] 
Dump stack end...
Segmentation fault (core dumped)

可以看到当执行 increase 函数时就开始调用段错误处理函数,其地址为:0x400b7b,使用 addr2line 查看具体的行号:

addr2line -e test 0x400b7b

//@ 输出
/test/muduo/add.c:17

动态链接测试

先将 add.c 编译成动态链接库 libadd.so,然后再编译执行程序:

gcc -g -rdynamic add.c -fPIC -shared -o libadd.so
gcc -g -rdynamic backtrace_test.c dump.c -L./ -ladd -Wl,-rpath=./ -o test

./test
  • -L:指定编译时链接的路径。
  • -ladd:指定编译时链接库。
  • -Wl,-rpath=./:指定程序执行时动态链接库的搜索路径。

输出:

============>>>catch signal 11<<<=============
Dump stack start...
backtrace() returned 8 addresses
 [00] ./test(dump+0xa5) [0x400bd1] 
 [01] ./test(signal_handler+0x33) [0x400cb5] 
 [02] /lib/x86_64-linux-gnu/libc.so.6(+0x354c0) [0x7fe20b74a4c0] 
 [03] ./libadd.so(add_one+0x1a) [0x7fe20badf6ba] 
 [04] ./libadd.so(increase+0x1c) [0x7fe20badf6ef] 
 [05] ./test(main+0x28) [0x400b0e] 
 [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fe20b735840] 
 [07] ./test(_start+0x29) [0x400a19] 
Dump stack end...
Segmentation fault (core dumped)

查看行号:

addr2line -e libadd.so 0x7fe20badf6ef

//@ 输出
??:0

原因:动态链接库是程序运行时动态加载的而其加载地址也是每次可能多不一样的,可见 0x7fe20badf6ef 是一个非常大的地址,和能得到正常信息的地址如 0x400b0e 相差甚远,其也不是一个实际的物理地址(用户空间的程序无法直接访问物理地址),而是经过 MMU(内存管理单元)映射过的。

只需要得到此次 libadd.so 的加载地址然后用 0x7fe20badf6ef 这个地址减去 libadd.so 的加载地址得到的结果再利用 addr2line 命令就可以正确的得到出错的地方。

修改函数:

void signal_handler(int signo)
{
#if 1
	//@ 打印进程的 maps
    char buf[64] = {0};
    sprintf(buf,"cat /proc/%d/maps",getpid());
    system((const char*)buf);
#endif

    printf("\n============>>>catch signal %d<<<=============\n",signo);
    printf("Dump stack start...\n");
    dump();
    printf("Dump stack end...\n");
    signal(signo,SIG_DFL); //@ 恢复信号默认处理
    raise(signo); //@ 重新发送信号
}

编译执行,摘取部分输出结果:

7f075ebab000-7f075ebac000 r-xp 00000000 08:01 26355037                  /test/muduo/libadd.so
7f075ebac000-7f075edab000 ---p 00001000 08:01 26355037                  /test/muduo/libadd.so
7f075edab000-7f075edac000 r--p 00000000 08:01 26355037                  /test/muduo/libadd.so
7f075edac000-7f075edad000 rw-p 00001000 08:01 26355037                  /test/muduo/libadd.so

maps 的输出:

  • 第一列表示地址范围。
  • 第二列表示的读写执行权限,p 表示私有的。由此可以知道 libadd.text 段为 7f075ebab000-7f075ebac000
============>>>catch signal 11<<<=============
Dump stack start...
backtrace() returned 8 addresses
 [00] ./test(dump+0xa5) [0x400cb1] 
 [01] ./test(signal_handler+0x84) [0x400de6] 
 [02] /lib/x86_64-linux-gnu/libc.so.6(+0x354c0) [0x7f075e8164c0] 
 [03] ./libadd.so(add_one+0x1a) [0x7f075ebab6ba] 
 [04] ./libadd.so(increase+0x1c) [0x7f075ebab6ef] 
 [05] ./test(main+0x28) [0x400bee] 
 [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f075e801840] 
 [07] ./test(_start+0x29) [0x400af9] 
Dump stack end...
Segmentation fault (core dumped)

0x7f075ebab6ef 正好是在 7f075ebab000-7f075ebac000。计算地址偏差:

0x7f075ebab6ef - 0x7f075ebab000 = 0x6ef

查看行号:

addr2line -e libadd.so 0x6ef

//@ 输出
/test/muduo/add.c:17

__cxa_demangle

typeid操作符可以用来获取一个类型/表达式的名称:

#include <iostream>
#include <typeinfo>

using namespace std;

int main()
{
    std::cout << typeid(int).name() << std::endl;
    return 0;
}

输出:

//@ msvc
int
//@ gcc
i

想在 gcc 里得到和 msvc 差不多显示效果的方法也是有的,那就是使用 __cxa_demangle

char* abi::__cxa_demangle (const char * mangled_name, char * output_buffer, size_t * length, int * status)

测试:

#include <iostream>
#include <typeinfo>
#include <cxxabi.h>
#include <malloc.h>

using namespace std;

int main()
{
    char *name = abi::__cxa_demangle(typeid(int).name(), NULL, NULL, NULL);
    std::cout << name << std::endl;
    free(name);
    return 0;
}

线程号

在多线程中,pthread_self() 函数获得的线程号是 pthread 库对线程的编号,而不是 Linux 系统对线程的编号。pthread_create() 返回的线程号,使用 top 命令是查不到的,top 显示的是 Linux 的线程号。

在单线程中,Linux 的线程号和进程号是一样的。在多线程中,主线程的线程号(main函数的线程)与进程号一样,其他线程则有各自的线程号。

getpid() 函数不同的是,Linux 并没有直接给一个 gettid() 的API,而是使用 syscall() 直接用 SYS_gettid 的系统调用号去获取线程号。

测试:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>

void* worker(void*)
{
    printf("tid:%ld\n",static_cast<long>(pthread_self()));
    printf("pid:%ld\n",static_cast<long>(::syscall(SYS_gettid)));
    while(1)
    {
        ;
    }
}

int main()
{
    pthread_t thr;
    pthread_create(&thr,NULL,worker,NULL);
    pthread_join(thr,NULL);

    return 0;
}

输出:

tid:140612210849536
pid:2081

使用 top -H 查看:
muduo 库解析之三:CurrentThread

延时

sleep

#include<unistd.h>
unsigned int sleep(unsigned int seconds);
  • 以秒为单位。
  • 若进程暂停到参数 seconds 所指定的时间,成功则返回0,若有信号中断则返回剩余秒数。
  • 在 linux 中,sleep 是通过 nanosleep 实现的。在一些其他系统中(例如POSIX.1),它是通过 alarm() 来实现的。

usleep

#include<unistd.h>
unsigned int usleep(unsigned int useconds);
  • 以微秒为单位。
  • 若进程暂停到参数seconds 所指定的时间,成功则返回0,若有信号中断则返回剩余微秒数。

nanosleep

#include<time.h>
struct timespec
{
    time_t tv_sec; /* 秒seconds /
    long tv_nsec; / 纳秒nanoseconds */
};

int nanosleep(const struct timespec req, struct timespec rem);
  • 以纳秒为单位。
  • 若进程暂停到参数 req 所指定的时间,成功则返回0,若有信号中断则返回-1,并且将剩余微秒数记录在 rem 中。
    req->tv_sec 是以秒为单位,而 tv_nsec 以毫微秒为单位(10的-9次方秒)。
  • 由于调用 nanosleep 是是进程进入TASK_INTERRUPTIBLE,这种状态是会相应信号而进入TASK_RUNNING 状态的。
  • unix、linux 系统尽量不要使用 usleepsleep 而应该使用 nanosleep,使用 nanosleep 应注意判断返回值和错误代码,否则容易造成 cpu占用率100%。
while (nanosleep(&ts, &ts) == -1 && errno == EINTR)
{}

select

struct timeval 
{
    long    tv_sec;         /* 秒数 */
    long    tv_usec;        /* 微秒数 */
 };
         
int select(int maxfdp1,fd_set * readsest,fd_set * writeset,fd_set * exceptset,const struct timeval * timeout);
  • timeout 显然是一个超时时间值,其类型是 struct timeval *,即一个 struct timeval 结构的变量的指针。

源码

CurrentThread.h

#pragma once

#include <string>
#include <unistd.h>
#include <sys/syscall.h> //@ syscall
#include <sys/types.h>
#include <execinfo.h> //@ backtrace
#include <cxxabi.h>   //__abi::__cxa_demangle

namespace muduo
{
    namespace CurrentThread
    {
        extern __thread int t_cached_tid;
        extern __thread char t_tid_string[32];
        extern __thread int t_tid_string_length;
        extern __thread const char *t_thread_name;

        void cache_tid();

        inline int tid()
        {
            if (__builtin_expect(t_cached_tid == 0, 0))
            {
                cache_tid();
            }
            return t_cached_tid;
        }

        inline const char *tid_string()
        {
            return t_tid_string;
        }

        inline int tid_string_length()
        {
            return t_tid_string_length;
        }

        inline const char *name()
        {
            return t_thread_name;
        }

        pid_t gettid();

        bool is_main_thread();

        void sleep_usec(int64_t usec);

        std::string stack_trace(bool demangle);
    }
} // namespace name

CurrentThread.cc

#include "CurrentThread.h"

namespace muduo
{
    namespace CurrentThread
    {
        __thread int t_cached_tid = 0;
        __thread char t_tid_string[32];
        __thread int t_tid_string_length = 6;
        __thread const char *t_thread_name = "unknown";

        static_assert(std::is_same<int, pid_t>::value, "pid should be int");

        std::string stack_trace(bool demangle)
        {
            std::string stack;
            const int kMaxFrames = 200;
            void *buf[kMaxFrames];
            int nptrs = ::backtrace(buf, kMaxFrames);
            char **strings = ::backtrace_symbols(buf, nptrs);
            if (strings)
            {
                size_t len = 256;
                char *demangled = demangle ? static_cast<char *>(::malloc(len)) : nullptr;
                for (int i = 1; i < nptrs; ++i) //@ i == 0,表示当前的函数,在此直接跳过
                {
                    if (demangle)
                    {
                        char *left_par = nullptr;
                        char *plus = nullptr;
                        for (char *p = strings[i]; *p; ++p)
                        {
                            if (*p == '(')
                                left_par = p;
                            else if (*p == '+')
                                plus = p;
                        }

                        if (left_par && plus)
                        {
                            *plus = '\0';
                            int status = 0;
                            char *ret = abi::__cxa_demangle(left_par + 1, demangled, &len, &status);
                            *plus = '+';
                            if (status == 0)
                            {
                                demangled = ret;
                                stack.append(strings[i], left_par + 1);
                                stack.append(demangled);
                                stack.append(plus);
                                stack.push_back('\n');
                                continue;
                            }
                        }
                    }
                    stack.append(strings[i]);
                    stack.push_back('\n');
                }
                free(demangled);
                free(strings);
            }
            return stack;
        }

        pid_t gettid()
        {
            return static_cast<pid_t>(::syscall(SYS_gettid));
        }

        void cache_tid()
        {
            if (t_cached_tid == 0)
            {
                t_cached_tid = gettid();
                t_tid_string_length = snprintf(t_tid_string, sizeof(t_tid_string), "%5d ", t_cached_tid);
            }
        }

        bool is_main_thread()
        {
            return tid() == getpid();
        }

        void sleep_usec(int64_t usec)
        {
            struct timespec ts = {0, 0}; //@ nano sleep
            ts.tv_sec = static_cast<time_t>(usec / 1000 * 1000);
            ts.tv_nsec = static_cast<long>(usec % 1000 * 1000 * 1000);
            ::nanosleep(&ts, NULL);
        }
    }

}
上一篇:身为java开发,掌握这8个Synchronized 用法,你就厉害了!


下一篇:ThreadLocal学习(四)