pthread_create源码分析

下面来看glibc中pthread_create函数的源码,分为两部分来看。

__pthread_create_2_1第一部分
nptl/pthread_create.c

int __pthread_create_2_1 (newthread, attr, start_routine, arg)pthread_t *newthread;const pthread_attr_t *attr;void *(*start_routine) (void *);void *arg;
{STACK_VARIABLES;const struct pthread_attr *iattr = (struct pthread_attr *) attr;if (iattr == NULL)iattr = &default_attr;struct pthread *pd = NULL;ALLOCATE_STACK (iattr, &pd);pd->header.self = pd;pd->header.tcb = pd;pd->start_routine = start_routine;pd->arg = arg;

传入的参数newthread为即将创建的新线程的pthread结构指针,attr为用户指定的线程创建属性,start_routine为新线程执行的函数指针,arg为新线程函数的参数地址。

STACK_VARIABLES宏定义了新的堆栈指针stackaddr,stackaddr指向即将分配的线程栈的有效栈顶(不含保护区)。

# define STACK_VARIABLES void *stackaddr = NULL

如果用户未指定线程创建的属性attr,则使用默认的属性值default_attr,其定义如下。

static const struct pthread_attr default_attr ={.guardsize = 1,};

默认的属性只定义了栈保护区guardsize的大小,该保护区通常用来检测栈的数据是否到达保护区,即是否下溢。

# define ALLOCATE_STACK(attr, pd) allocate_stack (attr, pd, &stackaddr)

ALLOCATE_STACK宏用来分配线程栈,并在栈底创建pthread结构并初始化。
然后设置执行线程函数的地址start_routine和参数地址arg。

下面重点来看allocate_stack函数的源码。

__pthread_create_2_1->allocate_stack 第一部分
glibc nptl/allocatestack.c

static int allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,ALLOCATE_STACK_PARMS)
{struct pthread *pd;size_t size;size_t pagesize_m1 = __getpagesize () - 1;void *stacktop;size = attr->stacksize ?: __default_stacksize;if (__builtin_expect (attr->flags & ATTR_FLAG_STACKADDR, 0)){uintptr_t adj;if (attr->stacksize != 0&& attr->stacksize < (__static_tls_size + MINIMAL_REST_STACK))return EINVAL;adj = ((uintptr_t) attr->stackaddr - TLS_TCB_SIZE)& __static_tls_align_m1;pd = (struct pthread *) ((uintptr_t) attr->stackaddr- TLS_TCB_SIZE - adj);memset (pd, '\0', sizeof (struct pthread));pd->specific[0] = pd->specific_1stblock;pd->stackblock = (char *) attr->stackaddr - size;pd->stackblock_size = size;pd->user_stack = true;pd->header.multiple_threads = 1;pd->pid = THREAD_GETMEM (THREAD_SELF, pid);pd->setxid_futex = -1;_dl_allocate_tls (pd);list_add (&pd->list, &__stack_user);}

首先,如果属性attr中没有设置堆栈大小stacksize,则使用默认值__default_stacksize,在不同的cpu体系结构中的默认值不一样。__default_stacksize变量在__pthread_initialize_minimal_internal函数中根据系统的限制值计算得出。而__pthread_initialize_minimal_internal会在main函数前调用,因此在main函数前就为linux线程做了初始化工作,具体可参考《__pthread_initialize_minimal_internal源码分析》。

ATTR_FLAG_STACKADDR标志位置位表示由用户指定栈的地址空间。如果用户指定了堆栈大小,就检查该堆栈大小是否小于__static_tls_size + MINIMAL_REST_STACK。MINIMAL_REST_STACK在x64系统中的默认值为2048。__static_tls_size由__pthread_initialize_minimal_internal的_dl_get_tls_static_info函数赋值并对齐,其默认值为 GL(dl_tls_static_size),而GL(dl_tls_static_size)也是在__pthread_initialize_minimal_internal中初始化,如果可执行文件中并没有定义tls段,则该变量的默认值为初始化的2048个字节加上pthread结构的大小(参考init_static_tls函数)。

第一个if循环表示由用户指定栈的最高地址stackaddr(下面假设栈由高地址向低地址拓展),TLS_TCB_SIZE宏表示pthread结构的大小。

# define TLS_TCB_SIZE sizeof (struct pthread)

接下来计算即将在栈上初始化的pthread结构按照__static_tls_align_m1对齐后,需要扣除多少地址adj。
然后再计算在栈上分配的pthread结构的地址pd,并通过memset函数将其清0,可以看出pthread结构在新线程的栈底。

specific_1stblock和specific在pthread结构中的定义如下,

  struct pthread_key_data{uintptr_t seq;void *data;} specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];

