Linux时间子系统之(五):POSIX Clock_monotonic clockdat-程序员宅基地

技术标签: kernel  linux  时间管理  内核  Linux内核转载  posix  

一、前言

clock是timer的基础,任何一个timer都需要运作在一个指定的clock上来。内核中维护了若干的clock,本文第二章描述了clock的基本概念和一些静态定义的posix clock。根据计时的特点,clock分成两种:一种是真实世界的时间概念,另外一个是仅仅计算CPU执行时间 ,这两种clock分别在第三和第四章描述。从clock的生命周期来看,可以分成静态和动态的posix clock,静态是一直存在于内核中的,而动态clock有创建和销毁的概念,本文第五章描述了dynamic posix clock。

 

二、基本概念

1、核心数据结构

所谓clock,实际上就是一种计时工具,可能是硬件,也可能是软件,当然对于POSIX clock而言,当然是指软件抽象了。clock能够记录一段时间的流逝,这段时间可能是真实的墙上时间,也可能是虚拟的时间,例如基于某个进程或者线程的CPU执行时间。在linux kernel中,用struct k_clock来抽象,具体定义如下:

struct k_clock { 
    int (*clock_getres) (const clockid_t which_clock, struct timespec *tp); 
    int (*clock_set) (const clockid_t which_clock, const struct timespec *tp); 
    int (*clock_get) (const clockid_t which_clock, struct timespec * tp); 
    int (*clock_adj) (const clockid_t which_clock, struct timex *tx); 
    int (*timer_create) (struct k_itimer *timer); 
    int (*nsleep) (const clockid_t which_clock, int flags, struct timespec *, struct timespec __user *); 
    long (*nsleep_restart) (struct restart_block *restart_block); 
    int (*timer_set) (struct k_itimer * timr, int flags, struct itimerspec * new_setting, 
                            struct itimerspec * old_setting); 
    int (*timer_del) (struct k_itimer * timr); 
    void (*timer_get) (struct k_itimer * timr, struct itimerspec * cur_setting); 
};

clock作为一个计时工具当然有计时精度,通过clock_getres函数可以获取该clock的时间精度,需要说明的是这个精度是和timer相关的,用于将用户设定的timer超时时间规整到clock精度允许的数值上。clock_get和clock_set函数可以分别获取和设定当前的时间,这个时间值是一个绝对时间值(对于时间轴而言,这个绝对时间也是相对的,是相对于该timeline的epoch而言),标记了当前时间点。clock计时有可能是不准确的,例如基于系统晶振的clock。一方面本身晶振的精度有限,时间累积长了会出现较大误差。另外,晶振也会随着使用时间的推移、温度的变化等等因素而导致误差。clock_adj函数允许系统根据外部的精确时间信息对本clock进行调整。nsleep和nsleep_restart这两个成员函数可以让进程sleep一段时间。timer_xxx系列函数是和POSIX interval timer相关,具体会在POSIX timer文档中描述。

2、静态定义的clock

static struct k_clock posix_clocks[MAX_CLOCKS];

posix_clocks数组定义了系统支持的所有的clock,相关的定义如下:

#define CLOCK_REALTIME            0 
#define CLOCK_MONOTONIC            1 
#define CLOCK_PROCESS_CPUTIME_ID    2 
#define CLOCK_THREAD_CPUTIME_ID        3 
#define CLOCK_MONOTONIC_RAW        4 
#define CLOCK_REALTIME_COARSE        5 
#define CLOCK_MONOTONIC_COARSE        6 
#define CLOCK_BOOTTIME            7 
#define CLOCK_REALTIME_ALARM        8 
#define CLOCK_BOOTTIME_ALARM        9 
#define CLOCK_SGI_CYCLE            10    /* Hardware specific */ 
#define CLOCK_TAI            11

#define MAX_CLOCKS            16

POSIX标准定义了4种类型的clock,CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID,其他是linux specific。如果一个clock的timeline是基于CPU运行时间的,那么我们称之CPU-time clock。CPU-time clock主要是用来为某个进程或者线程的执行时间进行计时的,一旦线程(进程)被切换,那么该clock就停掉了,直到下次调度器切换回该线程(进程)执行。

