Linux时间子系统7:sleep timer接口定时实现

1、前言

        之前的文章中介绍了Linux时间相关的内容,包括用户态/内核态的时间获取,时间的种类,时钟源等,本篇开始的后续几篇文章将介绍Linux系统关于定时相关的服务,这与之前的内容是高度相关的,本篇还是从应用的角度出发,先看Linux用户态和内核态的操作访问接口。

        本篇文章主要是对下面两篇文章的整理,同时增加了部分个人的理解:
Linux时间子系统之(三):用户空间接口函数

Linux内核时钟系统和定时器实现 - 知乎

2、用户态定时器相关API接口

2.1 sleep系列

我们经常遇到过具有睡眠功能的库函数API接口:

sleep()
usleep()
nanosleep()
clock_nanosleep

sleep:

调用该函数会导致当前进程sleep,seconds之后(基于CLOCK_REALTIME)会返回继续执行程序。该函数的返回值说明了进程没有进入睡眠的时间。例如如果我们想要睡眠8秒,但是由于siganl中断了睡眠,只是sleep了5秒,那么返回值就是3,表示有3秒还没有睡。

sleep在musl1.2.3中的实现如下,可以看到sleep调用了nanosleep

unsigned sleep(unsigned seconds)
{struct timespec tv = { .tv_sec = seconds, .tv_nsec = 0 };if (nanosleep(&tv, &tv))return tv.tv_sec;return 0;
}

usleep:

usleep支持精度更高的微妙级别的定时操作,同样usleep也调用了nanosleep

int usleep(unsigned useconds)
{struct timespec tv = {.tv_sec = useconds/1000000,.tv_nsec = (useconds%1000000)*1000};return nanosleep(&tv, &tv);
}

nanosleep:

#include <time.h>int nanosleep(const struct timespec *req, struct timespec *rem);

usleep函数已经是过去式,不建议使用,取而代之的是nanosleep函数。req中设定你要sleep的秒以及纳秒值,然后调用该函数让当前进程sleep。返回0表示执行成功,返回-1说明执行失败,错误码在errno中获取。EINTR表示该函数被signal打断。rem参数是remaining time的意思,也就是说还有多少时间没有睡完。

linux kernel并没有提供sleep和usleep对应的系统调用,sleep和usleep的实现位于c lib。在有些系统中,这些实现是依赖信号的,也有的系统使用timer来实现的,对于GNU系统,sleep和usleep和nanosleep函数一样,都是通过kernel的sys_nanosleep的系统调用实现的(底层是基于hrtimer)。

nanosleep在musl中的实现,调用了clock_nanosleep系统调用

int nanosleep(const struct timespec *req, struct timespec *rem)
{return __syscall_ret(-__clock_nanosleep(CLOCK_REALTIME, 0, req, rem));
}

clock_nanosleep:
 

int clock_nanosleep(clockid_t clock_id, int flags,const struct timespec *request,struct timespec *remain);

clock_nanosleep接口函数需要传递更多的参数,当然也就是意味着它功能更强大。clock_id说明该接口函数不仅能基于real time clock睡眠,还可以基于其他的系统时钟睡眠,可以翻看以下之前对于系统时间种类的介绍。flag等于0或者1,分别指明request参数设定的时间值是相对时间还是绝对时间。

OK,到这里我们可以看到,对于musl而言,sleep的相关函数实际上都是基于clock_nanosleep系统调用,我们看一下clock_nanosleep的内核实现,它是这样的:

SYSCALL_DEFINE4(clock_nanosleep, const clockid_t, which_clock, int, flags,const struct __kernel_timespec __user *, rqtp,struct __kernel_timespec __user *, rmtp)
{const struct k_clock *kc = clockid_to_kclock(which_clock);struct timespec64 t;if (!kc)return -EINVAL;if (!kc->nsleep)return -EOPNOTSUPP;if (get_timespec64(&t, rqtp))return -EFAULT;if (!timespec64_valid(&t))return -EINVAL;if (flags & TIMER_ABSTIME)rmtp = NULL;current->restart_block.fn = do_no_restart_syscall;current->restart_block.nanosleep.type = rmtp ? TT_NATIVE : TT_NONE;current->restart_block.nanosleep.rmtp = rmtp;return kc->nsleep(which_clock, flags, &t);
}

可以看到,在内核,clock_nanosleep最终调用了posix clocks注册的nsleep函数,我们以clock_realtime为例,可以看到注册的nsleep函数是common_nsleep,而common_nsleep最终调用到了hrtimer_nanosleep。好的,我们先记住这个hrtimer。

static int common_nsleep(const clockid_t which_clock, int flags,const struct timespec64 *rqtp)
{ktime_t texp = timespec64_to_ktime(*rqtp);return hrtimer_nanosleep(texp, flags & TIMER_ABSTIME ?HRTIMER_MODE_ABS : HRTIMER_MODE_REL,which_clock);
}

2.2 timer系列

alarm:

alarm()函数可以设置一个定时器,在特定时间超时,并产生SIGALRM信号,如果不忽略或不捕捉该信号,该进程会被终止。

alarm在musl1.2.3中的实现如下:

unsigned alarm(unsigned seconds)
{struct itimerval it = { .it_value.tv_sec = seconds }, old = { 0 };setitimer(ITIMER_REAL, &it, &old);return old.it_value.tv_sec + !!old.it_value.tv_usec;
}

可以看到alarm是基于间隔定时器itimer来实现的,调用了setitimer系统调用,至于setitimer后面会讲到,是基于hrtimer实现的。

Interval timer:

#include <sys/time.h>int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);struct itimerval {struct timeval it_interval; /* next value */struct timeval it_value;    /* current value */
};

Interval timer函数的行为和alarm函数类似,不过功能更强大。每个进程支持3种timer,不同的timer定义了如何计时以及发送什么样的信号给进程,which参数指明使用哪个timer:

(1)ITIMER_REAL。基于CLOCK_REALTIME计时,超时后发送SIGALRM信号,和alarm函数一样。

(2)ITIMER_VIRTUAL。只有当该进程的用户空间代码执行的时候才计时,超时后发送SIGVTALRM信号。

(3)ITIMER_PROF。只有该进程执行的时候才计时,不论是执行用户空间代码还是陷入内核执行(例如系统调用),超时后发送SIGPROF信号。

两个成员分别指明了本次和下次(超期后如何设定)的时间值。通过这样的定义,interval timer可以实现one shot类型的timer和periodic的timer。例如current value设定为5秒,next value设定为3秒,设定这样的timer后,it_value值会不断递减,直到5秒后触发,而随后it_value的值会被重新加载(使用it_interval的值),也就是等于3秒,之后会按照3为周期不断的触发。

old_value返回上次setitimer函数的设定值。getitimer函数获取当前的Interval timer的状态,其中的it_value成员可以得到当前时刻到下一次触发点的世时间信息,it_interval成员保持不变,除非你重新调用setitimer重新设定。

虽然interval timer函数也是POSIX标准的一部分,不过在新的POSIX标准中,interval timer接口函数被标注为obsolescent,取而代之的是POSIX timer接口函数。

在Linux 2.6.16 之前,itimer的实现是基于内核定时器timer wheel封装成的定时器接口。内核封装的定时器接口是提供给其他内核模块使用,也是其他定时器的基础。itimer通过内核定时器的封装,生成提供给用户层使用的接口setitimer和getitimer。内核定时器timer wheel提供的内核态调用接口可参考

add_timer() 
del_timer() 
init_timer()

当然现在的setitimer和getitimer是基于hrtimer的,我们以musl为例看一下:

int setitimer(int which, const struct itimerval *restrict new, struct itimerval *restrict old)
{if (sizeof(time_t) > \sizeof(long)) {time_t is = new->it_interval.tv_sec, vs = new->it_value.tv_sec;long ius = new->it_interval.tv_usec, vus = new->it_value.tv_usec;if (!IS32BIT(is) || !IS32BIT(vs))return __syscall_ret(-ENOTSUP);long old32[4];int r = __syscall(SYS_setitimer, which,((long[]){is, ius, vs, vus}), old32);if (!r && old) {old->it_interval.tv_sec = old32[0];old->it_interval.tv_usec = old32[1];old->it_value.tv_sec = old32[2];old->it_value.tv_usec = old32[3];}return __syscall_ret(r);}return syscall(SYS_setitimer, which, new, old);
}

内核对应的实现:

SYSCALL_DEFINE3(setitimer, int, which, struct __kernel_old_itimerval __user *, value,struct __kernel_old_itimerval __user *, ovalue)
{struct itimerspec64 set_buffer, get_buffer;int error;if (value) {error = get_itimerval(&set_buffer, value);if (error)return error;} else {memset(&set_buffer, 0, sizeof(set_buffer));printk_once(KERN_WARNING "%s calls setitimer() with new_value NULL pointer."" Misfeature support will be removed\n",current->comm);}error = do_setitimer(which, &set_buffer, ovalue ? &get_buffer : NULL);if (error || !ovalue)return error;if (put_itimerval(ovalue, &get_buffer))return -EFAULT;return 0;
}

跟踪do_setitimer可以看到又是调用了hrtimer

alrm和itimer

函数alarm本质上设置的是低精确、非重载的ITIMER_REAL类定时器,它只能精确到秒,并且每次设置只能产生一次定时。函数setitimer 设置的定时器则不同,它们不但可以计时到微妙(理论上),还能自动循环定时。在一个Unix进程中,不能同时使用alarm和ITIMER_REAL类定时器。

2.3 posix timer

POSIX定时器的是为了解决间隔定时器itimer的以下问题:

  • 一个进程同一时刻只能有一个同一种类型(ITIMER_REAL, ITIMER_PROF, ITIMER_VIRT)的itimer。POSIX定时器在一个进程中可以创建任意多个timer。
  • itimer定时器到期后,只能通过信号(SIGALRM,SIGVTALRM,SIGPROF)的方式通知进程,POSIX定时器到期后不仅可以通过信号进行通知,还可以使用自定义信号,还可以通过启动一个线程来进行通知。
  • itimer支持us级别,POSIX定时器支持ns级别。

POSIX定时器提供的定时器API如下:

int timer_create(clockid_t clock_id, struct sigevent *evp, timer_t *timerid);
int timer_settime(timer_t timerid, int flags, const struct itimerspec *value, struct itimerspect *ovalue);
int timer_gettime(timer_t timerid,struct itimerspec *value);
int timer_getoverrun(timer_t timerid);
int timer_delete (timer_t timerid);

(1)创建timer

从timer_create我们就可以看到,这和posix 时间接口就很像了,首先它需要用户指定clock id,这就意味着它可以指定clock创建定时器。

struct sigevent设置了定时器到期时的通知方式和处理方式等,结构的定义如下:

struct sigevent
{int sigev_notify;   //设置定时器到期后的行为int sigev_signo;    //设置产生信号的信号码union sigval   sigev_value; //设置产生信号的值void (*sigev_notify_function)(union sigval);//定时器到期,从该地址启动一个线程pthread_attr_t *sigev_notify_attributes;    //创建线程的属性
}union sigval
{int sival_int;  //integer valuevoid *sival_ptr; //pointer value
}

如果sigevent传入NULL,那么定时器到期会产生默认的信号,对CLOCK_REALTIMER来说,默认信号就是SIGALRM,如果要产生除默认信号之外的其他信号,程序必须将evp->sigev_signo设置为期望的信号码。