PTHREAD_KEY_2NDLEVEL_SIZE宏的默认值为32。specific相当于二维数组,specific_1stblock为第一组的数组,当第一次超过PTHREAD_KEY_2NDLEVEL_SIZE时,就要分配新的pthread_key_data数组,并将specific[1]指向新分配的数组,以此类推。

再往下继续设置stackblock记录线程栈的低端地址也即起始地址,设置stackblock_size记录线程栈的大小。
接下来的user_stack成员变量标识这是一个根据用户提供的线程栈。

THREAD_SELF返回调用进程的pthread结构,再通过THREAD_GETMEM宏获取其中的pid值,因此谁调用了pthread_create函数,就初始化为谁的pid,也即当前进程的pid。

接下来设置setxid_futex防止setxid函数的调用,这里和同步机制有关,回头碰到了再研究。

再往下通过_dl_allocate_tls函数在pthread结构中分配dtv并初始化。
然后将刚刚初始化的pthread结构添加到全局的__stack_user链表中。

__pthread_create_2_1->allocate_stack->_dl_allocate_tls
elf/dl-tls.c

void *internal_function _dl_allocate_tls (void *mem)
{return _dl_allocate_tls_init (allocate_dtv (mem));
}static void *internal_function allocate_dtv (void *result)
{dtv_t *dtv;size_t dtv_length;dtv_length = GL(dl_tls_max_dtv_idx) + DTV_SURPLUS;dtv = calloc (dtv_length + 2, sizeof (dtv_t));dtv[0].counter = dtv_length;INSTALL_DTV (result, dtv);return result;
}

首先计算即将分配的dtv的个数dtv_length,其中dl_tls_max_dtv_idx的值在__pthread_initialize_minimal_internal函数中被初始化为1,DTV_SURPLUS是额外需要分配的内存。
dtv结构的定义如下,

typedef union dtv
{size_t counter;struct{void *val;bool is_static;} pointer;
} dtv_t;

接下来通过calloc分配dtv数组内存,返回内存的起始指针dtv。注意这里多分配了两个dtv,其中dtv数组的第一个项用来记录dtv数组的有效大小dtv_length,因此dtv是一个union结构。
INSTALL_DTV将刚刚分配的dtv数组设置到pthread结构中。

# define INSTALL_DTV(descr, dtvp) ((tcbhead_t *) (descr))->dtv = (dtvp) + 1

注意INSTALL_DTV宏是将dtv数组的第二个元素设置到pthread结构的dtv成员变量中。

__pthread_create_2_1->allocate_stack->_dl_allocate_tls->_dl_allocate_tls_init
elf/dl-tls.c

