一、clock event控制的通用逻辑

1、产生clock event的设备

各种系统的timer硬件形形色色,不过在general clock event device layer,struct clock_event_device被来抽象一个可以产生clock event的timer硬件设备,如下:

struct clock_event_device {
    void          (*event_handler)(struct clock_event_device *);
    int            (*set_next_event)(unsigned long evt,   struct clock_event_device *);
    int            (*set_next_ktime)(ktime_t expires,     struct clock_event_device *);
    ktime_t       next_event;
    u64            max_delta_ns;
    u64            min_delta_ns;
    u32            mult;
    u32            shift;
    enum clock_event_mode    mode;
    unsigned int        features;
    unsigned long        retries;

void            (*broadcast)(const struct cpumask *mask);
    void            (*set_mode)(enum clock_event_mode mode, struct clock_event_device *);
    void            (*suspend)(struct clock_event_device *);
    void            (*resume)(struct clock_event_device *);
    unsigned long        min_delta_ticks;
    unsigned long        max_delta_ticks;

const char        *name;
    int            rating;
    int            irq; -------------使用的IRQ number
    const struct cpumask    *cpumask; ----该clock event device附着在哪一个CPU core上
    struct list_head    list;
    struct module        *owner;
} ____cacheline_aligned;

在把目光投向每一个成员之前,我们先聊一聊____cacheline_aligned,其实最开始定义这个数据结构的时候没有这个属性,而且struct
clock_event_device的各个成员也不是这样排列的。为了提高cacheline hit
radio,内核工程师对这个数据结构进行的改造,一方面对齐了cache
line,另外一方面重排了那些有密切关系的成员(所谓密切关系就是指在代码执行上下文中,如果访问了A成员,有非常大的概率访问B成员)。

event_handler顾名思义就是产生了clock event的时候调用的handler。一般而言,底层的clock event chip

driver会注册中断处理函数,在硬件timer中断到来的时候调用该timer中断handler,而在这个中断handler中再调用event_handler。

既然是产生clock
event的device,那么总是要控制下一次event产生的时间点,我们有两个成员函数完成这个功能:set_next_event和set_next_ktime。set_next_ktime函数可以直接接收ktime作为参数,而set_next_event设定的counter的cycle数值。一般的timer硬件都是用cycle值设定会比较方便,当然,不排除有些奇葩可以直接使用ktime(秒、纳秒),这时候clock
event device的features成员要打上CLOCK_EVT_FEAT_KTIME的标记。

features成员是描述底层硬件的功能feature的,包括:

#define CLOCK_EVT_FEAT_PERIODIC        0x000001------具备产生周期性event的能力
#define CLOCK_EVT_FEAT_ONESHOT        0x000002-----具备产生oneshot类型event的能力
#define CLOCK_EVT_FEAT_KTIME        0x000004-------上面已经描述了

#define CLOCK_EVT_FEAT_C3STOP        0x000008
#define CLOCK_EVT_FEAT_DUMMY        0x000010

CLOCK_EVT_FEAT_C3STOP是一个很有意思的feature:内核中有一个模块叫做cpuidle
framework,当没有任务做的时候,cpu会进入idle状态。这种CPU的sleep
state叫做C-states,有C1/C2…Cn种states(具体多少种和CPU设计相关),当然不同的状态是在功耗和唤醒时间上进行平衡,CPU睡的越浅,功耗越大,但是能够很快的唤醒。一般而言,在sleep
state的CPU可以被local timer唤醒,但是,当CPU进入某个深度睡眠状态的时候,停止了local
timer的运作,这时候,local timer将无法唤醒CPU了。

kernel的注释说这是一个x86(64)的功能设计失误(misfeature),不过,在嵌入式平台上,这也可以认为是对功耗的极致追求(ARM
的generic timer也有这个misfeature,呵呵~~)。为了让系统可以继续运作,传说中tick broadcast
framework粉墨登场了。struct clock_event_device中的broadcast这个callback函数是和clock
event广播有关。在per CPU 的local
timer硬件无法正常运作的时候,需要一个独立于各个CPU的timer硬件来作为broadcast clock event
device。在这种情况下,它可以将clock event广播到所有的CPU core,以此推动各个CPU core上的tick
device的运作。当然它不是本文的主要内容,如果有兴趣可以参考本站其他文档。

