https://zhuanlan.zhihu.com/p/581587583

Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。这与用户线程是不一样的。因为内核线程只运行在内核态,因此,它只能使用大于PAGE_OFFSET(3G)的地址空间。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,mm指针被设置为NULL;它只在 内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。

内核线程(thread)或叫守护进程(daemon),在操作系统中占据相当大的比例,当Linux操作系统启动以后,你可以用”ps -ef”命令查看系统中的进程,这时会发现很多以”d”结尾的进程名,确切说名称显示里面加 "[]"的,这些进程就是内核线程。

内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,它只在 内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。让模块在加载后能一直运行下去的方法——内核线程。要创建一个内核线程有许多种方法,我们这里要学的是最简单的一种。打开include/linux/kthread.h,你就看到了它全部的API,一共三个函数:

struct task_struct kthread_run(int (*threadfn)(void *data),
void *data, const char namefmt[],...);
int kthread_stop(struct task_struct *k);
int kthread_should_stop(void);

一、线程的创建

/**
* kthread_run - create and wake a thread.
* @threadfn: the function to run until signal_pending(current).
* @data: data ptr for @threadfn.
* @namefmt: printf-style name for the thread.
*
* Description: Convenient wrapper for kthread_create() followed by
* wake_up_process(). Returns the kthread or ERR_PTR(-ENOMEM).
*/
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})

这个函数的英文注释里很明确的说明: 创建并启动一个内核线程。可见这里的函数kthread_create()只是创建了内核线程,而后面的这个函数wake_up_process()则是启动了这个线程,让它在一开始就一直运行下去。直到遇见kthread_should_stop函数或者kthread_stop()函数。

学习地址(腾讯课堂):Linux内核源码/进程管理/内存管理/网络协议/设备驱动/文件系统

kthread_run实际是一个宏定义,它由kthread_create()和wake_up_process()两部分组成,调用了kthread_create后执行了wake_up_process.这样的好处是用kthread_run()创建的线程可以直接运行,使用方便。
kthread_run()负责内核线程的创建,参数包括入口函数threadfn,参数data,线程名称namefmt。可以看到线程的名字可以是类似sprintf方式组成的字符串。如果线程创建成功,再调用wake_up_process()唤醒新创建的线程。kthread_create()根据参数向kthread_create_list中发送一个请求,并唤醒kthreadd,之后会调用wait_for_completion(&create.done)等待线程创建完成。新创建的线程开始运行后,入口在kthread(),kthread()调用complete(&create->done)唤醒阻塞的模块进程,并使用schedule()调度出去。kthread_create()被唤醒后,设置新线程的名称,并返回到kthread_run中。kthread_run调用wake_up_process()重新唤醒新创建线程,此时新线程才开始运行kthread_run参数中的入口函数。

struct kthread {

       int should_stop;

       struct completion exited;

};

kthread() (注:原型为:static int kthread(void *_create) )的实现在kernel/kthread.c中,头文件是include/linux/kthread.h。内核中一直运行一个线程kthreadd,它运行kthread.c中的kthreadd函数。在kthreadd()中,不断检查一个kthread_create_list链表。kthread_create_list中的每个节点都是一个创建内核线程的请求,kthreadd()发现链表不为空,就将其第一个节点退出链表,并调用create_kthread()创建相应的线程。create_kthread()则进一步调用更深层的kernel_thread()创建线程,入口函数设在kthread()中。

int kthreadd(void *unused)
{ struct task_struct *tsk = current; /* Setup a clean context for our children to inherit. */ set_task_comm(tsk, "kthreadd"); ignore_signals(tsk); set_cpus_allowed_ptr(tsk, cpu_all_mask); set_mems_allowed(node_states[N_HIGH_MEMORY]); current->flags |= PF_NOFREEZE | PF_FREEZER_NOSIG; for (;;) { set_current_state(TASK_INTERRUPTIBLE); if (list_empty(&kthread_create_list)) schedule(); __set_current_state(TASK_RUNNING); spin_lock(&kthread_create_lock); while (!list_empty(&kthread_create_list)) { struct kthread_create_info *create; create = list_entry(kthread_create_list.next, struct kthread_create_info, list); list_del_init(&create->list); spin_unlock(&kthread_create_lock); create_kthread(create); spin_lock(&kthread_create_lock); } spin_unlock(&kthread_create_lock); } return 0;
}

