2019独角兽企业重金招聘Python工程师标准>>>

引自:http://blog.csdn.net/ren911/article/details/6652395

1 Asterisk项目概述

Asterisk是一个开源的软件包,通常运行在Linux操作系统平台上。Asterisk可以用三种协议来实现VoIP,同时可以与目前电话使用的标准硬件进行交互通信,Asterisk在实现VoIP时,不需要任何附加硬件,本文所采用的也是这种使用方式。但是,如果企业没有与VoIP语音网关运营商建立合作关系,想要自己构建这样的一个平台,那么要和数字电话设备与模拟电话设备进行交互通信,Asterisk需要一个PCI硬件的支持,这个硬件生产商中最著名的是Digium平台提供的。

Asterisk 的结构基本上是十分简单,但是它不同于大多数的电话产品。基本上,Asterisk担任的是一个中间件的功能,它连接了底层的电话技术和上层的电话应用。所以,Asterisk 具有很大的柔韧性,特殊的API接口都围绕着PBX核心系统。这个核心处理着PBX内部之间的相互联系。每一部分都是清晰来自于协议、编码或内部电话使用的硬件接口的抽象。这些抽象的接口使Asterisk可以与任何的硬件和技术以及将来的硬件和软件技术完美的结合。从下图可以看出,Asterisk由内部核心和外围动态可加载模块组成。内部核心由以下六个部分组成:PBX交换核心模块(PBX Switching Core)、调度和I/O管理模块(Scheduler and I/O Manager)、应用调用模块(Application Launcher)、编解码转换模块(Codec Translator)、动态模块加载器模块(Dynamic Module Loader)和CDR生成模块(CDR Core) 。

2 Asterisk二次开发概述

Asterisk是一个开源的PBX架构;但它并不是一个成品。通常情况下,由于企业应用的多样性,很难有一个成型的PBX产品可以满足企业的各种需求。传统的PBX成品,要么功能和灵活性不足,要么配置和维护复杂;而且都具有一个致命的缺点,那就是开放性、可扩展性。

Asterisk具有传统PBX无法比拟的优点,那就是其灵活性,可扩展能力;Asterisk的扩展能力是通过开放相应的架构和接口来实现的。这就意味着Asterisk是一个组件而不是一个成型的产品,Asterisk的核心提供了一个基本的可运行环境,而外围相应的能力则可以通过加载和配置相关的插件和模块来实现。

Asterisk是一个开源的PBX架构;但它并不是一个成品。Asterisk的扩展能力是通过开放相应的架构和接口来实现的。这就意味着Asterisk是一个组件而不是一个成型的产品,Asterisk的核心提供了一个基本的可运行环境,而外围相应的能力则可以通过加载和配置相关的插件和模块来实现。

因此,使用Asterisk,一定会面临二次开发问题,这些二次开发主要围绕以下几个方面:

(1)内部核心模块

①开发扩展编解码能力模块

②开发扩展相应的通道模块

(2)外围动态可加载模块

①开发应用部分

②开发外围管理部分

一般来说,Asterisk使用者很少需要去开发编解码能力模块和通道模块等内部核心模块;而需要开发最多的情况则是外围动态可加载模块,即外围管理部分和应用开发,本文也是指这些方面的开发。

3 Asterisk通道模型与呼叫流程

3.1什么是asterisk通道?

Asterisk通道是指通过asterisk建立起来的一路通话。这类通话都包含一个incoming连接和一个outbound连接。每个电话都是通过一种通道驱动程序建立起来的,比如SIP,ZAP,IAX2等等。每一类的通道驱动,都拥有自己私有的通道数据结构,这些私有的结构从属于一个通用的Asterisk通道数据结构中,具体定义在channel.h和channel.c中。

3.2基本的呼叫流程

Asterisk PBX呼叫流程如图2所示。

(1)通过Asterisk的一个电话呼叫在一个通道驱动接口上到达,如SIP Socket。

(2)通道驱动在该通道上创建一个PBX通道并启动一个pbx线程

(3)拨号方案被执行,拨号方案在一些地方通过dial应用(查看app_dial.c)

强制Asterisk创建一个呼出呼叫,一旦呼出,Asterisk会有以下两个动作将发生。