各个具体的操作系统实现可以定义自己特有的clock,对于Linux kernel,我们定义了若干种clock。CLOCK_MONOTONIC_RAW启动时间点被设成0,此后一直不断累加,而且能设定,不会随NTP调整。CLOCK_REALTIME_COARSE、CLOCK_MONOTONIC_COARSE的概念和CLOCK_REALTIME、CLOCK_MONOTONIC的概念是类似的,只不过是精度是比较粗的版本。有时候,timer没有必要要求那么高的精度,那么我们可以使用这种clock,从而可以获取更好的性能。CLOCK_BOOTTIME和CLOCK_MONOTONIC类似,也是单调上述,在系统初始化的时候设定的基准数值是0,不过CLOCK_BOOTTIME计算系统suspend的时间,也就是说,不论是running还是suspend(这些都算是启动时间),CLOCK_BOOTTIME都会累积计时,直到系统reset或者shutdown。

CLOCK_REALTIME_ALARM和CLOCK_BOOTTIME_ALARM主要用于Alarmtimer,这种timer是基于RTC的,更详细的内容请参考本站Alarmtimer的文档。CLOCK_TAI是原子钟的时间,和基于UTC的CLOCK_REALTIME类似,不过没有leap second。

用户空间的clock_xxx函数会传递clock id的参数,在内核态,根据id作为index在posix_clocks数组中可以索引到对应的clock,然后调用clock对应的callback函数就OK了。当然基本意思就是这样,具体实现如下:

static struct k_clock *clockid_to_kclock(const clockid_t id) 

    if (id < 0) 
        return (id & CLOCKFD_MASK) == CLOCKFD ? 
            &clock_posix_dynamic : &clock_posix_cpu;

    if (id >= MAX_CLOCKS || !posix_clocks[id].clock_getres) 
        return NULL; 
    return &posix_clocks[id]; 
}

clockid_to_kclock这个函数用来将clock id和具体的posix clock的k_clock 数据结构对应起来。在linux平台上,clockid是int类型的数据,共32个bit,高29个bit用来保存一个pid(用于CPU-time clock)或者fd(动态分配的clock),bit 2用来说明该CPU-time clock是一个进程clock还是线程clock。bit 1和bit 0用来说明该clock id的类型:PROF=0, VIRT=1, SCHED=2, or FD=3。

当clock id小于0的时候,要么是CPU-time clock,要么是动态分配的clock,可以根据clock id的类型来判断。CPU-time clock和动态分配的clock后面会具体介绍。

 

三、各种real timeclock的定义

系统初始化的时候会调用init_posix_timers函数对各种静态定义的real time clock进行注册。注:monotonic clock也是real time clock的一种,全称是monotonic real time clock。

1、real time clock的定义如下(timer相关内容不在本文描述):

struct k_clock clock_realtime = { 
    .clock_getres    = hrtimer_get_res, 
    .clock_get    = posix_clock_realtime_get, 
    .clock_set    = posix_clock_realtime_set, 
    .clock_adj    = posix_clock_realtime_adj, 
    .nsleep        = common_nsleep, 
    .nsleep_restart    = hrtimer_nanosleep_restart, 
};

real time clock需要调用timekeeping模块的接口来获取和设定当前时间值。对于获取当前时间值的函数posix_clock_realtime_get而言,是调用ktime_get_real_ts函数,该函数是timekeeping模块的接口函数,以timespec的格式回了real time clock的当前值。posix_clock_realtime_set函数主要是调用do_settimeofday这个timekeeping模块的接口函数。posix_clock_realtime_adj是调用do_adjtimex接口函数来实现具体的功能。

纳秒级别的sleep是通过高精度timer实现的,real time clock的精度和hrtimer相关,具体可以参考hrtimer相关文档。

2、monotonic clock的定义如下:

struct k_clock clock_monotonic = { 
    .clock_getres    = hrtimer_get_res, 
    .clock_get    = posix_ktime_get_ts, 
    .nsleep        = common_nsleep, 
    .nsleep_restart    = hrtimer_nanosleep_restart, 
};

monotonic clock没有clock_set函数,不能被设定。通过ktime_get_ts这个timekeeping模块的接口可以获得monotonic clock的当前值。纳秒级别的sleep以及精度的获取函数和real time clock一样。

3、monotonic raw clock的定义如下:

struct k_clock clock_monotonic_raw = { 
    .clock_getres    = hrtimer_get_res, 
    .clock_get    = posix_get_monotonic_raw, 
};

posix_get_monotonic_raw函数是调用timekeeping模块getrawmonotonic接口函数实现获取monotonic raw clock当前时间数值的。和monotonic clock一样,不能设定。和monotonic clock不同的是该clock没有timer相关的callback函数。

4、coarse clock

struct k_clock clock_realtime_coarse = { 
    .clock_getres    = posix_get_coarse_res, 
    .clock_get    = posix_get_realtime_coarse, 
}; 
struct k_clock clock_monotonic_coarse = { 
    .clock_getres    = posix_get_coarse_res, 
    .clock_get    = posix_get_monotonic_coarse, 
};

这两个clock的精度都是和tick相关的,KTIME_LOW_RES定义就是tick的纳秒数值。clock_get函数分别调用current_kernel_time和get_monotonic_coarse获取当前时间点的值。

CLOCK_BOOTTIME和CLOCK_TAI的clock实现非常简单,大家自行阅读代码就OK了。

 

四、CPU-time clock

1、概述

从用户空间的角度看,有两种CPU-time clock的应用场景:

(1)调用clock_xxx函数并传递CLOCK_PROCESS_CPUTIME_ID或者CLOCK_THREAD_CPUTIME_ID给该函数

(2)调用clock_getcpuclockid或者pthread_getcpuclockid函数来获取指定进程或者线程的clock id,之后调用clock_xxx函数并传递该clock id参数

应对第一种场景,系统初始化的时候会调用init_posix_cpu_timers函数对静态定义的CPU-time clock进行注册。对于第二种场景,内核静态定义了一个clock_posix_cpu的clock来应对这种需求。

2、指定进程或者线程的CPU-time clock

内核静态定义了一个clock如下(去掉了timer的callback函数):

struct k_clock clock_posix_cpu = { 
    .clock_getres    = posix_cpu_clock_getres, 
    .clock_set    = posix_cpu_clock_set, 
    .clock_get    = posix_cpu_clock_get, 
    .nsleep        = posix_cpu_nsleep, 
    .nsleep_restart    = posix_cpu_nsleep_restart, 
};

(1)获取精度信息

static int posix_cpu_clock_getres(const clockid_t which_clock, struct timespec *tp) 

    int error = check_clock(which_clock);--------参数校验 
    if (!error) { 
        tp->tv_sec = 0; 
        tp->tv_nsec = ((NSEC_PER_SEC + HZ - 1) / HZ); 
        if (CPUCLOCK_WHICH(which_clock) == CPUCLOCK_SCHED) { 
            tp->tv_nsec = 1; 
        } 
    } 
    return error; 
}

该函数的执行逻辑分成两个部分,一部分是参数校验,一部分是返回精度。参数校验需要检查的包括:

(a)clock id中的高29个bit包含了pid,获取pid的代码如下:

#define CPUCLOCK_PID(clock)        ((pid_t) ~((clock) >> 3))

从代码可知,实际上并不是将pid放到高29个bit,而是将反码保存到了高29个bit。为何保存反码?这样做为了确保clock id是一个负数(MSB是1),还记得clockid_to_kclock的实现吗?要获取该clock id的精度,要确保该pid的task存在