clock event mode的定义如下:

enum clock_event_mode {
    CLOCK_EVT_MODE_UNUSED = 0, ------未使用
    CLOCK_EVT_MODE_SHUTDOWN, ------被软件shutdown
    CLOCK_EVT_MODE_PERIODIC, -------工作状态,处于periodic模式,周期性产生event
    CLOCK_EVT_MODE_ONESHOT, -------工作状态,处于one shot模式,event是一次性的
    CLOCK_EVT_MODE_RESUME, -------处于系统resume中
};

具体的mode设定是由set_mode这个callback函数来完成的,毫无疑问,这是需要底层的clock event chip
driver需要设定的。虽然定义了这么多mode,实际上底层的硬件未必支持,有些HW
timer硬件支持oneshot和periodic的设定,不过有些就不支持(例如:ARM general timer)。具体clock
event device如何在各个mode中切换会在tick device的文档中描述。

rating、mult和shift的概念和clocksource中的类似,这里就不描述了。此外很多成员这里也不细述了,在后面的逻辑分析中自然就清晰了。

2、如何组织系统中的clock event device?

相关的全局变量定义如下:

static LIST_HEAD(clockevent_devices);
static LIST_HEAD(clockevents_released);

clock event device core模块使用两个链表来管理系统中的clock event
device,一个是clockevent_devices链表,该链表中的clock event
device都是当前active的device。active的clock device有两种情况,一种是cpu core的current 
clockevent device,这些device是各个cpu上产生tick的那个clock event
device,还有一些active的device由于优先级比较低,当前没有使用,不过可以作为backup的device。另外一个是clockevents_released链表,这个链表中clock
event device都是由于种种原因,无法进入active list,从而挂入了该队列。

二、向上层的其它driver提供操作clock event的通用API

1、设定clock event device的触发event的时间参数

int clockevents_program_event(struct clock_event_device *dev, ktime_t expires, bool force)
{
    unsigned long long clc;
    int64_t delta;
    int rc;

dev->next_event = expires; -------设定下一次触发clock event的时间
    if (dev->features & CLOCK_EVT_FEAT_KTIME)----- -------------(1)
        return dev->set_next_ktime(expires, dev);

delta = ktime_to_ns(ktime_sub(expires, ktime_get()));----- ----------(2)
    if (delta <= 0)
        return force ? clockevents_program_min_delta(dev) : -ETIME; -----------(3)

delta = min(delta, (int64_t) dev->max_delta_ns);
    delta = max(delta, (int64_t) dev->min_delta_ns); ------------------(4)

clc = ((unsigned long long) delta * dev->mult) >> dev->shift; ---------转换成cycle值

rc = dev->set_next_event((unsigned long) clc, dev); ---调用底层driver callback函数进行设定

return (rc && force) ? clockevents_program_min_delta(dev) : rc; -----------(5)
}

我们先看看该接口函数的参数值:dev指向具体的clock event
device,expires参数是设定下一次产生event的时间点,force参数控制在expires设定异常的时候(例如设定在一个过去的时间点上产生event)该函数
的行为,一种是出错返回,另外一种还是进行event的产生,只是设定一个最小的delta。

(1)如果chip driver支持使用ktime的设定(这需要硬件支持,设定了CLOCK_EVT_FEAT_KTIME flag的那些clock event device才支持哦),事情会比较简单,直接调用set_next_ktime就搞定了。

(2)对于一个“正常”的clock event device,需要转换成cycle这样的单位。不过在转换成cycle之前,需要先将ktime格式的时间(传入的expires参数就是这样的格式)转换成纳秒这样的时间单位。

(3)delta小于0意味着用户设定的时间点已经是过去的一个时间点,如果强制产生event的话,那么事不宜迟,要立刻产生event,这需要调用clockevents_program_min_delta函数,代码如下:

static int clockevents_program_min_delta(struct clock_event_device *dev)
{
    unsigned long long clc;
    int64_t delta;

delta = dev->min_delta_ns; ---------------------------(a)
    dev->next_event = ktime_add_ns(ktime_get(), delta); ----------------(b)

if (dev->mode == CLOCK_EVT_MODE_SHUTDOWN)
        return 0;

dev->retries++; -------记录retry的次数
    clc = ((unsigned long long) delta * dev->mult) >> dev->shift; ------转换成cycle值
    return dev->set_next_event((unsigned long) clc, dev); ----调用底层driver callback函数进行设定
}