如果几个定时器产生了同一个信号,处理程序可以用 sigev_value来区分是哪个定时器产生了信号。要实现这种功能,程序必须在为信号安装处理程序时,使用struct sigaction的成员sa_flags中的标志符SA_SIGINFO。

sigev_notify定义了当timer超期后如何通知该进程,可以设定:

(a)SIGEV_NONE。不需要异步通知,程序自己调用timer_gettime来轮询timer的当前状态

(b)SIGEV_SIGNAL。使用sinal这样的异步通知方式。发送的信号由sigev_signo定义。如果发送的是realtime signal,该信号的附加数据由sigev_value定义。

(c)SIGEV_THREAD。创建一个线程执行timer超期callback函数,_attribute定义了该线程的属性。

(d)SIGEV_THREAD_ID。行为和SIGEV_SIGNAL类似,不过发送的信号被送达进程内的一个指定的thread,这个thread由_tid标识。

(2)设定timer

timerid就是上一节中通过timer_create创建的timer。new_value和old_value这两个参数类似setitimer函数,这里就不再细述了。flag等于0或者1,分别指明new_value参数设定的时间值是相对时间还是绝对时间。如果new_value.it_value是一个非0值,那么调用timer_settime可以启动该timer。如果new_value.it_value是一个0值,那么调用timer_settime可以stop该timer。

timer_gettime函数和getitimer类似,可以参考上面的描述。

(3)删除timer

有创建就有删除,timer_delete用来删除指定的timer,释放资源。

我们照例来看一下musl和对应内核的实现

int timer_create(clockid_t clk, struct sigevent *restrict evp, timer_t *restrict res)
{volatile static int init = 0;pthread_t td;pthread_attr_t attr;int r;struct start_args args;struct ksigevent ksev, *ksevp=0;int timerid;sigset_t set;switch (evp ? evp->sigev_notify : SIGEV_SIGNAL) {case SIGEV_NONE:case SIGEV_SIGNAL:case SIGEV_THREAD_ID:if (evp) {ksev.sigev_value = evp->sigev_value;ksev.sigev_signo = evp->sigev_signo;ksev.sigev_notify = evp->sigev_notify;if (evp->sigev_notify == SIGEV_THREAD_ID)ksev.sigev_tid = evp->sigev_notify_thread_id;elseksev.sigev_tid = 0;ksevp = &ksev;}if (syscall(SYS_timer_create, clk, ksevp, &timerid) < 0)return -1;*res = (void *)(intptr_t)timerid;break;case SIGEV_THREAD:if (!init) {struct sigaction sa = { .sa_handler = SIG_DFL };__libc_sigaction(SIGTIMER, &sa, 0);a_store(&init, 1);}if (evp->sigev_notify_attributes)attr = *evp->sigev_notify_attributes;elsepthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);pthread_barrier_init(&args.b, 0, 2);args.sev = evp;__block_app_sigs(&set);__syscall(SYS_rt_sigprocmask, SIG_BLOCK, SIGTIMER_SET, 0, _NSIG/8);r = pthread_create(&td, &attr, start, &args);__restore_sigs(&set);if (r) {errno = r;return -1;}ksev.sigev_value.sival_ptr = 0;ksev.sigev_signo = SIGTIMER;ksev.sigev_notify = SIGEV_THREAD_ID;ksev.sigev_tid = td->tid;if (syscall(SYS_timer_create, clk, &ksev, &timerid) < 0)timerid = -1;td->timer_id = timerid;pthread_barrier_wait(&args.b);if (timerid < 0) return -1;*res = (void *)(INTPTR_MIN | (uintptr_t)td>>1);break;default:errno = EINVAL;return -1;}return 0;
}

可以看到POSIX定时器通过调用内核的posix_timer系统调用进行实现,但c库对POSIX timer进行了一定的封装,例如如果POSIX timer到期通知方式被设置为 SIGEV_THREAD 时,glibc 需要自己完成一些辅助工作,因为内核无法在 Timer 到期时启动一个新的线程。