那我们具体看看前一个函数到底做了什么吧。

在这个宏里面主要是调用了函数:kthread_create()

这个函数是干什么的呢?在Kernel/Kthread.c里面我们可以看到:

/**
* kthread_create - create a kthread.
* @threadfn: the function to run until signal_pending(current).
* @data: data ptr for @threadfn.
* @namefmt: printf-style name for the thread.
*
* Description: This helper function creates and names a kernel
* thread. The thread will be stopped: use wake_up_process() to start
* it. See also kthread_run(), kthread_create_on_cpu().
*
* When woken, the thread will run @threadfn() with @data as its
* argument. @threadfn can either call do_exit() directly if it is a
* standalone thread for which noone will call kthread_stop(), or
* return when 'kthread_should_stop()' is true (which means
* kthread_stop() has been called). The return value should be zero
* or a negative error number; it will be passed to kthread_stop().
*
* Returns a task_struct or ERR_PTR(-ENOMEM).
*/
struct task_struct *kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...)
{
struct kthread_create_info create;
DECLARE_WORK(work, keventd_create_kthread, &create); create.threadfn = threadfn;
create.data = data;
init_completion(&create.started);
init_completion(&create.done); /*
* The workqueue needs to start up first:
*/
if (!helper_wq)
work.func(work.data);
else {
queue_work(helper_wq, &work);
wait_for_completion(&create.done);
}
if (!IS_ERR(create.result)) {
va_list args;
va_start(args, namefmt);
vsnprintf(create.result->comm, sizeof(create.result->comm),
namefmt, args);
va_end(args);
} return create.result;
}
EXPORT_SYMBOL(kthread_create);

注意到上面的这段英文解释:说这个函数会创建一个名为namefmt的内核线程,这个线程刚创建时不会马上执行,要等到它将kthread_create() 返回的task_struct指针传给wake_up_process(),然后通过此函数运行线程。我们看到creat结构体,我们将传入的参数付给了它,而threadfn这个函数就是创建的运行函数。在使用中我们可以在此函数中调用kthread_should_stop()或者kthread_stop()函数来结束线程。这里我们看到创建线程函数中使用工作队列DECLARE_WORK,我们跟踪一下发现这只是将函数

#define DECLARE_WORK(n, f, d)      \
struct work_struct n = __WORK_INITIALIZER(n, f, d)

然后再跟进:

#define __WORK_INITIALIZER(n, f, d) {     \
.entry = { &(n).entry, &(n).entry }, \
.func = (f), \
.data = (d), \
.timer = TIMER_INITIALIZER(NULL, 0, 0), \
}

目的是创建一个工作组队列,而其中keventd_create_kthread()函数主要是起到创建线程的功能

/* We are keventd: create a thread. */
static void keventd_create_kthread(void *_create)
{
struct kthread_create_info *create = _create;
int pid; /* We want our own signal handler (we take no signals by default). */
pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
if (pid < 0) {
create->result = ERR_PTR(pid);
} else {
wait_for_completion(&create->started);
read_lock(&tasklist_lock);
create->result = find_task_by_pid(pid);
read_unlock(&tasklist_lock);
}
complete(&create->done);
}

再看看kernel_thread()函数最后调用到了哪里:

/*
* Create a kernel thread.
*/
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
struct pt_regs regs;
long pid; memset(s, 0, sizeof(regs)); regs.ARM_r1 = (unsigned long)arg;
regs.ARM_r2 = (unsigned long)fn;
regs.ARM_r3 = (unsigned long)do_exit;
regs.ARM_pc = (unsigned long)kernel_thread_helper;
regs.ARM_cpsr = SVC_MODE; pid = do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, s, 0, NULL, NULL); MARK(kernel_thread_create, "%ld %p", pid, fn);
return pid;
}
EXPORT_SYMBOL(kernel_thread);

最后我们看到了线程通过申请进程的pid号来被创建,关键是我们要知道如何使用这个宏函数,也就是如何应用它。要注意的是它调用了创建线程函数,同时也激活了线程。所以代码中调用了它的话就隐含着已经启动一个线程。

在非内核线程中调用kernel_thread,必须在调用daemonize(...)来释放资源,成为真正的内核线程,kthread_create实际调用kernel_thread但是内部已经做了处理,不需要自己调用daemonize。

二、线程的退出