(a)在逻辑思维的世界里,你可以想像任意小的时间片段,但是在实现面,这个要收到timer硬件能力的限制。一个输入频率是1Hz的timer硬件,其最小时间粒度就是1秒,如何产生0.1秒的clock
event呢?因此,所谓立刻产出也就是在硬件允许的最小的时间点上产生event。在注册clock event
device的时候已经设定这个参数了,就是struct clock_event_device的min_delta_ns这个成员。

(b)ktime_get函数获取当前的时间点,加上min_delta_ns就是下一次要触发event的时间点,struct clock_event_device的next_event 这个成员就是用来记录下一次要触发event的时间点信息的。

(4)有最小值就有最大值,struct clock_event_device的max_delta_ns这个成员就是设定next
event触发的最大时间值。这个最大值是和硬件counter的bit数目有关:如果一个硬件timer最大能表示60秒的时间长度,那么设定65秒后触发clock

event是没有意义的,因为这时候counter会溢出,如果强行设定那么硬件实际会在5秒后触发event。同样的,max_delta_ns也是在注册clock
event device的时候设定了这个参数。

(5)如果调用底层driver
callback函数进行实际的cycle设定的时候出错(例如:由于种种原因,在实际设定的时候发现时间点是过去值,如果仍然设定,那么上层软件实际上要等到硬件counter

溢出后在下一个round中才会触发event,实际这时候黄花菜都凉了),并且是强制产生event的话,那么也需要调用clockevents_program_min_delta函数在最小时间点上产生clock
event。

2、更换clock event设备

上层的tick device驱动层会根据情况(clock event的精度,是否local cpu的)调用clockevents_exchange_device 进行clock event设备的更换,具体代码如下:

void clockevents_exchange_device(struct clock_event_device *old,  struct clock_event_device *new)
{
    unsigned long flags;

local_irq_save(flags);

if (old) { -------------------------------(1)
        module_put(old->owner);
        clockevents_set_mode(old, CLOCK_EVT_MODE_UNUSED);
        list_del(&old->list);
        list_add(&old->list, &clockevents_released);
    }

if (new) { ------------------------------(2)
        BUG_ON(new->mode != CLOCK_EVT_MODE_UNUSED);
        clockevents_shutdown(new);
    }
    local_irq_restore(flags);
}

(1)旧的clock event device要被替换掉,因此将其模式设定为CLOCK_EVT_MODE_UNUSED,并且从全局clock event device链表中摘下来,挂入clockevents_released链表

(2)我们要确保新的clock event设备没有被使用,如果新的clock
event设备不是CLOCK_EVT_MODE_UNUSED状态,说明其目前被其他的上层软件使用,因此要kernel
panic,并且shutdown该clock event
device。这是该设备状态是CLOCK_EVT_MODE_SHUTDOWN状态。这里没有插入clockevent_devices全局链表的动作,主要是因为在调用该函数之前,新的clock
event device已经挂入队列了。

三、向底层clockevent chip driver提供的接口

1、配置clock event device

底层的clock event chip
driver可以调用clockevents_config对该设备进行配置。这里的操作类似clocksource中的内容,也就是说调用者输入频率参数,在clockevents_config中计算struct
clock_event_device中一些类似mult、shift等成员的配置值:

void clockevents_config(struct clock_event_device *dev, u32 freq)
{
    u64 sec;

if (!(dev->features & CLOCK_EVT_FEAT_ONESHOT))--------------(1)
        return;

sec = dev->max_delta_ticks;-------------------------(2)
    do_div(sec, freq);
    if (!sec)
        sec = 1;-----限制最小值
    else if (sec > 600 && dev->max_delta_ticks > UINT_MAX)
        sec = 600; -------------------------------(3)

clockevents_calc_mult_shift(dev, freq, sec);------------------(4)
    dev->min_delta_ns = cev_delta2ns(dev->min_delta_ticks, dev, false);-------(5)
    dev->max_delta_ns = cev_delta2ns(dev->max_delta_ticks, dev, true);
}