我们再看内核对应的实现:

SYSCALL_DEFINE3(timer_create, const clockid_t, which_clock,struct sigevent __user *, timer_event_spec,timer_t __user *, created_timer_id)
{if (timer_event_spec) {sigevent_t event;if (copy_from_user(&event, timer_event_spec, sizeof (event)))return -EFAULT;return do_timer_create(which_clock, &event, created_timer_id);}return do_timer_create(which_clock, NULL, created_timer_id);
}

跟踪do_timer_create可以看到,同样是调用了注册的posix clocks对应的timer create函数,我们仍然以clock_realtime为例,可以看到实际调用的是hrtimer_init。

3、内核态睡眠、定时器相关API接口

睡眠相关接口:

usleep_range(umin,umax);  //推荐使用
msleep(ms);
msleep_interruptible(ms);
ssleep(s);

低精度定时器相关接口:

1. 初始化:

#define timer_setup(timer, callback, flags)			\__init_timer((timer), (callback), (flags))#define DEFINE_TIMER(_name, _function)				\struct timer_list _name =				\__TIMER_INITIALIZER(_function, 0)

2. mod_timer

内核通过函数mod_timer来修改已经激活的定时器超时时间,也可以用来激活已经初始化但是没有激活的定时器。

int mod_timer(struct timer_list *timer, unsigned long expires)
{return __mod_timer(timer, expires, 0);
}
EXPORT_SYMBOL(mod_timer);

3. add_timer

void add_timer(struct timer_list *timer)
{BUG_ON(timer_pending(timer));__mod_timer(timer, timer->expires, MOD_TIMER_NOTPENDING);
}
EXPORT_SYMBOL(add_timer);

如果要指定运行定时器的cpu,可以使用

void add_timer_on(struct timer_list *timer, int cpu)

4. 删除定时器

int del_timer(struct timer_list *timer)
int del_timer_sync(struct timer_list *timer)

同add_timer函数相反,del_timer类的函数负责从系统的定时器管理队列中摘除一个定时器对象。

对于del_timer函数要摘除的定时器对象timer,函数会首先判断该对象是否是一个pending的定时器,一个处于pending状态的定时器是处在处理器的定时器管理队列中正等待被调度执行的定时器对象。如果一个要被del_timer函数删除的timer对象已经被调度执行(内核源码称这种定时器状态为inactive),函数将直接返回0,否则函数将通过detach_timer将该定时器对象从队列中删除。

在多处理器的SMP系统中,del_timer_sync函数要完成的任务除了同del_timer一样从定时器队列中删除一个定时器对象外,还会确保当函数返回时系统中没有任何处理器正在执行定时器对象上的定时器函数(这里没有提到,如果有,就会一直等待完成),而如果只是调用del_timer,那么当函数返回时,被删除的定时器对象的定时器函数可能正在其他处理器上运行。

  • 内核定时器会在超时后主动删除,del_timer主要用来删除那些没有超时的定时器。
  • 对于多处理器,使用del_timer_sync要比del_timer安全,del_timer_sync会等待定时器执行完成。
  • del_timer_sync不能在中断上下文使用(会引起死锁),只能在进程上下文使用

定时器的回调函数是在中断上下文中执行,所以使用中断上下文的注意事项依然适用于定时器回调函数。

  • 定时器回调函数需要保护共享数据。
  • 定时器回调函数中不能触发进程调度。
  • 定时器回调函数中不能睡眠。
  • 定时器回调函数中不能使用信号量。

高精度定时器相关接口:
初始化:

void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,enum hrtimer_mode mode)EXPORT_SYMBOL_GPL(hrtimer_init);

启动:

static inline void hrtimer_start(struct hrtimer *timer, ktime_t tim,const enum hrtimer_mode mode)