/**
* kthread_stop - stop a thread created by kthread_create().
* @k: thread created by kthread_create().
*
* Sets kthread_should_stop() for @k to return true, wakes it, and
* waits for it to exit. This can also be called after kthread_create()
* instead of calling wake_up_process(): the thread will exit without
* calling threadfn().
*
* If threadfn() may call do_exit() itself, the caller must ensure
* task_struct can't go away.
*
* Returns the result of threadfn(), or %-EINTR if wake_up_process()
* was never called.
*/ int kthread_stop(struct task_struct *k)
{ struct kthread *kthread; int ret; trace_sched_kthread_stop(k); get_task_struct(k); kthread = to_kthread(k); barrier(); /* it might have exited */ if (k->vfork_done != NULL) { kthread->should_stop = 1; wake_up_process(k); wait_for_completion(&kthread->exited); } ret = k->exit_code; put_task_struct(k); trace_sched_kthread_stop_ret(ret); return ret; }

kthread_stop:设置线程的退出标记(线程函数内应用int kthread_should_stop(void)函数,当返回真时应退出函数),kthread_stop会一直等待至线程结束,线程结束前会发送完成结束给kthread_stop,如果直接使用do_exit直接退出线程那么kthread_stop不会收到完成信号将一直等待下去。如果线程已经退出那么kthread_stop会先设置退出标记再唤醒一下thread,唤醒线程后会判断退出标记因此设定的处理函数不会被调用。如果线程已经被唤醒并已经退出那么kthread_stop会一直等待。
如果处理函数没用kthread_should_stop判断退出,那么 kthread_stop会一直等待处理函数主动退出。 kthread_stop()负责结束创建的线程,参数是创建时返回的task_struct指针。kthread设置标志should_stop,并等待线程主动结束,返回线程的返回值。线程可能在kthread_stop()调用前就结束。(经过实际验证,如果线程在kthread_stop()调用之前就结束,之后kthread_stop()再调用会发生可怕地事情—调用kthread_stop()的进程crash!!之所以如此,是由于kthread实现上的弊端)在调用 kthread_stop()结束线程之前一定要检查该线程是否还在运行(通过 kthread_run 返回的 task_stuct 是否有效),否则会造成灾难性的后果。
kthread_should_stop()返回should_stop标志(参见 struct kthread )。它用于创建的线程检查结束标志,并决定是否退出。线程完全可以在完成自己的工作后主动结束,不需等待should_stop标志。

外界调用kthread_stop()删除线程。kthread_stop首先设置结束标志should_stop,然后调用wake_for_completion(&kthread->exited)上,这个其实是新线程task_struct上的vfork_done,会在线程结束调用do_exit()时设置。

三、源码分析 (内核版本是2.6.21.5)

3.1 管理调度其它的内核线程kthread

使用ps命令可以查看有个名叫kthread的进程,它在内核初始化的时候被创建。

static __init int helper_init(void)
{
//创建一个单线程的共享列队
helper_wq = create_singlethread_workqueue("kthread");
BUG_ON(!helper_wq);
return 0;
}
core_initcall(helper_init);

就是这个共享列队kthread_create会定义一个工作,在工作内创建创建具体的线程。

3.2 kthread_create创建线程

再看kthread_create前先看下kthread_create_info结构,每个线程创建时使用。