void * internal_function _dl_allocate_tls_init (void *result)
{dtv_t *dtv = GET_DTV (result);struct dtv_slotinfo_list *listp;size_t total = 0;size_t maxgen = 0;listp = GL(dl_tls_dtv_slotinfo_list);while (1){size_t cnt;for (cnt = total == 0 ? 1 : 0; cnt < listp->len; ++cnt){struct link_map *map;void *dest;if (total + cnt > GL(dl_tls_max_dtv_idx))break;map = listp->slotinfo[cnt].map;maxgen = MAX (maxgen, listp->slotinfo[cnt].gen);if (map->l_tls_offset == NO_TLS_OFFSET){dtv[map->l_tls_modid].pointer.val = TLS_DTV_UNALLOCATED;dtv[map->l_tls_modid].pointer.is_static = false;continue;}dest = (char *) result - map->l_tls_offset;dtv[map->l_tls_modid].pointer.val = dest;dtv[map->l_tls_modid].pointer.is_static = true;memset (__mempcpy (dest, map->l_tls_initimage,map->l_tls_initimage_size), '\0',map->l_tls_blocksize - map->l_tls_initimage_size);}total += cnt;if (total >= GL(dl_tls_max_dtv_idx))break;listp = listp->next;}dtv[0].counter = maxgen;return result;
}

传入的参数result为pthread结构的地址。
GET_DTV宏和INSTALL_DTV宏相反,用来获取pthread结构中的dtv结构。

# define GET_DTV(descr) (((tcbhead_t *) (descr))->dtv)

该dtv结构在前面分析的allocate_dtv函数中分配并初始化。
dl_tls_dtv_slotinfo_list为static_slotinfo中的si成员变量,类型为dtv_slotinfo_list,定义如下

  EXTERN struct dtv_slotinfo_list{size_t len;struct dtv_slotinfo_list *next;struct dtv_slotinfo{size_t gen;struct link_map *map;} slotinfo[0];} *_dl_tls_dtv_slotinfo_list;

该结构中的len变量记录了slotinfo中数组的长度,根据《__pthread_initialize_minimal_internal源码分析》中的init_slotinfo函数,dl_tls_dtv_slotinfo_list结构中的slotinfo数组的第二项也即slotinfo[1]的ink_map变量存储了程序初始化时的static_map。

接下来遍历dl_tls_dtv_slotinfo_list中的每个dtv_slotinfo_list结构的每个slotinfo,如果对应的link_map中的l_tls_offset为NO_TLS_OFFSET,则清空pthread结构中对应位置上dtv结构,否则将tls段的数据也即l_tls_initimage拷贝到对应的dtv结构中。

__pthread_create_2_1->allocate_stack 第二部分
glibc nptl/allocatestack.c