(1)底层的硬件驱动知道自己的能力,可以通过struct
clock_event_device中的features成员宣称自己的功能:CLOCK_EVT_FEAT_PERIODIC说明该硬件timer可以产生周期性的clock
event,CLOCK_EVT_FEAT_ONESHOT说明自己只能产生one shot类型的clock event。如果能产生one
shot类型的event,那么即便是硬件不支持周期性的clock event,其实上层的软件可以通过不断设定next
event的方法来模拟周期性的clock event。但是如果只是支持周期性的clock event就有些麻烦了,这时候是无法模拟one
shot类型的event。也就是说,在这种情况下,整个系统必须有周期性tick,同时,系统无法支持高精度timer和dynamic
tick的情况。在这样的硬件条件下,后续的配置是没有意义的,因此直接return。

(2)底层的硬件驱动需要设定max_delta_ticks和min_delta_ticks。需要注意的是这里的tick不是system
tick,而是输入硬件timer的clock
tick,其实就是可以设定最大和最小cycle数目。这个cycle数目除以freq就是时间值,单位是秒。

(3)又是600秒,是否似曾相识。我们在clock source那篇文档中已经介绍过了,这里就不再赘述

(4)根据输入频率和最大的秒数可以计算出clock event的mult和shift这两个成员的数值,具体计算过程在clock source那篇文档中已经介绍过了,这里就不再赘述。

(5)计算min_delta_ticks和max_delta_ticks对应的ns值。

2、注册clock event device

底层的timer硬件驱动会调用clockevents_register_device函数向系统注册clock event device,代码如下:

void clockevents_register_device(struct clock_event_device *dev)
{
    unsigned long flags;

if (!dev->cpumask) {---------------------------(1)
        WARN_ON(num_possible_cpus() > 1);
        dev->cpumask = cpumask_of(smp_processor_id());
    }

raw_spin_lock_irqsave(&clockevents_lock, flags); --------------(2)

list_add(&dev->list, &clockevent_devices); ----加入clock event设备全局列表
    tick_check_new_device(dev); -----------------------(3)
    clockevents_notify_released(); ----------------------(4)

raw_spin_unlock_irqrestore(&clockevents_lock, flags);
}

(1)clock event
device的cpumask指明该设备为哪一个CPU工作,如果没有设定并且cpu的个数大于1的时候要给出warning信息并进行设定(设定为当前运行该代码的那个CPU
core)。在multi core的环境下,底层driver在调用该接口函数注册clock
event设备之前就需要设定cpumask成员,毕竟一个timer硬件附着在哪一个cpu上底层硬件最清楚,这里仅仅是亡羊补牢(毕竟我们还是希望代码不要随随便便就kernel
panic了,尽量让代码执行下去)。

(2)考虑到来自多个CPU上的并发,这里使用spin lock进行保护。关闭本地中断,可以防止来自本cpu上的并发操作。

(3)调用tick_check_new_device函数,让上层软件知道底层又注册一个新的clock
device,当然,是否上层软件要使用这个新的clock event device是上层软件的事情,clock event device
driver这个层次完成描述自己能力这部分代码就OK了。tick device相关内容请参考本站其他文档。

(4)如果上层软件想要使用新的clock event
device的话(tick_check_new_device函数中有可能会进行此操作),它会调用clockevents_exchange_device函数(可以参考上面的描述)。这时候,旧的clock

event会被从clockevent_devices链表中摘下,挂到clockevents_released队列中。在clockevents_notify_released函数中,会将old
clock event device重新挂入clockevent_devices,并调用tick_check_new_device函数。

3、配置并注册clock event device

是上面两个接口函数的综合体,这里不再详细描述。

四、用户空间接口

1、sysfs接口初始化

在系统初始化的时候会调用clockevents_init_sysfs函数(呵呵~~~对于clock
source,这个函数名字是init_clocksource_sysfs,很显然不是一个人的风格,我喜欢clockevents_sysfs_init和clocksource_sysfs_init这种函数定义风格)来初始化clock
event device layer的sys file system接口,如下:

static int __init clockevents_init_sysfs(void)
{
    int err = subsys_system_register(&clockevents_subsys, NULL); --注册clock event这种bus type

if (!err)
        err = tick_init_sysfs();
    return err;
}

该说的在clocksource那份文档中都描述了,这里直接不再赘述,重点关注tick_init_sysfs函数:

static int __init tick_init_sysfs(void)
{
    int cpu;

for_each_possible_cpu(cpu) {----------遍历各个CPU的clock event device
        struct device *dev = &per_cpu(tick_percpu_dev, cpu);--------------(1)
        int err;

dev->id = cpu;
        dev->bus = &clockevents_subsys;
        err = device_register(dev);--------------------------(2)
        if (!err)
            err = device_create_file(dev, &dev_attr_current_device);------------(3)
        if (!err)
            err = device_create_file(dev, &dev_attr_unbind_device);
        if (err)
            return err;
    }
    return tick_broadcast_init_sysfs(); -----------------------(4)
}

(1)我们始终还是要回归统一模型的三件套,bus type,device和driver。现在有了clock event bus
type,还缺少device。在clock source中,由于它是全局的,属于所有CPU
core的,因此我们定义了一个device就OK了。对于clock event,我们需要为每一个CPU定义一个device,如下:

static DEFINE_PER_CPU(struct device, tick_percpu_dev);

是否需要定义clock event device对应的driver呢?本来定义device和driver的目的是让它们在适当的时机(bus
type的match函数)相遇,相知(调用driver的probe函数)。对于clock event
device而言,是不需要定义driver的,因为设备树提供了其他的方法来进行具体底层chip级别的初始化(具体可以参考ARM generic timer驱动中的描述)。

还有一点需要注意,实际上,每一个硬件设备都是设备模型中的一个device,不过,在clock event
device这个场景下,实际上系统并不是为每一个timer硬件定义一个device,而仅仅是为每一个当前active(作为tick
device那个clock event
device)的clockevent设备定义了一个device数据结构,有空的时候大家可以思考一下这个问题。

(2)调用device_register就可以把所有的clock event device注册到系统中,统一设备模型会帮我们做一切事情。

(3)device_register之后,每个clock event device体现为一个sysfs中的目录,我们还需要为这个目录增加一些属性文件。定义如下:

static DEVICE_ATTR(current_device, 0444, sysfs_show_current_tick_dev, NULL);

static DEVICE_ATTR(unbind_device, 0200, NULL, sysfs_unbind_tick_dev);

(4)除了per cpu的device,还有一个broadcast device,定义如下:

static struct device tick_bc_dev = {
    .init_name    = "broadcast",
    .id        = 0,
    .bus        = &clockevents_subsys,
};

这个device在tick_broadcast_init_sysfs函数中调用device_register注册到系统,并创建了dev_attr_current_device的属性。和per
cpu device不同的是:没有提供用户空间的unbind操作,也就是说,userspace无法unbind当前的broadcast
clock event device。

2、属性文件操作:显示current tick device

读clock event device的current_device属性文件

static ssize_t sysfs_show_current_tick_dev(struct device *dev, struct device_attribute *attr, char *buf)
{
    struct tick_device *td;
    ssize_t count = 0;

raw_spin_lock_irq(&clockevents_lock);
    td = tick_get_tick_dev(dev); ----获取tick device
    if (td && td->evtdev)
        count = snprintf(buf, PAGE_SIZE, "%s\n", td->evtdev->name);
    raw_spin_unlock_irq(&clockevents_lock);
    return count;
}

一个active的clock event device对应的tick device有两种情况,一种是per cpu的tick
device,驱动系统运作。另外一种是broadcast tick device,这些内容留在在tick device部分描述吧。

3、属性文件操作:unbind tick device

一个tick device总是绑定一个属于该cpu core并且精度最高的那个clock event
device。通过sysfs的接口可以解除这个绑定。如果解除绑定的那个clock event
device是unused,可以直接从clockevent_devices全局链表中删除。如果该设备当前使用中,那么需要找到一个替代的clock
event device。如果找不到一个替代的clock event
device,那么不能unbind当前的device,返回EBUSY。具体的代码逻辑就不分析,大家可以自己阅读代码理解。