struct kthread_create_info
{
/* Information passed to kthread() from keventd. */
int (*threadfn)(void *data); //线程处理函数
void *data; //线程参数
struct completion started; //在工作中等待kernel_thread创建线程完成,线程创建完后线程会通知工作继续。 /* Result passed back to kthread_create() from keventd. */
struct task_struct *result; // started当收到线程创建完信号started后,用来存放创建的任务结构体
struct completion done; // 工作者线程加入一个工作后会等待工作做完,这个工作只是创建线程。
struct work_struct work; // 创建线程的工作,具体工作看后面源码
};
/**
* kthread_create - 创建一个线程.
* @threadfn: the function to run until signal_pending(current).
* @data: data ptr for @threadfn.
* @namefmt: printf-style name for the thread.
*
* 描述:这个帮助函数创建并命名一个内核线程,线程创建后并不运行,使用wake_up_process() 函数来运行,参考kthread_run(), kthread_create_on_cpu()
*
*被唤醒后,线程调用threadfn()函数data作为参数,如果是独立线程没有其他线程调用 kthread_stop()那么可以直接使用do_exit(),或当检测到kthread_should_stop()返回真时(kthread_stop()已被调用了)返回处理函数 , 应返回0或负数,返回值会传给 kthread_stop()返回。
*/
struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
{
struct kthread_create_info create; //下面五行初始化kthread_create_info
create.threadfn = threadfn;
create.data = data;
init_completion(&create.started);
init_completion(&create.done);
INIT_WORK(&create.work, keventd_create_kthread); //可见创建的工作是在keventd_create_kthread函数内进行 /*The workqueue needs to start up first:*/
if (!helper_wq) //这个系统启动后正常是已经初始化了的
create.work.func(&create.work); //如没初始化那只有在当前进程下完成工作了而不是在kthread 里
else {
queue_work(helper_wq, &create.work); //将工作加入列队并调度
wait_for_completion(&create.done); //等待工作执行完,执行完后create.result返回创建的任务结构或错误,由于工作是在kthread 里执行所以必须等待工作做完才能返回
}
if (!IS_ERR(create.result)) {
va_list args;
va_start(args, namefmt);
vsnprintf(create.result->comm, sizeof(create.result->comm),
namefmt, args);
va_end(args);
} return create.result;
}

上面看到创建工作是在keventd_create_kthread函数里,那么看下keventd_create_kthread函数

/* We are keventd: create a thread.   这个函数工作在keventd内核线程中*/
static void keventd_create_kthread(struct work_struct *work)
{
struct kthread_create_info *create =container_of(work, struct kthread_create_info, work);
int pid; /* We want our own signal handler (we take no signals by default)*/
/*我们使用自己的信号处理,默认不处理信号*/
pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);//在这里创建函数,线程处理函数为kthread函数,参数为struct kthread_create_info指针create。 if (pid < 0) {
create->result = ERR_PTR(pid);
} else {
wait_for_completion(&create->started); //等待创建的线程执行,线程执行后会发送完成信号create->started
read_lock(&tasklist_lock);
create->result = find_task_by_pid(pid);
read_unlock(&tasklist_lock);
}
complete(&create->done);
}

这时kthread_create在等待create->done信号,内核线程keventd在等待线程创建完create->started。上面创建了线程,处理函数为kthread

static int kthread(void *_create)
{
struct kthread_create_info *create = _create;
int (*threadfn)(void *data);
void *data;
sigset_t blocked;
int ret = -EINTR; kthread_exit_files(); /* Copy data: it's on keventd's stack */
threadfn = create->threadfn;
data = create->data; /* Block and flush all signals (in case we're not from keventd). 阻塞全部信号*/
sigfillset(&blocked);
sigprocmask(SIG_BLOCK, &blocked, NULL);
flush_signals(current); /* By default we can run anywhere, unlike keventd. 允许线程在任意CPU上运行 keventd值在1个CPU上运行*/
set_cpus_allowed(current, CPU_MASK_ALL); /* OK, tell user we're spawned, wait for stop or wakeup */
__set_current_state(TASK_INTERRUPTIBLE);
complete(&create->started); //这里通知keventd完成线程初始化,keventd收到后获取新线程的任务结构,然后发出工作完成的信号后kthread_create返回。
schedule(); if (!kthread_should_stop()) //判断先前是否调用过kthread_stop
ret = threadfn(data); //这里才真正执行定义的线程函数 /* It might have exited on its own, w/o kthread_stop. Check. */
if (kthread_should_stop()) { //判断是否执行过kthread_stop
kthread_stop_info.err = ret; //ret是线程函数的返回,后面会经过kthread_stop函数返回
complete(&kthread_stop_info.done); //如执行过kthread_stop 还要通知kthread_stop线程完成结束了,如果用户定义的处理函数使用了do_exit那么就不会通知kthread_stop,造成kthread_stop一直等待。
}
return 0;
}

至此我们看到kthread_create是如何创建线程,和线程是如何工作的了

3.3 kthread_stop线程的停止

先看下停止相关的结构

struct kthread_stop_info
{
struct task_struct *k; //要停止的线程结构
int err; //返回值
struct completion done; //线程完成结束的等待信号
};
/* Thread stopping is done by setthing this var: lock serializes multiple kthread_stop calls. */
/* 线程结束锁 kthread_stop在整个系统内一次只能被一个线程调用*/
static DEFINE_MUTEX(kthread_stop_lock);
static struct kthread_stop_info kthread_stop_info;
/**
* kthread_should_stop - should this kthread return now?
* When someone calls kthread_stop() on your kthread, it will be woken
* and this will return true. You should then return, and your return
* value will be passed through to kthread_stop().
*/
int kthread_should_stop(void)
{
return (kthread_stop_info.k == current);
}