static int allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,ALLOCATE_STACK_PARMS)
{...if (__builtin_expect (attr->flags & ATTR_FLAG_STACKADDR, 0)){...}else{size_t guardsize;size_t reqsize;void *mem;const int prot = (PROT_READ | PROT_WRITE| ((GL(dl_stack_flags) & PF_X) ? PROT_EXEC : 0));size &= ~__static_tls_align_m1;guardsize = (attr->guardsize + pagesize_m1) & ~pagesize_m1;if (__builtin_expect (size < ((guardsize + __static_tls_size+ MINIMAL_REST_STACK + pagesize_m1)& ~pagesize_m1),0))return EINVAL;reqsize = size;pd = get_cached_stack (&size, &mem);if (pd == NULL){mem = mmap (NULL, size, prot,MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);pd = (struct pthread *) ((char *) mem + size) - 1;pd->stackblock = mem;pd->stackblock_size = size;pd->specific[0] = pd->specific_1stblock;pd->header.multiple_threads = 1;pd->setxid_futex = -1;pd->pid = THREAD_GETMEM (THREAD_SELF, pid);_dl_allocate_tls (pd);stack_list_add (&pd->list, &stack_used);if (__builtin_expect ((GL(dl_stack_flags) & PF_X) != 0&& (prot & PROT_EXEC) == 0, 0)){change_stack_perm (pd);}}

第二种情况是用户没有提供新线程的栈空间。

此时首先将size对齐,static_tls_align_m1在__pthread_initialize_minimal_internal中的_dl_get_tls_static_info函数被初始化,默认为__alignof (struct pthread)。
再往下将guardsize按页pagesize_m1对齐,前面看到传入的参数属性attr中的guardsize成员变量默认为1,guardsize为一个页面。
再往下检查即将分配的栈的大小size是否充足。

get_cached_stack从stack_cache缓存链表中获得空闲的栈和栈的大小,分别保存在mem和size中。
如果从缓存中没有找到合适的栈,就通过mmap函数在堆上分配size大小内存空间,然后在栈底初始化一个pthread结构,重点是设置了栈的起始地址(最低地址)和大小到stackblock和stackblock_size成员变量中,并且将pthread结构的pid设置为调用进程的pid。

_dl_allocate_tls函数分配并初始化pthread结构的dtv数组,该函数在前面已经分析过了。

再往下如果有别的进程修改了dl_stack_flags变量,即将原来对应的PF_X的值由0修改为了1,就要修改内存的属性。change_stack_perm函数内部通过mprotect系统调用在栈对应的内存空间增加PF_X属性,表示对应的内存段可执行。

__pthread_create_2_1->allocate_stack->get_cached_stack
glibc nptl/allocatestack.c

static struct pthread * get_cached_stack (size_t *sizep, void **memp)
{size_t size = *sizep;struct pthread *result = NULL;list_t *entry;list_for_each (entry, &stack_cache){struct pthread *curr;curr = list_entry (entry, struct pthread, list);if (FREE_P (curr) && curr->stackblock_size >= size){if (curr->stackblock_size == size){result = curr;break;}if (result == NULL|| result->stackblock_size > curr->stackblock_size)result = curr;}}if (__builtin_expect (result == NULL, 0)|| __builtin_expect (result->stackblock_size > 4 * size, 0))
      return NULL;result->setxid_futex = -1;stack_list_del (&result->list);stack_list_add (&result->list, &stack_used);stack_cache_actsize -= result->stackblock_size;*sizep = result->stackblock_size;*memp = result->stackblock;result->cancelhandling = 0;result->cleanup = NULL;result->nextevent = NULL;dtv_t *dtv = GET_DTV (result);for (size_t cnt = 0; cnt < dtv[-1].counter; ++cnt)if (! dtv[1 + cnt].pointer.is_static&& dtv[1 + cnt].pointer.val != TLS_DTV_UNALLOCATED)free (dtv[1 + cnt].pointer.val);memset (dtv, '\0', (dtv[-1].counter + 1) * sizeof (dtv_t));_dl_allocate_tls_init (result);
  return result;
}

stack_cache被初始化为一个链表头,即其next变量指向自身。

static LIST_HEAD (stack_cache);

list_for_each宏遍历该链表,再利用list_entry宏根据entry指针获得pthread的结构指针。pthread结构中的list结构变量组成链表stack_cache,因此获得list地址entry之后,将其减去list变量在pthread结构中的偏移,就获得了pthread结构的起始地址。
FREE_P宏检查该pthread结构是否在使用中,其实是检查pthread结构的tid成员变量是否小于等于0。
如果有空闲的pthread结构,并且其栈的大小stackblock_size等于即将建立的栈的大小size,就直接使用该pthread结构,否则就选择所有栈大于即将分配的栈的大小size中最小的一个空闲的pthread结构。
再往下如果没有空闲的pthread结构,或者空闲的pthread结构大小大于四倍的需求大小size,就返回null。

如果从链表stack_cache中找到了合适的pthread结构,接下来就重新初始化该结构,将其从链表stack_cache中删除,加入stack_used链表中,表示该pthread结构已经在使用中了。
stack_cache_actsize记录了stack_cache链表中所有空闲pthread结构的stackblock_size大小的和,因此将其减去即将使用的pthread结构的stackblock_size大小。
然后设置传入的参数sizep和mem,分别表示新的栈的大小和起始地址,用于返回。

再往下清空pthread结构的dtv变量,注意dtv数组的第一个项是用来记录dtv数组长度的,因此从第二个项开始释放内存。然后遍历每个项,通过free函数将非静态并且已经分配内存的dtv_t结构释放,最后通过memset函数清空该数组,注意这里讲counter加1是因为allocate_dtv函数中通过calloc分配dtv内存时多分配了一块内存。

最后通过_dl_allocate_tls_init初始化该结构,该函数在前面已经分析过了,然后返回刚刚从stack_cache中找到的pthread结构。

__pthread_create_2_1->allocate_stack 第三部分
glibc nptl/allocatestack.c

static int
allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,ALLOCATE_STACK_PARMS)
{...if (__builtin_expect (attr->flags & ATTR_FLAG_STACKADDR, 0)){...}else{...pd = get_cached_stack (&size, &mem);if (pd == NULL){...}if (__builtin_expect (guardsize > pd->guardsize, 0)){char *guard = mem;if (mprotect (guard, guardsize, PROT_NONE) != 0){...}pd->guardsize = guardsize;}else if (__builtin_expect (pd->guardsize - guardsize > size - reqsize,0)){if (mprotect ((char *) mem + guardsize, pd->guardsize - guardsize,prot) != 0)goto mprot_error;pd->guardsize = guardsize;}pd->reported_guardsize = guardsize;}*pdp = pd;stacktop = ((char *) (pd + 1) - __static_tls_size);*stack = stacktop;return 0;
}

执行到这里,已经在栈上分配了内存并在栈底初始化了pthread结构。接下来调整栈上的保护区即guardsize的大小。
第一种情况是用户需要的保护区大小guardsize大于当前pthread结构中guardsize的大小,如果pthread是刚刚在堆上通过mmap分配的,则此时pthread结构的guardsize成员变量为0,因此条件成立,此时通过mprotect函数将栈顶向上guardsize大小的内存属性设置为PROT_NONE,因此当有数据访问这段内存时,便会抛出异常,起到了保护线程栈的效果。
第二种情况是从缓存中找到的空闲的栈空间的保护区的大小大于需要的保护区的大小,此时如果空闲的栈没有足够的空间,就要通过mprotect函数删除多余的保护区大小。

最后将pthread结构的地址存入pdp指针中并返回,并设置栈的有效栈顶的地址stacktop,即栈底减去一个pthread结构的大小再减去__static_tls_size的大小,从前面的分析可知,此时有效的栈大小就为2048个字节,即_dl_tls_static_size的默认值。
最后将该地址存入传入的参数stack中并返回。

__pthread_create_2_1第二部分
nptl/pthread_create.c

  struct pthread *self = THREAD_SELF;pd->flags = ((iattr->flags & ~(ATTR_FLAG_SCHED_SET | ATTR_FLAG_POLICY_SET))| (self->flags & (ATTR_FLAG_SCHED_SET | ATTR_FLAG_POLICY_SET)));pd->joinid = iattr->flags & ATTR_FLAG_DETACHSTATE ? pd : NULL;pd->eventbuf = self->eventbuf;pd->schedpolicy = self->schedpolicy;pd->schedparam = self->schedparam;if (attr != NULL&& __builtin_expect ((iattr->flags & ATTR_FLAG_NOTINHERITSCHED) != 0, 0)&& (iattr->flags & (ATTR_FLAG_SCHED_SET | ATTR_FLAG_POLICY_SET)) != 0){INTERNAL_SYSCALL_DECL (scerr);if (iattr->flags & ATTR_FLAG_POLICY_SET)pd->schedpolicy = iattr->schedpolicy;else if ((pd->flags & ATTR_FLAG_POLICY_SET) == 0){pd->schedpolicy = INTERNAL_SYSCALL (sched_getscheduler, scerr, 1, 0);pd->flags |= ATTR_FLAG_POLICY_SET;}if (iattr->flags & ATTR_FLAG_SCHED_SET)memcpy (&pd->schedparam, &iattr->schedparam,sizeof (struct sched_param));else if ((pd->flags & ATTR_FLAG_SCHED_SET) == 0){INTERNAL_SYSCALL (sched_getparam, scerr, 2, 0, &pd->schedparam);pd->flags |= ATTR_FLAG_SCHED_SET;}int minprio = INTERNAL_SYSCALL (sched_get_priority_min, scerr, 1,iattr->schedpolicy);int maxprio = INTERNAL_SYSCALL (sched_get_priority_max, scerr, 1,iattr->schedpolicy);if (pd->schedparam.sched_priority < minprio|| pd->schedparam.sched_priority > maxprio){...return EINVAL;}}*newthread = (pthread_t) pd;return create_thread (pd, iattr, STACK_VARIABLES_ARGS);
}