Linux时间子系统(十六) clockevent的更多相关文章

  1. Linux时间子系统(十五) clocksource

    一.前言 和洋葱一样,软件也是有层次的,内核往往需要对形形色色的某类型的驱动进行抽象,屏蔽掉其具体的特质,获取该类驱动共同的逻辑,而又根据这些逻辑撰写该类驱动的抽象层.嵌入式系统总是会提供timer的 ...

  2. Linux时间子系统(十四) tick broadcast framework

    一.前言 在内核中,有cpuidle framework可以控制cpu的节电:当没有进程调度到该cpu上执行的时候,swapper进程粉墨登场,将该cpu会被推入到idle状态.当然CPU的idle状 ...

  3. Linux时间子系统(十二) periodic tick

    一.tick device概念介绍 1.数据结构 在内核中,使用struct tick_device来抽象系统中的tick设备,如下: struct tick_device {     struct ...

  4. Linux时间子系统之(十六):clockevent

    专题文档汇总目录 Notes:介绍struct clocke_event_device及其功能feature.模式:触发event接口clockevents_program_event:clockev ...

  5. Linux时间子系统之(十四):tick broadcast framework

    专题文档汇总目录 Notes:BroadcastTick作为cpuidle的waker,硬件基础.BroadcastTick嵌入在当前系统Tick框架中.BroadcastTick设备初始化:周期性T ...

  6. Linux时间子系统之(十二):periodic tick

    专题文档汇总目录 Notes:TickDevice模式,以及clocckevent设备.TickDevice设备的初始化,TickDevice是如何加入到系统中的.周期性Tick的产生. 原文地址:L ...

  7. Linux时间子系统之(十五):clocksource

    专题文档汇总目录 Notes:clocksource基本概念,struct clocksource详解:注册和注销clocksource:内核如何选取clocksource:clocksource相关 ...

  8. Linux时间子系统之(六):POSIX timer

    专题文档汇总目录 Notes:首先讲解了POSIX timer的标识(唯一识别).POSIX Timer的组织(管理POSIX Timer).内核中如何抽象POSIX Timer:然后分析了POSIX ...

  9. Linux时间子系统专题汇总

    关于Linux时间子系统有两个系列文章讲的非常好,分别是WowoTech和DroidPhone. 还有两本书分别是介绍: Linux用户空间时间子系统<Linux/UNIX系统编程手册>的 ...

随机推荐

  1. JSP简单练习-定时刷新页面

    <%@ page contentType="text/html; charset=gb2312" %> <%@ page import="java.ut ...

  2. Cognos开发自定义排序规则的报表和自定义排名报表

    场景:有一个简单的销售数据分析,可以按照日期,按照商品类型来分析订单笔数和订单金额. 目的:用户可以自定义查看按照不同指标排序的数据,用户可以查看按照不同指标排名的前N名数据 一:功能及效果展示 效果 ...

  3. UITabBarController — 标签视图控制器

    UITabBarController - 标签视图控制器 UITabBarController 分为三层结构: (1).tab bar (2.)Custom Content (3.). Tab bar ...

  4. POJ2762 Going from u to v or from v to u? 强连通+缩点

    题目链接: poj2762 题意: 给出一幅单向图.问这张图是否满足   随意两点ab 都能 从a到达b 或  从b到达a 题解思路: 推断一幅图是否满足弱连通 首先想到的是将图中的 强连通分量(能互 ...

  5. C++中用完需要释放掉内存的几个类

      BSTR BSTR bstrXML = NULL; //用完以后,或者 catch段中 if(bstrXML) ::SysFreeString(result); VARIANT VARIANT v ...

  6. easyui combotree不让父级选中

    easyui combotree不让父级选中? <ul id="combotree"></ul> $(function () { $("#comb ...

  7. 【iOS】网络操作与AFNetworking

    众所周知.苹果搞的一套框架NSContention发送请求与接收请求的方式十分繁琐.操作起来非常不方便. 不仅要做区分各种请求设置各种不同的參数,并且还要常常在多线程里操作,同一时候还要对请求与返回的 ...

  8. JavaScript三(语法、关键保留字及变量)

    基本概念 一.区分大小写 在ECMAScript中的一切(变量.函数名.操作符)都是区分大小写的. 如变量名test和Test分别表示两个不同的变量, 二.标识符 所谓标识符,就是指变量.函数.属性的 ...

  9. 算法笔记_172:历届试题 波动数列(Java)

    目录 1 问题描述 2 解决方案   1 问题描述 问题描述 观察这个数列: 1 3 0 2 -1 1 -2 ... 这个数列中后一项总是比前一项增加2或者减少3. 栋栋对这种数列很好奇,他想知道长度 ...

  10. caffe 代码阅读笔记1

    首先查看caffe.cpp里的train函数: // Train / Finetune a model. //训练,微调一个网络模型 int train() { // google的glog库,检查- ...