这个函数在kthread_stop()被调用后返回真,当返回为真时你的处理函数要返回,返回值会通过kthread_stop()返回。所以你的处理函数应该有判断kthread_should_stop然后退出的代码。

/**
* kthread_stop - stop a thread created by kthread_create().
* @k: thread created by kthread_create().
*
* Sets kthread_should_stop() for @k to return true, wakes it, and
* waits for it to exit. Your threadfn() must not call do_exit()
* itself if you use this function! This can also be called after
* kthread_create() instead of calling wake_up_process(): the thread
* will exit without calling threadfn().
*
* Returns the result of threadfn(), or %-EINTR if wake_up_process()
* was never called.
*/
int kthread_stop(struct task_struct *k)
{
int ret;
mutex_lock(&kthread_stop_lock); //系统一次只能处理一个结束线程申请
/* It could exit after stop_info.k set, but before wake_up_process. */
get_task_struct(k); //增加线程引用计数
/* Must init completion *before* thread sees kthread_stop_info.k */
init_completion(&kthread_stop_info.done);
smp_wmb();
/* Now set kthread_should_stop() to true, and wake it up. */
kthread_stop_info.k = k;//设置了这个之后 kthread_should_stop() 会返回真
wake_up_process(k); //不管线程有没运行 先叫醒再说(如果已经唤醒过并结束了,该线程是唤醒不了的,这样会造成后面一直等待kthread_stop_info.done信号),即便没运行叫醒后也不会运行用户定义的函数。
put_task_struct(k);
/* Once it dies, reset stop ptr, gather result and we're done. */
wait_for_completion(&kthread_stop_info.done);//等待线程结束
kthread_stop_info.k = NULL;
ret = kthread_stop_info.err; //返回值
mutex_unlock(&kthread_stop_lock);
return ret;
}

注意如果调用了kthread_stop你的处理函数不能调用do_exit(),函数返回你处理函数的返回值,如果创建的线程还没调用过wake_up_process()那么会返回-EINTR .

四、测试代码

struct task_struct *mytask;
/*代码中要有kthread_should_stop()判断 至于返回值只对kthread_stop才有意义*/
int func(void* data)
{
while(1 )
{
if( kthread_should_stop()) return -1;
printk(KERN_ALERT "func running\n");
set_current_state(TASK_UNINTERRUPTIBLE);
schedule_timeout(1*HZ);
}
return 0;
} 线程创建和驱动
mytask=kthread_create(func,0,"mykthread");
wake_up_process(mytask); 在需要结束的地方调用
kthread_stop(mytask);

通过几个函数可以很容易的创建内核线程,但线程创建出来之后我们更关注的是有多线程带来的并发和竞争问题。并发的管理是操作系统编程的核心问题之一,引起的错误是一些最易出现又最难发现的问题.