首先通过THREAD_SELF获取当前进程的pthread结构。
接下来当前pthread结构的ATTR_FLAG_SCHED_SET和ATTR_FLAG_POLICY_SET值设置flag。
然后设置pthread的各个成员变量,其中schedpolicy表示线程的调度策略,schedparam表示线程调度的优先级,首先使用调用进程的schedpolicy和schedparam进行赋值。

如果用户指定了ATTR_FLAG_NOTINHERITSCHED标志,则表示新的线程不使用调用进程的调度策略,并且用户指定了ATTR_FLAG_SCHED_SET和ATTR_FLAG_POLICY_SET其中一个,此时就要重新设置新线程的调度策略。
如果用户指定了ATTR_FLAG_POLICY_SET标志位,则直接使用用户指定的调度策略schedpolicy,否则,如果调用进程的ATTR_FLAG_POLICY_SET标志位被置位,则新线程通过sched_getscheduler系统调用获得当前进程的调度策略。
类似的,如果用户指定了ATTR_FLAG_SCHED_SET标志位,则直接使用用户指定的调度优先级schedparam,否则,如果调用进程的ATTR_FLAG_SCHED_SET标志位被置位,则新线程通过sched_getparam系统调用获得当前进程的调度优先级。
再往下检查新线程的调度优先级是否在合理的范围内,分别通过sched_get_priority_min和sched_get_priority_max函数获得优先级的最小值和最大值。