(b)如果该clock id是一个进程相关的(调用clock_getcpuclockid获得),那么这个进程id应该是一个实实在在的进程id。在linux kernel中,pid实际上是线程ID,POSIX标准的进程ID,也就是PID在内核中被成为线程组ID。因此,所谓一个“实实在在的进程id”就是说该线程的id(pid)和tgid一样,该pid标识的线程是线程组leader。当然,就是获取精度而已,实际上要求并不要那么严格,也许该pid标识的线程leader会退出,因此实际上要求该pid标识的task有thread group leader就OK了。(这里有可能理解有误,TODO)

(c)如果该clock id是一个线程相关的(调用pthread_getcpuclockid获得),那么调用者必须和该线程(clock id中指明的那个线程)属于一个进程(线程组)。

返回精度部分的代码逻辑很简单,对于PROF和VIRT类型的CPU-time clock,其精度是tick,对于SCHED类型,精度是1ns。

(2)获取当前时间值

同样的,首先需要从clock id中获取pid的值,然后根据pid的值获取对应的task sturct,如果pid等于0,那么不需要费劲去寻找。得到task struct之后,可以调用posix_cpu_clock_get_task函数获取时间值:

static int posix_cpu_clock_get_task(struct task_struct *tsk,   const clockid_t which_clock, 
                    struct timespec *tp) 

    int err = -EINVAL; 
    unsigned long long rtn;

    if (CPUCLOCK_PERTHREAD(which_clock)) {---per 线程的cpu clock 
        if (same_thread_group(tsk, current))---必须和调用者是同一个线程组,也就是同一个进程 
            err = cpu_clock_sample(which_clock, tsk, &rtn); 
    } else {

        if (tsk == current || thread_group_leader(tsk))---进程的cpu clock 
            err = cpu_clock_sample_group(which_clock, tsk, &rtn);  
    }

    if (!err) 
        sample_to_timespec(which_clock, rtn, tp); ---给返回值赋值

    return err; 
}

这里仍然存在校验问题,也就是说是否允许调用者获取该task的CPU-time clock。对于进程,只允许调用者进程获取自己的CPU-time clock,在多线程环境下,主线程(线程组leader)可以获取整个进程的CPU-time clock信息。对于per线程的操作,必须和调用者是同一个线程组,也就是同一个进程。

(a)获取线程的clock信息

static int cpu_clock_sample(const clockid_t which_clock, struct task_struct *p, 
                unsigned long long *sample) 

    switch (CPUCLOCK_WHICH(which_clock)) { 
    default: 
        return -EINVAL; 
    case CPUCLOCK_PROF: 
        *sample = prof_ticks(p);----获取该task在用户空间加上在kernel space的执行时间 
        break; 
    case CPUCLOCK_VIRT: 
        *sample = virt_ticks(p);----获取该task在用户空间的执行时间 
        break; 
    case CPUCLOCK_SCHED: 
        *sample = task_sched_runtime(p);----和调度器相关的cpu clock 
        break; 
    } 
    return 0; 
}

计算进程或者线程在cpu上的执行时间是一个挺烦人的事,一方面想要精度高,另外一方面又不想计算量大。因此,实际上CPU-time clock有三种,CPUCLOCK_PROF和CPUCLOCK_VIRT这两种都是比较粗略估计CPU执行时间的clock,它的工作原理就是在周期性tick中进行进程cpu time的统计,如果该tick是用户态(timer中断了用户态程序的执行),那么整个tick的时间都是该进程的用户态执行时间。如果该tick是内核态,并且是用户程序进行系统调用而陷入内核,那么整个tick的时间都是该进程的系统态执行时间。

CPUCLOCK_SCHED clock和上面的方法不一样,它的精度是纳秒级别的,是在调度器上进行计算进程时间。具体的计算方法还是留到调度器文章中再描述吧。

(b)cpu_clock_sample_group函数概念类似,不过是统计一个进程上所有线程的时间而已。

3、CLOCK_PROCESS_CPUTIME_ID 类型的clock

struct k_clock process = { 
    .clock_getres    = process_cpu_clock_getres, 
    .clock_get    = process_cpu_clock_get, 
    .nsleep        = process_cpu_nsleep, 
    .nsleep_restart    = process_cpu_nsleep_restart, 
};