取消/删除

int hrtimer_cancel(struct hrtimer *timer)

以上内核定时器详细的内容后面再补充。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/855650.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SolidWorks科研版更快地开发产品创意

在当今竞争激烈的市场环境中&#xff0c;产品创新的速度和质量直接决定了企业的生死存亡。对于科研人员和设计师来说&#xff0c;如何能够快速、准确地实现产品创意的转化&#xff0c;是摆在面前的一大挑战。SolidWorks科研版作为一款功能强大的三维设计软件&#xff0c;为科研…

正则表达式之三剑客grep

正则表达式匹配的是文本内容&#xff0c;linux的文本三剑客 都是针对文本内容 grep 过滤文本内容 sed 针对文本内容进行增删改查 awk 按行取列 文本三剑客都是按行进行匹配。 grep grep 的作用就是使用正则表达式来匹配文本内容 选项&#xff1a; -m …

centos查找文件 写入的进程

du -sh * 查看目录空间占用、发现大文件&#xff0c;确定进程&#xff0c;结束 yum install lsof 安装lsof 查看文件写入的 进程 2. lsof /root/.influxdbv2/engine/data/bab49411e5f7cbce/autogen/1/000000036-000000002.tsm COMMAND PID USER FD TYPE …

Ubuntu-基础工具配置

基础工具配置 点击左下角 在弹出界面中点击 以下命令都是在上面这个界面执行&#xff08;请大家注意空格&#xff09; 命令输入完后&#xff0c;回车键就是执行,系统会提示输入密码&#xff08;就是你登录的密码&#xff09; 1.安装net工具 &#xff1a;&#xff08;ifconfi…

vue3-自定义指令来实现input框输入限制

文章目录 前言具体实现分析主要部分详细解析导入和类型定义mounted 钩子函数unmounted 钩子函数指令注册使用 总结 前言 使用vue中的自定义指令来实现input框输入限制 其中关键代码强制触发input &#xff0c;来避免&#xff0c;输入规则外的字符时&#xff0c;没触发vue的响…

无需安装就能一键部署Stable Diffusion 3?

一键部署使用SD3&#xff1f;让你的创作更加便捷&#xff01; 前言 厚德云上架SD3! 距离Stable Diffusion 3的上线已经有一阵时间了。从上线至今SD3也是一直好评不断&#xff0c;各项性能的提升也让它荣获“最强开源新模型”的称号。成为了AI绘画设计师们新的香馍馍。 可对于SD…

短期内股票跌了就难受的人有哪些?

短期内股票跌了难受的人&#xff0c;主要是四类 第一类压根就没有打算长期持有&#xff0c;就是玩短线的。这类人来股市是为了一夜暴富的。 第二类人&#xff0c;这类人也是打算一夜暴富的&#xff0c;但是他们会上杠杆&#xff0c;借钱买股票。股价涨了好说&#xff0c;股价…

python网站地图解析

分析&#xff1a; ⽹站的地图&#xff08;sitemap.xml&#xff09;是⼀个XML⽂件&#xff0c;列出了⽹站上所有可访问的⻚⾯的URL。解析⽹站的地图可以⾼效地发现⽹站上所有的⻚⾯&#xff0c;特别是那些可能不容易通过常规爬⾍发现的⻚⾯。 # Python代码&#xff1a; 以下是⼀…

Mac用虚拟机玩游戏很卡 Mac电脑玩游戏怎么流畅运行 苹果电脑怎么畅玩Windows游戏

对于许多Mac电脑用户而言&#xff0c;他们经常面临一个令人头疼的问题&#xff1a;在虚拟机中玩游戏时卡顿严重&#xff0c;影响了游戏体验。下面我们将介绍Mac用虚拟机玩游戏很卡&#xff0c;Mac电脑玩游戏怎么流畅运行的相关内容。 一、Mac用虚拟机玩游戏很卡 下面我们来看…