最后将前面创建的pthread结构赋值给传入的参数newthread,最后调用create_thread继续创建线程。create_thread函数留在下一章继续分析。

__pthread_create_2_1->sched_getscheduler
linux kernel/sched/core.c

SYSCALL_DEFINE1(sched_getscheduler, pid_t, pid)
{struct task_struct *p;int retval = -ESRCH;p = find_process_by_pid(pid);if (p) {retval = p->policy | (p->sched_reset_on_fork ? SCHED_RESET_ON_FORK : 0);}return retval;
}

传入的参数pid为0,因此find_process_by_pid函数会简单返回当前进程的task_struct结构,然后获得当前进程的调度策略policy并返回。

__pthread_create_2_1->sched_get_priority_min
linux kernel/sched/core.c

SYSCALL_DEFINE1(sched_get_priority_min, int, policy)
{int ret = -EINVAL;switch (policy) {case SCHED_FIFO:case SCHED_RR:ret = 1;break;case SCHED_DEADLINE:case SCHED_NORMAL:case SCHED_BATCH:case SCHED_IDLE:ret = 0;}return ret;
}

sched_get_priority_min系统调用返回调度优先级的最小值,当调度策略为SCHED_FIFO或SCHED_RR时,默认的调度优先级最小值为1。

__pthread_create_2_1->sched_get_priority_max
linux kernel/sched/core.c

SYSCALL_DEFINE1(sched_get_priority_max, int, policy)
{int ret = -EINVAL;switch (policy) {case SCHED_FIFO:case SCHED_RR:ret = MAX_USER_RT_PRIO-1;break;case SCHED_DEADLINE:case SCHED_NORMAL:case SCHED_BATCH:case SCHED_IDLE:ret = 0;break;}return ret;
}

MAX_USER_RT_PRIO的默认值为100,因此当调度策略为SCHED_FIFO或SCHED_RR时,调度优先级的最大值为99。