(1)Dial创建一个呼出的PBX通道并请求一种通道驱动创建一个呼叫

(2)当呼叫被应答时,Asterisk桥接媒体流,于是在第一个通道上的主叫可以和在第

二个通道也就是呼出通道上的被叫通话。

3.3详细呼叫流程分析

我们以sip的呼叫过程为例来描述,其他channel的呼叫过程基本类似。
Astersik下注册的sip用户主动发起一个呼叫的函数调用过程如下:
do_monitor->sipsock_read->handle_request->handle_request_invite->sip_new/ast_pbx_start->pbx_thread->__ast_pbx_run
-> ast_spawn_extension ->pbx_extension_helper->pbx_exec->执行dialplan

当Chan_sip模块被加载时,会启动一个独立的监听线程do_monitor,不断侦听sip端口上的外部消息;

当sip用户拨叫被叫号码后,chan_sip的do_monitor调用sipsock_read函数,在sip端口收到invite消息,然后就调用handle_request和handle_request_invite进行处理。

static void *do_monitor(void *data)
{
int res;
struct sip_pvt *sip;
struct sip_peer *peer = NULL;
time_t t;
int fastrestart = FALSE;
int lastpeernum = -1;
int curpeernum;
int reloading;
/* Add an I/O event to our SIP UDP socket */
if (sipsock > -1)
/*io.c实现了asterisk跟外部交互时的I/O管理,如chan_sip为了从外部接收SIP信令,调用ast_io_add添加IO接口,并调用ast_io_wait实现外部消息接收。*/
sipsock_read_id =ast_io_add(io, sipsock,sipsock_read,AST_IO_IN, NULL);
。。。
}
static int sipsock_read(int *id, int fd, short events, void *ignore)
{
/*当sip用户拨叫被叫号码后,chan_sip的do_monitor调用sipsock_read函数,在sip端口收到invite消息,然后就调用handle_request和handle_request_invite进行处理。*/
if (handle_request(p, &req, &sin, &recount, &nounlock) == -1) {
/* Request failed */
if (option_debug)
ast_log(LOG_DEBUG, "SIP message could not be handled, bad request: %-70.70s\n", p->callid[0] ? p->callid : "<no callid>");
}
在
static int handle_request(struct sip_pvt *p, struct sip_request *req, struct sockaddr_in *sin, int *recount, int *nounlock)
{
。。。
switch (p->method) {
case SIP_OPTIONS:
res = handle_request_options(p, req);
break;
case SIP_INVITE:
res = handle_request_invite(p, req, debug, seqno, sin, recount, e, nounlock);
break;
case SIP_REFER:
res = handle_request_refer(p, req, debug, ignore, seqno, nounlock);
break;
。。。
}

在handle_request_invite中,首先解析invite消息,对该sip用户的业务属性分析,确认被叫可达,然后就调用sip_new申请channel资源:

static int handle_request_invite(struct sip_pvt *p, struct sip_request *req, int debug, int seqno, struct sockaddr_in *sin, int *recount, char *e, int *nounlock)
{
…
/* Don't hold a sip pvt lock while we allocate a channel */
/*ast_channel_alloc定义在channel.c中,每个呼叫都会调用ast_channel_alloc来申请ast_channel*/
tmp = ast_channel_alloc(1, state, i->cid_num, i->cid_name, i->accountcode, i->exten, i->context, i->amaflags, "SIP/%s-%08x", my_name, (int)(long) i);
}
if (!tmp) {
ast_log(LOG_WARNING, "Unable to allocate AST channel structure for SIP channel\n");
ast_mutex_lock(&i->lock);
return NULL;
}
ast_mutex_lock(&i->lock);
if (ast_test_flag(&i->flags[0], SIP_DTMF) == SIP_DTMF_INFO)
/*Channel.c/channel.h定义了channel操作的结构体和接口函数。
struct ast_channel_tech结构体是所有channel都要用到的关键结构体,它定义channel操作的一系列回调函数指针,如call、hangup、answer等。每个channel模块都会定义ast_channel_tech的实体,并将各自的回调函数赋值给它。*/
tmp->tech = &sip_tech_info;
else
tmp->tech = &sip_tech;
(struct ast_channel结构体定义了channel的上下文参数,它是每个参与呼叫的channel必不可少的,都会调用ast_channel_alloc来申请)
/*channel.h*/
struct ast_channel {
/*! \brief Technology (point to channel driver) */
const struct ast_channel_tech *tech;
…
}

里面ast_channel_tech应该是最主要的一个结构体,定义呼叫流程中标准的过程有那些,在具体的chan_**文件中各协议注册自己对应这些过程的回调函数

/*channel.h*/
/*! \brief
Structure to describe a channel "technology", ie a channel driver
See for examples:
\arg chan_iax2.c - The Inter-Asterisk exchange protocol
\arg chan_sip.c - The SIP channel driver
\arg chan_zap.c - PSTN connectivity (TDM, PRI, T1/E1, FXO, FXS)
If you develop your own channel driver, this is where you
tell the PBX at registration of your driver what properties
this driver supports and where different callbacks are
implemented.
*/
struct ast_channel_tech {
const char * const type;
const char * const description;
int capabilities;/*!< Bitmap of formats this channel can handle */
int properties;/*!< Technology Properties */
/*! \brief Requester - to set up call data structures (pvt's) */
struct ast_channel *(* const requester)(const char *type, int format, void *data, int *cause);
int (* const devicestate)(void *data);/*!< Devicestate call back */
/*! \brief Start sending a literal DTMF digit 开始传送DTMF数据*/
int (* const send_digit_begin)(struct ast_channel *chan, char digit);
/*! \brief Stop sending a literal DTMF digit 停止传送DTMF数据*/
int (* const send_digit_end)(struct ast_channel *chan, char digit, unsigned int duration);
/*! \brief Call a given phone number (address, etc), but don't
take longer than timeout seconds to do so.  */
int (* const call)(struct ast_channel *chan, char *addr, int timeout);
/*! \brief Hangup (and possibly destroy) the channel 挂断通道*/
int (* const hangup)(struct ast_channel *chan);
/*! \brief Answer the channel 响应通道*/
int (* const answer)(struct ast_channel *chan);
/*! \brief Read a frame, in standard format (see frame.h) 以标准祯格式读一个祯*/
struct ast_frame * (* const read)(struct ast_channel *chan);
/*! \brief Write a frame, in standard format (see frame.h) 以标准祯格式写一个祯*/
int (* const write)(struct ast_channel *chan, struct ast_frame *frame);
/*! \brief Display or transmit text 显示或发送文本*/
int (* const send_text)(struct ast_channel *chan, const char *text);
/*! \brief Display or send an image 显示或发送图片*/
int (* const send_image)(struct ast_channel *chan, struct ast_frame *frame);
/*! \brief Send HTML data 发送HTML数据*/
int (* const send_html)(struct ast_channel *chan, int subclass, const char *data, int len);
/*! \brief Handle an exception, reading a frame 处理读取祯时发生的异常*/
struct ast_frame * (* const exception)(struct ast_channel *chan);
/*! \brief Bridge two channels of the same type together 桥接两种相同类型的通道*/
enum ast_bridge_result (* const bridge)(struct ast_channel *c0, struct ast_channel *c1, int flags,
struct ast_frame **fo, struct ast_channel **rc, int timeoutms);
/*! \brief Indicate a particular condition (e.g. AST_CONTROL_BUSY or AST_CONTROL_RINGING or AST_CONTROL_CONGESTION 指示一种特殊的状况如sip的486error*/
int (* const indicate)(struct ast_channel *c, int condition, const void *data, size_t datalen);
/*! \brief Fix up a channel:  If a channel is consumed, this is called.  Basically update any ->owner links */
int (* const fixup)(struct ast_channel *oldchan, struct ast_channel *newchan);
/*! \brief Set a given option */
int (* const setoption)(struct ast_channel *chan, int option, void *data, int datalen);
/*! \brief Query a given option */
int (* const queryoption)(struct ast_channel *chan, int option, void *data, int *datalen);
/*! \brief Blind transfer other side (see app_transfer.c and ast_transfer() */
int (* const transfer)(struct ast_channel *chan, const char *newdest);
/*! \brief Write a frame, in standard format */
int (* const write_video)(struct ast_channel *chan, struct ast_frame *frame);
/*! \brief Find bridged channel 查询已经找到的通道*/
struct ast_channel *(* const bridged_channel)(struct ast_channel *chan, struct ast_channel *bridge);
/*! \brief Provide additional read items for CHANNEL() dialplan function */
int (* func_channel_read)(struct ast_channel *chan, char *function, char *data, char *buf, size_t len);
/*! \brief Provide additional write items for CHANNEL() dialplan function */
int (* func_channel_write)(struct ast_channel *chan, char *function, char *data, const char *value);
/*! \brief Retrieve base channel (agent and local) */
struct ast_channel* (* get_base_channel)(struct ast_channel *chan);
/*! \brief Set base channel (agent and local) */
int (* set_base_channel)(struct ast_channel *chan, struct ast_channel *base);
};

如在chan_sip.c文件中实现如下:

/*! \brief Definition of this channel for PBX channel registration */
static const struct ast_channel_tech sip_tech = {
.type = "SIP",
.description = "Session Initiation Protocol (SIP)",
.capabilities = ((AST_FORMAT_MAX_AUDIO << 1) - 1),
.properties = AST_CHAN_TP_WANTSJITTER | AST_CHAN_TP_CREATESJITTER,
.requester = sip_request_call,
.devicestate = sip_devicestate,
.call = sip_call,
.hangup = sip_hangup,
.answer = sip_answer,
.read = sip_read,
.write = sip_write,
.write_video = sip_write,
.indicate = sip_indicate,
.transfer = sip_transfer,
.fixup = sip_fixup,
.send_digit_begin = sip_senddigit_begin,
.send_digit_end = sip_senddigit_end,
.bridge = ast_rtp_bridge,
.send_text = sip_sendtext,
.func_channel_read = acf_channel_read,
};

并调用ast_pbx_start函数启动一个pbx_thread线程来专门处理该呼叫。

static struct ast_channel *sip_new(struct sip_pvt *i, int state, const char *title)
{
…
tmp = ast_channel_alloc(…
…
/*ast_pbx_start函数启动一个pbx_thread线程来专门处理该呼叫*/
if (state != AST_STATE_DOWN &&ast_pbx_start(tmp)) {
ast_log(LOG_WARNING, "Unable to start PBX on %s\n", tmp->name);
tmp->hangupcause = AST_CAUSE_SWITCH_CONGESTION;
ast_hangup(tmp);
tmp = NULL;
}
…
}
enum ast_pbx_result ast_pbx_start(struct ast_channel *c)
{
pthread_t t;
pthread_attr_t attr;
if (!c) {
ast_log(LOG_WARNING, "Asked to start thread on NULL channel?\n");
return AST_PBX_FAILED;
}
if (increase_call_count(c))
return AST_PBX_CALL_LIMIT;
/* Start a new thread, and get something handling this channel. */
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (ast_pthread_create(&t, &attr,pbx_thread, c)) {
ast_log(LOG_WARNING, "Failed to create new channel thread\n");
pthread_attr_destroy(&attr);
return AST_PBX_FAILED;
}
pthread_attr_destroy(&attr);
return AST_PBX_SUCCESS;
}

pbx_thread线程调用__ast_pbx_run。

static void *pbx_thread(void *data)
{
/* Oh joyeous kernel, we're a new thread, with nothing to do but
answer this channel and get it going.
*/
/* NOTE:
The launcher of this function _MUST_ increment 'countcalls'
before invoking the function; it will be decremented when the
PBX has finished running on the channel
*/
struct ast_channel *c = data;
__ast_pbx_run(c);
decrease_call_count();
pthread_exit(NULL);
return NULL;
}

__ast_pbx_run是一个衔接dialplan和内核的关键函数,它首先调用ast_exists_extension函数,根据分机号码的context属性,匹配到对应的dialplan;然后进入一个for死循环,逐条执行dialplan对应的context中的语句。
pbx_extension_helper函数调用pbx_extension_helper,在pbx_extension_helper中调用pbx_find_extension找到对应的context后,通过verbose打印dialplan执行语句“Executing ……”,同时调用pbx_exec执行该dialplan。执行到dial语句呼叫被叫。

static int __ast_pbx_run(struct ast_channel *c)
{
…
while (ast_exists_extension(c, c->context, c->exten, c->priority, c->cid.cid_num)) {
…
}
int ast_exists_extension(struct ast_channel *c, const char *context, const char *exten, int priority, const char *callerid)
{
returnpbx_extension_helper(c, NULL, context, exten, priority, NULL, callerid, E_MATCH);
}
/*!
* \brief The return value depends on the action:
*
* E_MATCH, E_CANMATCH, E_MATCHMORE require a real match,
*and return 0 on failure, -1 on match;
* E_FINDLABEL maps the label to a priority, and returns
*the priority on success, ... XXX
* E_SPAWN, spawn an application,
*and return 0 on success, -1 on failure.
*
* \note The channel is auto-serviced in this function, because doing an extension
* match may block for a long time.  For example, if the lookup has to use a network
* dialplan switch, such as DUNDi or IAX2, it may take a while.  However, the channel
* auto-service code will queue up any important signalling frames to be processed
* after this is done.
*/
static int pbx_extension_helper(struct ast_channel *c, struct ast_context *con,
const char *context, const char *exten, int priority,
const char *label, const char *callerid, enum ext_match_t action)
{
…
ast_rdlock_contexts();
e =pbx_find_extension(c, con, &q, context, exten, priority, label, callerid, action);
if (e) {
if (matching_action) {
ast_unlock_contexts();
return -1;/* success, we found it */
} else if (action == E_FINDLABEL) { /* map the label to a priority */
res = e->priority;
ast_unlock_contexts();
return res;/* the priority we were looking for */
} else {/* spawn */
app = pbx_findapp(e->app);
ast_unlock_contexts();
if (!app) {
ast_log(LOG_WARNING, "No application '%s' for extension (%s, %s, %d)\n", e->app, context, exten, priority);
return -1;
}
if (c->context != context)
ast_copy_string(c->context, context, sizeof(c->context));
if (c->exten != exten)
ast_copy_string(c->exten, exten, sizeof(c->exten));
c->priority = priority;
'''/*passdata应该是在此赋值*/'''
pbx_substitute_variables(passdata, sizeof(passdata), c, e);
if (option_debug) {
ast_log(LOG_DEBUG, "Launching '%s'\n", app->name);
}
if (option_verbose > 2) {
char tmp[80], tmp2[80], tmp3[EXT_DATA_SIZE];
ast_verbose( VERBOSE_PREFIX_3 "Executing [%s@%s:%d] %s(\"%s\", \"%s\") %s\n",
exten, context, priority,
term_color(tmp, app->name, COLOR_BRCYAN, 0, sizeof(tmp)),
term_color(tmp2, c->name, COLOR_BRMAGENTA, 0, sizeof(tmp2)),
term_color(tmp3, passdata, COLOR_BRMAGENTA, 0, sizeof(tmp3)),
"in new stack");
}
/* 管理事件 */
manager_event(EVENT_FLAG_CALL, "Newexten",
"Channel: %s\r\n"
"Context: %s\r\n"
"Extension: %s\r\n"
"Priority: %d\r\n"
"Application: %s\r\n"
"AppData: %s\r\n"
"Uniqueid: %s\r\n",
c->name, c->context, c->exten, c->priority, app->name, passdata, c->uniqueid);
returnpbx_exec(c, app, passdata);/* 0 on success, -1 on failure */
…
}

Pbx_exec函数体为:

/*
\note This function is special. It saves the stack so that no matter
how many times it is called, it returns to the same place */
int pbx_exec(struct ast_channel *c, /*!< Channel */
struct ast_app *app,/*!< Application */
void *data)/*!< Data for execution */
{
int res;
const char *saved_c_appl;
const char *saved_c_data;
if (c->cdr && !ast_check_hangup(c))
ast_cdr_setapp(c->cdr, app->name, data);
/* save channel values */
saved_c_appl= c->appl;
saved_c_data= c->data;
c->appl = app->name;
c->data = data;
/* XXX remember what to to when we have linked apps to modules */
if (app->module) {
/* XXX LOCAL_USER_ADD(app->module) */
}
/*应该是在此执行了应用*/
res = app->execute(c, S_OR(data, ""));
if (app->module) {
/* XXX LOCAL_USER_REMOVE(app->module) */
}
/* restore channel values */
c->appl = saved_c_appl;
c->data = saved_c_data;
return res;
}

在等待被叫接通的过程中,完成媒体协商过程,向主叫发送180、200OK消息接通呼叫。

当其他用户呼叫asterisk的sip用户时,函数调用过程如下:Dial->dial_exec->dial_exec_full->ast_request/ast_call/wait_for_answer/ ast_bridge_call
呼叫执行到dial时,pbx_exec调用application dial的接口函数dial_exec,dial_exec调用dial_exec_full。

static int load_module(void)
{
int res;
res = ast_register_application(app, dial_exec, synopsis, descrip);
res |= ast_register_application(rapp, retrydial_exec, rsynopsis, rdescrip);
return res;
}
static int dial_exec(struct ast_channel *chan, void *data)
{
struct ast_flags peerflags;
memset(&peerflags, 0, sizeof(peerflags));
return dial_exec_full(chan, data, &peerflags, NULL);
}
static int dial_exec_full(struct ast_channel *chan, void *data, struct ast_flags *peerflags, int *continue_exec)
{
。。。
if (!(c =chan->tech->requester(type, capabilities | videoformat, data, cause)))
return NULL;
。。。
}

chan->tech->requester在chan_sip.c中的ast_channel_tech实体中即为.requester = sip_request_call
在dial_exec_full中,首先调用ast_request,在ast_request调用chan_sip对应的回调函数sip_request_call为该被叫sip用户申请channel资源。然后调用ast_call,在ast_call中调用chan_sip对应的回调函数sip_call向被叫发送INVITE消息,呼叫被叫SIP用户。

static struct ast_channel *sip_request_call(const char *type, int format, void *data, int *cause)
{
…
/*申请资源*/
if (!(p = sip_alloc(NULL, NULL, 0, SIP_INVITE))) {
ast_log(LOG_ERROR, "Unable to build sip pvt data for '%s' (Out of memory or socket error)\n", (char *)data);
*cause = AST_CAUSE_SWITCH_CONGESTION;
return NULL;
}
…

然后该呼叫线程会调用wait_for_answer等待被叫接通。
在呼叫接通后,也即wait_for_answer函数返回,在dial_exec_full中调用ast_bridge_call桥接媒体,这样呼叫就正式接通了。
当chan_sip的侦听线程接收到BYE消息,则调用handle_request_bye找到相应的channel,执行hangup释放呼叫。

if (!ast_strlen_zero(get_header(req, "Also"))) {
ast_log(LOG_NOTICE, "Client '%s' using deprecated BYE/Also transfer method.  Ask vendor to support REFER instead\n",
ast_inet_ntoa(p->recv.sin_addr));
if (ast_strlen_zero(p->context))
ast_string_field_set(p, context, default_context);
res = get_also_info(p, req);
if (!res) {
c = p->owner;
if (c) {
bridged_to = ast_bridged_channel(c);
if (bridged_to) {
/* Don't actually hangup here... */
ast_queue_control(c, AST_CONTROL_UNHOLD);
ast_async_goto(bridged_to, p->context, p->refer->refer_to,1);
} else
ast_queue_hangup(p->owner);
}
} else {
ast_log(LOG_WARNING, "Invalid transfer information from '%s'\n", ast_inet_ntoa(p->recv.sin_addr));
if (p->owner)
ast_queue_hangup(p->owner);
}
} else if (p->owner) {
ast_queue_hangup(p->owner);
...
}


转载于:https://my.oschina.net/u/1537782/blog/347082

asterisk概述和代码分析相关推荐

  1. 2021SC@SDUSC山东大学软件学院软件工程应用与实践--Ebiten代码分析 源码分析(三)

    2021SC@SDUSC 目录 一.概述 二.代码分析 1.Update() 2.Draw() 3.Layout() 一.概述 本文将介绍ebiten在RunGame函数中逐帧执行的Update()方 ...

  2. 2021SC@SDUSC山东大学软件学院软件工程应用与实践--Ebiten代码分析 源码分析(四)

    2021SC@SDUSC 目录 一.概述 二.代码分析 1.graphics.QuadVertices() 2.mipmap.Mipmap() 3.i.mipmap.DrawTriangles() 一 ...

  3. 使用Hadoop和ELK进行业务代码分析!分分钟捉到Bug!

    大数据是计算领域的新高地,它有望提供一种方法来应对二十一世纪不断增长的数据生成.越来越多的大数据爱好者正在涌现,越来越多的公司正在采用各种大数据平台,并希望提出以客户为中心的解决方案,帮助他们在竞争激 ...

  4. tensorflow笔记:多层CNN代码分析

    tensorflow笔记系列:  (一) tensorflow笔记:流程,概念和简单代码注释  (二) tensorflow笔记:多层CNN代码分析  (三) tensorflow笔记:多层LSTM代 ...

  5. 模块加载过程代码分析1

    一.概述 模块是作为ELF对象文件存放在文件系统中的,并通过执行insmod程序链接到内核中.对于每个模块,系统都要分配一个包含以下数据结构的内存区. 一个module对象,表示模块名的一个以null ...

  6. Linux内核中的GPIO系统之(3):pin controller driver代码分析

    一.前言 对于一个嵌入式软件工程师,我们的软件模块经常和硬件打交道,pin control subsystem也不例外,被它驱动的硬件叫做pin controller(一般ARM soc的datash ...

  7. u-boot分析之两阶段代码分析(三)

    目录 u-boot(三)启动文件 1,概述 2,uboot第一阶段代码分析: 汇编 2,uboot第二阶段代码分析 C:_start_armboot C:main_loop u-boot(三)启动文件 ...

  8. 20145217《网络对抗》 恶意代码分析

    20145217<网络对抗> 免杀原理与实践 知识点学习总结 进行恶意代码分析之前必须具备以下知识:编程.汇编/反汇编.网络基本知识.PE文件结构以及一些常用行为分析软件. 一.在一个已经 ...

  9. pin controller driver代码分析

    一.前言 对于一个嵌入式软件工程师,我们的软件模块经常和硬件打交道,pin control subsystem也不例外,被它驱动的硬件叫做pin controller(一般ARM soc的datash ...

最新文章

  1. swif 在字符串中查找特定字符索引以及改变字符串的指定位置的颜色 字体大小
  2. java语言的数组描述_下列关于Java语言的数组描述中,错误的是()。_学小易找答案...
  3. java中的枚举类_java中的枚举类型
  4. UML大战需求分析阅读笔记——02
  5. scm maven_在运行时访问工件的Maven和SCM版本
  6. php赋值一个数组,PHP入门教程之数组的定义和赋值
  7. Linux 之 NTP 服务 服务器
  8. unity3d大型互动照片墙
  9. 深入浅出Word2Vec原理解析
  10. 软件升级 防火墙 飞塔_FortiGate软件版本升级
  11. 基2时域采样快速傅里叶变换、反变换算法在python上的实现(自制轮子)
  12. 初识语音合成软件eSpeak
  13. 如何使用JSON Web令牌(JWT)保护您的文档
  14. flv格式转换为mp4(ffmpeg)
  15. 对比自监督学习综述 - A Survey of Contrastive Self-Supervised Learning
  16. 菜农谋略:搞定牛人宋俊德,对女牛人孙昌旭说:“记住,俺是雁塔菜农~~~ ”
  17. 如何避免拼多多售后?拼多多售后有哪些规则?
  18. 使用tensorboard遇到:Output 0 of UnbindBackward is a view and is being modified inplace.....
  19. sci审稿意见_SCI审稿人的意见很多,要全盘接受吗?
  20. 工作簿(Workbook)基本操作应用示例

热门文章

  1. 服务器是多用户服务的计算机,Win10权限管理与多用户远程登录(多方案)
  2. 多个cpu+linux,linux查看多个cpu信息
  3. redis中hash类型介绍
  4. 设计模式--行为型模式
  5. s3c2440第一个裸奔程序——跑马灯(转)
  6. Docker简介以及Docker历史
  7. wkhtmltopdf的介绍与使用
  8. mongoshake实现mongodb数据同步
  9. new Class{}形式
  10. 桥接模式(Bridge)解析例子