嵌入式期末复习--补充(答案来自文心一言)

一、第一章 1、常见的RTOS&#xff0c;嵌入式操作系统的特点 RTOS就是实时操作系统。根据响应时间的不同&#xff0c;可分为以下3类&#xff1a; &#xff08;1&#xff09;强实时嵌入式操作系统 响应时间&#xff1a;微妙或毫秒 &#xff08;2&#xff09;一般实时…

删除重复文件如何操作?电脑重复文件删除教程分享:详细!高效!

在数字化时代&#xff0c;我们的电脑中往往存储着大量的文件&#xff0c;这些文件随着时间的推移可能会产生许多重复项。重复文件不仅占用了宝贵的硬盘空间&#xff0c;还可能导致文件管理的混乱。因此&#xff0c;定期删除重复文件是维护电脑健康和提高工作效率的重要步骤。本…

请问为什么下面的HTML代码没有显示内容?

请问下面的HTML程序为什么没有显示内容&#xff1f; <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title>HTML教程()</title> <script>function getTime() {var date new Date();var time date.toLocalString…

OSPF和RIP的路由引入(华为)

#交换设备 OSPF和RIP的路由引入 不同的网络会根据自身的实际情况来选用路由协议。比如有些网络规模很小&#xff0c;为了管理简单&#xff0c;部署了 RIP; 而有些网络很复杂&#xff0c;可以部署 OSPF。不同路由协议之间不能直接共享各自的路由信息&#xff0c;需要依靠配置路…

洗地机哪个品牌比较好?四款好用靠谱的优质洗地机推荐

随着现代生活节奏的加快&#xff0c;家庭清洁成了一项耗时且繁琐的任务。洗地机凭借其智能化和高效的清洁能力&#xff0c;越来越受到大家的青睐。然而&#xff0c;市场上各种品牌和型号琳琅满目&#xff0c;让人眼花缭乱。为了帮助大家在众多选择中找到心仪的产品&#xff0c;…

力扣540.有序数组中的单一元素

力扣540.有序数组中的单一元素 找到 偶数位做二分 若不等 说明当前mid在单一元素右侧 r mid若相等 说明当前mid在单一元素左侧 l mid 2 class Solution {public:int singleNonDuplicate(vector<int>& nums) {int l 0,r nums.size() -1;while(l<r){int mid…

【话题】层出不穷的大模型产品,你怎么选?

大家好&#xff0c;我是全栈小5&#xff0c;欢迎阅读小5的系列文章&#xff0c;这是《话题》系列文章 目录 引言元宝体验产品介绍AI作画 文档总结AI超级产品文章推荐 引言 随着近日腾讯元宝APP的正式上线&#xff0c;国内大模型产品又添一员。 关于接连出现的“全能“大模型AI…

jfrog artifactory oss的下载地址

在这里记录下下载地址&#xff1a; https://releases.jfrog.io/artifactory/bintray-artifactory/org/artifactory/oss/jfrog-artifactory-oss/

Java 开发面试题精选:Mysql 一篇全搞定

在这里插入图片描述 前言 在高级Java开发工程师的面试中&#xff0c;MySQL作为常见的数据库技术&#xff0c;其掌握程度往往是评估候选人综合能力的重要组成部分。在这篇文章中&#xff0c;我精选了一些最可能被问到的与MySQL相关的面试题目&#xff0c;这些题目可以全面考察…

dockerfile镜像制作

镜像的制作方式 基于容器&#xff08;现有的镜像&#xff09;创建镜像 基于现有镜像创建主要使用 docker commit 命令&#xff0c;即把一个容器里面运行的程序以及该 程序的运行环境打包起来生成新的镜像 docker commit [选项] 容器ID/名称 仓库名称:[标签]-m说明信息&#…

芯片验证 | FPGA 原型验证

更多完整内容访问&#xff1a;【芯片验证 | FPGA 原型验证】