process_cpu_clock_getres用来获取时间精度,该函数实际是调用posix_cpu_clock_getres(PROCESS_CLOCK, tp)来完成的。process_cpu_clock_get用来获取当前时间值,实际上是通过调用posix_cpu_clock_get完成。posix_cpu_clock_xxx函数在上一节中已经描述。

4、CLOCK_THREAD_CPUTIME_ID类型的clock

很简单,大家自行学习吧。

 

五、动态分配clock

1、源由

某些硬件提供了计时的能力,可以实现成一个posix clock,同时,这些硬件又类似USB设备那样可以热拔插,这也就意味着该posix clock不能静态定义。此外,除了标准的timer和clock相关的操作,这些提供计时能力的硬件还需要一些其他的类似字符设备界面的控制接口,在这样的需求推动下,内核提供了dynamic posix clock。

2、dynamic posix clock

系统中的每一个dynamic posix clock用struct posix_clock来抽象,如下:

struct posix_clock { 
    struct posix_clock_operations ops;--------------(1) 
    struct cdev cdev;----------------------(2) 
    struct kref kref; 
    struct rw_semaphore rwsem; 
    bool zombie;------------------------(3) 
    void (*release)(struct posix_clock *clk);-------------(4) 
};

(1)ops是该dynamic posix clock的操作函数集,分成两个group,一个是timer(例如:timer_create、timer_delete等)以及clock操作相关(例如clock_gettime、clock_settime等),另外一个是普通字符设备的操作函数(例如:open、read、write等)。

(2)该dynamic posix clock对应的cdev数据结构。在struct posix_clock_operations中有一个owner,其实在cdev中也有一个指向moudle的owner成员,看起来似乎是重复定义了。同样的疑问也存在与kref成员,因为在cdev中有kobject成员,kobject抽象了内核最基础的对象类别,包括名字、引用计数等,因此,我觉得只要struct posix_clock包括了cdev成员,struct posix_clock_operations中的owner以及struct posix_clock中的kref应该没有存在的必要了。

(3)zombie记录了底层硬件的状态,对于hotplug的外设,有可能硬件被拔除。rwsem用来保护该状态信息

(4)当reference count等于0的时候会调用release函数释放dynamic posix clock占用的资源。

3、注册和注销

底层的有计时能力的硬件driver可以调用posix_clock_register和posix_clock_unregister来注册或者注销一个posix clock,注册代码如下:

int posix_clock_register(struct posix_clock *clk, dev_t devid) 

    int err;

    kref_init(&clk->kref); 
    init_rwsem(&clk->rwsem);

    cdev_init(&clk->cdev, &posix_clock_file_operations);-----VFS接口的操作函数集合 
    clk->cdev.owner = clk->ops.owner; 
    err = cdev_add(&clk->cdev, devid, 1);

    return err; 
}

VFS接口的操作函数集合都非常简单,基本上都是struct posix_clock_operations上的字符设备操作函数集合上。这样,用户空间的程序可以通过标准的文件描述符进行设备操作。

4、clock和timer接口

通过clock_xxx或者timer_xxx函数可以指定clock id,对于dynamic posix clock可以通过下面的操作来生成一个dynamic posix clock ID:

#define FD_TO_CLOCKID(fd)    ((~(clockid_t) (fd) << 3) | CLOCKFD)

其中fd是通过设备节点打开的那个有计时能力的硬件。在内核态会通过clockid_to_kclock操作将clock id转换成

static struct k_clock *clockid_to_kclock(const clockid_t id) 

    if (id < 0) 
        return (id & CLOCKFD_MASK) == CLOCKFD ? 
            &clock_posix_dynamic : &clock_posix_cpu;

…… 
}

clock_posix_dynamic可以将dynamic posix clock ID转换成对应的posix_clock,然后调用struct posix_clock_operations上的time和clock相关的函数即可。


原创文章,转发请注明出处。蜗窝科技

http://www.wowotech.net/timer_subsystem/posix-clock.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/G_linuxer_/article/details/51012010

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法