pthread_create源码分析相关推荐

  1. HTTP服务器的本质:tinyhttpd源码分析及拓展

    已经有一个月没有更新博客了,一方面是因为平时太忙了,另一方面是想积攒一些干货进行分享.最近主要是做了一些开源项目的源码分析工作,有c项目也有python项目,想提升一下内功,今天分享一下tinyhtt ...

  2. memcached 源码分析

    1.Memcached概述 memcached是一个高性能的分布式内存缓存服务器,memcached在Linux上可以通过yum命令安装,这样方便很多,在生产环境下建议用Linux系统,memcach ...

  3. UDT 最新源码分析(三) -- UDT Socket 相关函数

    UDT 最新源码分析 -- UDT Socket 相关函数 UDT socket 建立与使用 主要流程 C/S 模式 Rendezvous 模式 UDT epoll UDT socket 创建 UDT ...

  4. UDT 最新源码分析(五) -- 网络数据收发

    UDT 最新源码分析 -- 网络数据收发 从接口实现看 UDT 网络收发 UDT 发送 send / sendmsg / sendfile UDT 接收 recv /recvmsg /recvfile ...

  5. UDT 最新源码分析(二) -- 开始与终止

    UDT 最新源码分析 -- 开始与终止 UDT 开始与终止 开始流程 终止流程 UDT 开始与终止 开始流程 UDT:: startup -> CUDT::startup -> CUDTU ...

  6. SRS4.0源码分析-RTMP入口

    本文采用的 SRS 版本是 4.0-b8 , 下载地址:github 上篇文章 <SRS4.0源码分析-main> 讲解了 SRS main 函数的基本流程,但是可能有些朋友还是比较懵逼. ...

  7. Android——RIL 机制源码分析

    Android 电话系统框架介绍 在android系统中rild运行在AP上,AP上的应用通过rild发送AT指令给BP,BP接收到信息后又通过rild传送给AP.AP与BP之间有两种通信方式: 1. ...

  8. Clamav杀毒软件源码分析笔记 六

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! Clam ...

  9. 第6季2:基于RTSP协议的实时视频流传输的源码分析

    以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除. 前言 博文第一季2:HI3518EV200的初体验中,所提供的测试文件sample_venc实现了基于RTSP协议的实时视频流传输功能. ...

最新文章

  1. 【硬件基础】制作直流电源
  2. 西瓜书公式推导讲解来了!
  3. ORACEL游标的使用实例
  4. s4-4 以太网概述
  5. 方法的反射---反射学习笔记(二)
  6. C# WebBrowser 设置独立的代理
  7. 七夕-探探小卡片鸿蒙版
  8. Android2D绘图四
  9. libiconv交叉移植
  10. 开课吧python小课学了有用吗-好消息!今天,审计、会计、税务、财务主管彻底沸腾了……...
  11. Anbox之Ubuntu18.04安装(二)
  12. 排序算法入门之堆排序
  13. 软件测试-----经常问道的面试题目
  14. android 页面跳转代码
  15. 3500字干货!精准解决3大难题,助力服装行业数字化转型
  16. E. Arranging The Sheep
  17. uniapp APP消息推送方案
  18. java将uuid转换成大写,python生成大写32位uuid代码
  19. 程序员学历不好是硬伤?苹果公司 50% 员工没大学学历
  20. 多线程启动停止暂停继续

热门文章

  1. 淘宝系统 B2C电子商务系统UML建模 范例
  2. ctf题库--这是什么鬼东西
  3. 安装64位win7(适合没有4G以上U盘或DVD光驱)
  4. android6.0的root工具,安卓6.0怎么root?安卓6.0 root教程!
  5. echart 广州3d_vue echarts 3D地图+省+弹窗
  6. 旧金山州立大学计算机科学专业,旧金山州立大学
  7. FreeRtos软件定时器复习
  8. ArrayMap源码注释
  9. Java创建学生喂养动物类
  10. cmd下提示“不是内部或外部命令,也不是可运行的程序或批处理文件