[转帖]Linux内核线程kthread简介【最好的一篇!】的更多相关文章

  1. Linux 内核引导选项简介

    Linux 内核引导选项简介 作者:金步国 连接地址:http://www.jinbuguo.com/kernel/boot_parameters.html 参考参数:https://www.cnbl ...

  2. Linux内核线程创建

    本文旨在简单介绍一下Linux内核线程: 先举个例子: 不插U盘,在Linux命令行中输入:ps -el:然后插上U盘,再次输入:ps -el 会发现多出了下面一行(当然还会有其他的,比如scsi相关 ...

  3. Java线程与Linux内核线程的映射关系(转)

    Java线程与Linux内核线程的映射关系 Java线程与Linux内核线程的映射关系Linux从内核2.6开始使用NPTL (Native POSIX Thread Library)支持,但这时线程 ...

  4. Linux内核线程的思考与总结

    1.内核线程,只是一个称呼,实际上就是一个进程,有自己独立的TCB,参与内核调度,也参与内核抢占. 这个进程的特别之处有两点,第一.该进程没有前台.第二.永远在内核态中运行. 2.创建内核线程有两种方 ...

  5. Linux内核线程kernel thread详解--Linux进程的管理与调度(十)

    内核线程 为什么需要内核线程 Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求). 内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的. 内核线程 ...

  6. Linux内核线程kernel thread详解--Linux进程的管理与调度(十)【转】

    转自:http://blog.csdn.net/gatieme/article/details/51589205 日期 内核版本 架构 作者 GitHub CSDN 2016-06-02 Linux- ...

  7. 常见linux内核线程说明

    ps进程名有方括号的是内核级的进程,执行辅助功能(比如将缓存写入到磁盘):所有其他进程都是使用者进程.您会注意到,就算是在您新安装的(最小化的)系统中,也会有很多进程在运行. 在文档kernel-pe ...

  8. Linux内核线程之深入浅出【转】

    转自:http://blog.csdn.net/yiyeguzhou100/article/details/53126626 [-] 线程和进程的差别 线程的分类 1     内核线程 2     轻 ...

  9. Java线程与Linux内核线程的映射关系[转]

    Linux从内核2.6开始使用NPTL (Native POSIX Thread Library)支持,但这时线程本质上还轻量级进程. Java里的线程是由JVM来管理的,它如何对应到操作系统的线程是 ...

  10. 【转】Java线程与Linux内核线程的映射关系

    Linux从内核2.6开始使用NPTL (Native POSIX Thread Library)支持,但这时线程本质上还轻量级进程. Java里的线程是由JVM来管理的,它如何对应到操作系统的线程是 ...

随机推荐

  1. 2023-10-28:用go语言,给定一个n*m的二维矩阵,每个位置都是字符, U、D、L、R表示传送带的位置,会被传送到 : 上、下、左、右, . 、O分别表示空地、目标,一定只有一个目标点, 可以

    2023-10-28:用go语言,给定一个n*m的二维矩阵,每个位置都是字符, U.D.L.R表示传送带的位置,会被传送到 : 上.下.左.右, . .O分别表示空地.目标,一定只有一个目标点, 可以 ...

  2. 文心一言 VS 讯飞星火 VS chatgpt (62)-- 算法导论6.5 1题

    文心一言 VS 讯飞星火 VS chatgpt (62)-- 算法导论6.5 1题 一.试说明 HEAP-EXTRACT-MAX在堆A=(15,13,9,5,12,8,7,4,0,6,2,1)上的操作 ...

  3. 自定义Graph Component:1.1-JiebaTokenizer具体实现

      JiebaTokenizer类继承自Tokenizer类,而Tokenizer类又继承自GraphComponent类,GraphComponent类继承自ABC类(抽象基类).本文使用<使 ...

  4. 打通数据治理全链路,火山引擎DataLeap数据治理平台公有云版本正式发布

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群   近日,火山引擎DataLeap正式对外发布数据治理平台公有云版.DataLeap是火山引擎大数据研发治理套件, ...

  5. 火山引擎DataLeap的Catalog系统搜索实践(三):Learning to rank与后续工作

    Learning to rank Learning to rank主要分为数据收集,离线训练和在线预测三个部分.搜索系统是一个Data-driven system,因此火山引擎DataLeap的Cat ...

  6. 火山引擎DataLeap数据调度实例的 DAG 优化方案(三):技术实现

    在原始数据中,是以一个数组的形式返回节点信息及依赖关系.所以,需要对数据进行处理形成图所需要的数据,同时,利用多个 map 对数据进行存储,方便后续对数据进行检索,减少时间复杂度. 实例节点的样式需要 ...

  7. Axure 单键快捷键

    如果怕误操作,可以把它关闭

  8. 如何利用CCXT交易数字货币合约

    更多精彩内容,欢迎关注公众号:数量技术宅,也可添加技术宅个人微信号:sljsz01,与我交流. 对于币圈量化老司机来说,相信或多或少都有接触过ccxt这个接口,ccxt为我们提供了多交易所统一的标准格 ...

  9. 这两种完全不同的JPEG加载方式,你肯定见过!

    现如今网站所使用的的图片格式多种多样,但是有一种图片格式占到了 74% 的使用量.它就是 JPEG,即联合图像专家组.这类文件的后缀通常为 .jpg 或 .jpeg,是摄影中常见的图片类型. JPEG ...

  10. C223 生产版本BAPI

    1.事务代码:C223 2.调用函数CM_FV_PROD_VERS_DB_UPDATE "-----------------------------@斌将军----------------- ...