Tini 源码分析(2)
Tini 源码分析(2)
Tini 源码分析
我会从程序在 main 函数中执行的顺序,来一步一步的分析源码。每一个函数我都会贴上源码,然后进行分析。我把 main 函数的大概流程画了出来,大家可以参考一下。文章太长发不了,只能分割成两章了,这是第二章。
父进程死亡,此进程收到的信号
if (parent_death_signal && prctl(PR_SET_PDEATHSIG, parent_death_signal)) {PRINT_FATAL("Failed to set up parent death signal");return 1;}
检测是否能收割
void reaper_check () {/* 检查我们是否能正确收割僵尸进程 */
#if HAS_SUBREAPERint bit = 0;
#endifif (getpid() == 1) {return;}#if HAS_SUBREAPERif (prctl(PR_GET_CHILD_SUBREAPER, &bit)) {PRINT_DEBUG("Failed to read child subreaper attribute: %s", strerror(errno));} else if (bit == 1) {return;}
#endifPRINT_WARNING(reaper_warning);
}
创建子进程
// 创建子进程
int spawn(const signal_configuration_t* const sigconf_ptr, char* const argv[], int* const child_pid_ptr) {pid_t pid;// TODO: check if tini was a foreground process to begin with (it's not OK to "steal" the foreground!")pid = fork();if (pid < 0) {PRINT_FATAL("fork failed: %s", strerror(errno));return 1;} else if (pid == 0) {// 把子进程放在一个进程组中,如果有tty的话,让它成为前台进程。if (isolate_child()) {return 1;}// 将所有的信号处理程序恢复到我们 触碰 它们之前的样子。if (restore_signals(sigconf_ptr)) {return 1;}execvp(argv[0], argv);// execvp只会在出错时返回,所以要确保我们检查errno,并为我们遇到的错误提供正确的返回状态。// See: http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREFint status = 1;switch (errno) {case ENOENT:status = 127;break;case EACCES:status = 126;break;}PRINT_FATAL("exec %s failed: %s", argv[0], strerror(errno));return status;} else {// ParentPRINT_INFO("Spawned child process '%s' with pid '%i'", argv[0], pid);*child_pid_ptr = pid;return 0;}
}
这里面主进程 fork() 了一个子进程出来。后面则是主进程和子进程的不同路线了,如果 pid 小于 0 则表示进程创建失败,打印错误信息并返回。
如果 pid == 0 则表示此进程为子进程,会进入以下的代码块:
else if (pid == 0) {// 把子进程放在一个进程组中,如果有tty的话,让它成为前台进程。if (isolate_child()) {return 1;}// 将所有的信号处理程序恢复到我们 触碰 它们之前的样子。if (restore_signals(sigconf_ptr)) {return 1;}execvp(argv[0], argv);// execvp只会在出错时返回,所以要确保我们检查errno,并为我们遇到的错误提供正确的返回状态。// See: http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREFint status = 1;switch (errno) {case ENOENT:status = 127;break;case EACCES:status = 126;break;}PRINT_FATAL("exec %s failed: %s", argv[0], strerror(errno));return status;
int isolate_child() {// 把子进程放到一个新的进程组中。 setpgid(0, 0) 与 setpgid(getpid(), getpid()) 等价if (setpgid(0, 0) < 0) {PRINT_FATAL("setpgid failed: %s", strerror(errno));return 1;}// 如果有一个tty,把它分配给这个新的进程组。// 我们可以在子进程中做到这一点,因为我们正在阻止SIGTTIN / SIGTTOU。// 如果Tini调用Tini,在子进程中这样做可以避免出现竞争条件的情况// 在子进程中这样做可以避免出现竞争条件的情况进程组,而实际的子进程最终在后台!)。// getpgrp()用来取得目前进程所属的组识别码。. 此函数相当于调用 getpgid (0);返回目前进程所属的组识别码。.// tcsetpgrp 函数---设置前台进程组ID// int tcsetpgrp(int fd, pid_t pgrpid);// 函数功能:使用 pgrpid 设置前台进程组ID,fd 必须引用该会话的控制终端,0 代表当前正在使用的终端// 返回值:成功返回 0,出错返回 -1if (tcsetpgrp(STDIN_FILENO, getpgrp())) {if (errno == ENOTTY) {PRINT_DEBUG("tcsetpgrp failed: no tty (ok to proceed)");} else if (errno == ENXIO) {// can occur on lx-branded zonesPRINT_DEBUG("tcsetpgrp failed: no such device (ok to proceed)");} else {PRINT_FATAL("tcsetpgrp failed: %s", strerror(errno));return 1;}}return 0;
}
if (setpgid(0, 0) < 0) {PRINT_FATAL("setpgid failed: %s", strerror(errno));return 1;}
int setpgid(pid_t pid,pid_t pgid);
函数作用:将pid进程的进程组ID设置成pgid,创建一个新进程组或加入一个已存在的进程组。
性质1:一个进程只能为自己或子进程设置进程组ID,不能设置其父进程的进程组ID。
性质2:if(pid == pgid),由pid指定的进程变成进程组长;即进程pid的进程组ID pgid=pid。
性质3:if(pid==0),将当前进程的pid作为进程组ID。
setpgid(0, 0) 与 setpgid(getpid(), getpid()) 等价
所以这段代码的含义就是把这个进程(子进程)放到一个新的进程组中,并设置为这个进程组的组长,进程组号就是这个进程的 pid 。
// 如果有一个tty,把它分配给这个新的进程组。// 我们可以在子进程中做到这一点,因为我们正在阻止SIGTTIN / SIGTTOU。// 如果Tini调用Tini,在子进程中这样做可以避免出现竞争条件的情况// 在子进程中这样做可以避免出现竞争条件的情况进程组,而实际的子进程最终在后台!)。// getpgrp()用来取得目前进程所属的组识别码。. 此函数相当于调用 getpgid (0);返回目前进程所属的组识别码。// tcsetpgrp 函数---设置前台进程组ID// int tcsetpgrp(int fd, pid_t pgrpid);// 函数功能:使用 pgrpid 设置前台进程组ID,fd 必须引用该会话的控制终端,0 代表当前正在使用的终端// 返回值:成功返回 0,出错返回 -1if (tcsetpgrp(STDIN_FILENO, getpgrp())) {if (errno == ENOTTY) {PRINT_DEBUG("tcsetpgrp failed: no tty (ok to proceed)");} else if (errno == ENXIO) {// can occur on lx-branded zonesPRINT_DEBUG("tcsetpgrp failed: no such device (ok to proceed)");} else {PRINT_FATAL("tcsetpgrp failed: %s", strerror(errno));return 1;}}
getpgrp()用来取得目前进程所属的组识别码。. 此函数相当于调用 getpgid (0);返回目前进程所属的组识别码。
int tcsetpgrp(int fd, pid_t pgrpid);
所以这段代码的含义就是把前台进程组的 pgid 设置成 此进程的 pgid。
然后是restore_signals(sigconf_ptr)
这个函数
// 恢复原来的信号响应
int restore_signals(const signal_configuration_t* const sigconf_ptr) {if (sigprocmask(SIG_SETMASK, sigconf_ptr->sigmask_ptr, NULL)) {PRINT_FATAL("Restoring child signal mask failed: '%s'", strerror(errno));return 1;}if (sigaction(SIGTTIN, sigconf_ptr->sigttin_action_ptr, NULL)) {PRINT_FATAL("Restoring SIGTTIN handler failed: '%s'", strerror((errno)));return 1;}if (sigaction(SIGTTOU, sigconf_ptr->sigttou_action_ptr, NULL)) {PRINT_FATAL("Restoring SIGTTOU handler failed: '%s'", strerror((errno)));return 1;}return 0;
}
在上面将配置主进程信号的时候解释过 sigprovmask() 这个函数的作用,这里就不过多赘述了。
等待并转发信号
int wait_and_forward_signal(sigset_t const* const parent_sigset_ptr, pid_t const child_pid) {siginfo_t sig;if (sigtimedwait(parent_sigset_ptr, &sig, &ts) == -1) {switch (errno) {case EAGAIN:break;case EINTR:break;default:PRINT_FATAL("Unexpected error in sigtimedwait: '%s'", strerror(errno));return 1;}} else {/* 这里有一个信号需要处理 */switch (sig.si_signo) {case SIGCHLD:/* 特殊情况下,因为我们不转发SIGCHLD。相反,我们将陷入收割过程。 */PRINT_DEBUG("Received SIGCHLD");break;default:PRINT_DEBUG("Passing signal: '%s'", strsignal(sig.si_signo));/* 转发其他所有信号 */if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo)) {if (errno == ESRCH) {PRINT_WARNING("Child was dead when forwarding signal");} else {PRINT_FATAL("Unexpected error when forwarding signal: '%s'", strerror(errno));return 1;}}break;}}return 0;
}
这个函数是在主进程死循环里面的一个函数,在我的上一篇文章提到过这段函数。
int sigtimedwait(const sigset_t *set, soirnfo_t *info, const struct timespec *timeout),
成功时,sigtimedwait() 返回信号号(即,大于零的值)。失败时,两个调用均返回-1,并设置errno来指示错误。
- EAGAIN :在sigtimedwait()指定的超时期限内,没有信号置入待处理状态;
- **EINTR :**等待被信号处理程序中断;
- **EINVAL :**超时无效。
所以 sigtimedwait(parent_sigset_ptr, &sig, &ts) == -1
的意思就是等待信号,如果 sigtimedwait(parent_sigset_ptr, &sig, &ts)
的返回值为 -1 的话就执行下面的代码:
switch (errno) {case EAGAIN:break;case EINTR:break;default:PRINT_FATAL("Unexpected error in sigtimedwait: '%s'", strerror(errno));return 1;}
判断 errno 的信息,如果是 EAGAIN 表示 1 秒内没有信号处于待处理的状态,则直接返回;如果是 EINTR 则表示等待被信号处理程序中断,此处理程序用于信号而不是集合中的信号之一。如果不是这两个值,则表示有其他不可预料的错误,打印错误信息,并直接返回。
如果 sigtimedwait(parent_sigset_ptr, &sig, &ts)
的返回值不为 -1 就执行 else 后面的语句:
/* 这里有一个信号需要处理 */switch (sig.si_signo) {case SIGCHLD:/* 特殊情况下,因为我们不转发SIGCHLD。相反,我们将陷入收割过程。 */PRINT_DEBUG("Received SIGCHLD");break;default:PRINT_DEBUG("Passing signal: '%s'", strsignal(sig.si_signo));/* 转发其他所有信号 */if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo)) {if (errno == ESRCH) {PRINT_WARNING("Child was dead when forwarding signal");} else {PRINT_FATAL("Unexpected error when forwarding signal: '%s'", strerror(errno));return 1;}}break;}
int kill(pid_t pid, int sig); 函数可以给对应的 pid 发送 指定的信号。
参数:pid:可能选择有以下四种:
- pid大于零时,pid是信号欲送往的进程的标识。
- pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
- pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了1 号(init) 进程。
- pid小于-1时,信号将送往以-pid为组标识的进程。
sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。
返回值说明: 成功执行时,返回0。失败返回-1,
errno被设为以下的某个值:
- EINVAL:指定的信号码无效(参数 sig 不合法(INVAL:invalid))
- EPERM;权限不够无法传送信号给指定进程 (PERM:permission权限)
- ESRCH:参数 pid 所指定的进程或进程组不存在(SRCH:search)
这里的 sig.si_signo
就是前面等待接收到的信号了,如果信号是 SIGCHLD,SIGCHLD 这个信号在子进程状态变更了,例如停止、继续、退出等,都会发送这个信号通知父进程。不过我们都不用处理,如果是子进程退出了,reap_zombies()
会帮我们收割僵尸进程的。
如果是其他信号,则会进行转发,也就是调用 kill(),发送给 子进程还是子进程所在的进程组,则取决于 kill_process_group
。
kill_process_group
这个值上面我们讲过,默认为 0,如果在命令行加了 -g
参数,或者在环境变量中配置了 KILL_PROCESS_GROUP_GROUP_ENV_VAR
都会使其 加 1。则会使这里的 kill() 函数发送给 -child_pid 即子进程所在的进程组。
收割僵尸进程
int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) {pid_t current_pid;int current_status;while (1) {current_pid = waitpid(-1, ¤t_status, WNOHANG);switch (current_pid) {case -1:if (errno == ECHILD) {PRINT_TRACE("No child to wait");break;}PRINT_FATAL("Error while waiting for pids: '%s'", strerror(errno));return 1;case 0:PRINT_TRACE("No child to reap");break;default:/* 一个孩子被收割了。检查它是否是主进程。* 如果是,那么设置exit_code,这将导致我们在收割完其他所有人后退出。*/PRINT_DEBUG("Reaped child with pid: '%i'", current_pid);if (current_pid == child_pid) {if (WIFEXITED(current_status)) {/* 我们的进程正常退出。 */PRINT_INFO("Main child exited normally (with status '%i')", WEXITSTATUS(current_status));*child_exitcode_ptr = WEXITSTATUS(current_status);} else if (WIFSIGNALED(current_status)) {/* 我们的进程被终止了。模仿sh / bash 的做法,即返回128 + 信号数。 */PRINT_INFO("Main child exited with signal (with signal '%s')", strsignal(WTERMSIG(current_status)));*child_exitcode_ptr = 128 + WTERMSIG(current_status);} else {PRINT_FATAL("Main child exited for unknown reason");return 1;}// 安全起见,确保退出码 在0和255之间。*child_exitcode_ptr = *child_exitcode_ptr % (STATUS_MAX - STATUS_MIN + 1);// 如果这个退出码 被重新设置,那么就把它设置为0。INT32_BITFIELD_CHECK_BOUNDS(expect_status, *child_exitcode_ptr);if (INT32_BITFIELD_TEST(expect_status, *child_exitcode_ptr)) {*child_exitcode_ptr = 0;}} else if (warn_on_reap > 0) {PRINT_WARNING("Reaped zombie process with pid=%i", current_pid);}// 检查其他子进程是否已被收割。continue;}/* 如果我们能到这里,那是因为我们没有 continue in the switch case. */break;}return 0;
}
了解这段代码,我们还需要知道 waitpid() 这个函数的含义和用法。
- pid < -1,表示等待进程组号为 pid 绝对值的任何子进程。
- pid = -1,表示等待任何子进程。
- pid = 0,表示等待进程组号与目前进程相同的任何子进程,也就是说任何和调用 waitpid() 函数的进程在同一个进程组的进程。
- pid > 0,表示等待进程号为 pid 的子进程。
这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会推出,是正常推出还是出了什么错误。如果status不是空指针,则状态信息将被写入
器指向的位置。当然,如果不关心子进程为什么推出的话,也可以传入空指针。
Linux提供了一些非常有用的宏来帮助解析这个状态信息,这些宏都定义在sys/wait.h头文件中。主要有以下几个:
- WIFEXITED(status),表示如果子进程正常结束,它就返回真;否则返回假。
- WEXITSTATUS(status),表示如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。
- WIFSIGNALED(status),表示如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。
- WTERMSIG(status),表示如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。
init options
参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0。主要使用的有以下两个选项:
- WNOHANG,表示如果pid指定的子进程没有结束,则waitpid()函数立即返回 0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。
- WUNTRACED,表示如果子进程进入暂停状态,则马上返回。
如果waitpid()函数执行成功,则返回子进程的进程号;如果有错误发生,则返回-1,并且将失败的原因存放在errno变量中。
失败的原因主要有:没有子进程(errno设置为ECHILD),调用被某个信号中断(errno设置为EINTR)或选项参数无效(errno设置为EINVAL)。
知道 waitpid() 这个函数后,我们就就可以看这段代码了。
在死循环里面 通过 waitpid() 来回收僵尸进程。查看其返回值:
- 如果为 -1 则代表出错了,检查错误信息,如果错误信息是 ECHILD,则表示没有子进程,打印 trace 信息并跳出死循环;如果错误信息不是 ECHILD,则表示不可预期的错误,直接返回。
- 如果错误信息如果为 0,则表示没有子进程退出,也就没有僵尸进程需要回收,则跳出死循环。
- 如果为其他值,则表示有一个子进程被收割了。switch 进入 default 下面的代码,并会再次循环:
/* 一个孩子被收割了。检查它是否是主进程。* 如果是,那么设置exit_code,这将导致我们在收割完其他所有人后退出。*/PRINT_DEBUG("Reaped child with pid: '%i'", current_pid);if (current_pid == child_pid) {if (WIFEXITED(current_status)) {/* 我们的进程正常退出。 */PRINT_INFO("Main child exited normally (with status '%i')", WEXITSTATUS(current_status));*child_exitcode_ptr = WEXITSTATUS(current_status);} else if (WIFSIGNALED(current_status)) {/* 我们的进程被终止了。模仿sh / bash 的做法,即返回128 + 信号数。 */PRINT_INFO("Main child exited with signal (with signal '%s')", strsignal(WTERMSIG(current_status)));*child_exitcode_ptr = 128 + WTERMSIG(current_status);} else {PRINT_FATAL("Main child exited for unknown reason");return 1;}// 安全起见,确保退出码 在0和255之间。*child_exitcode_ptr = *child_exitcode_ptr % (STATUS_MAX - STATUS_MIN + 1);// 如果这个退出码 被重新设置,那么就把它设置为0。INT32_BITFIELD_CHECK_BOUNDS(expect_status, *child_exitcode_ptr);if (INT32_BITFIELD_TEST(expect_status, *child_exitcode_ptr)) {*child_exitcode_ptr = 0;}} else if (warn_on_reap > 0) {PRINT_WARNING("Reaped zombie process with pid=%i", current_pid);}// 检查其他子进程是否已被收割。continue;
这里的 current_pid 表示的是此进程创建的子进程的 pid,如果 current_pid == child_pid
,则表示被收割的僵尸进程,就是自己创建的子进程,会执行下面的代码:
if (WIFEXITED(current_status)) {/* 我们的进程正常退出。 */PRINT_INFO("Main child exited normally (with status '%i')", WEXITSTATUS(current_status));*child_exitcode_ptr = WEXITSTATUS(current_status);} else if (WIFSIGNALED(current_status)) {/* 我们的进程被终止了。模仿sh / bash 的做法,即返回128 + 信号数。 */PRINT_INFO("Main child exited with signal (with signal '%s')", strsignal(WTERMSIG(current_status)));*child_exitcode_ptr = 128 + WTERMSIG(current_status);} else {PRINT_FATAL("Main child exited for unknown reason");return 1;}// 安全起见,确保退出码 在0和255之间。*child_exitcode_ptr = *child_exitcode_ptr % (STATUS_MAX - STATUS_MIN + 1);// 如果这个退出码 被重新设置,那么就把它设置为0。INT32_BITFIELD_CHECK_BOUNDS(expect_status, *child_exitcode_ptr);if (INT32_BITFIELD_TEST(expect_status, *child_exitcode_ptr)) {*child_exitcode_ptr = 0;}
首先判断子进程是否是正常退出 WIFEXITED(current_status)
,如果是正常退出则设置子进程退出码为 WEXITSTATUS(current_status)
子进程退出的返回代码。
如果不是正常退出,则再判断子进程是否是因为一个未捕获的信号而退出的 WIFSIGNALED(current_status)
,如果是,则打印是哪一个信号暂停的子进程 strsignal(WTERMSIG(current_status)))
。并模仿 sh / bash 的做法,将子进程退出码赋值为 128 + WTERMSIG(current_status)
即 128 + 子进程退出的返回码。
如果都不是,则表示是未知的理由退出的,直接打印错误信息,并返回。
为了确保安全,退出码在 0 - 255 之间
#define STATUS_MAX 255
#define STATUS_MIN 0*child_exitcode_ptr = *child_exitcode_ptr % (STATUS_MAX - STATUS_MIN + 1);
STATUS_MAX 的值是 255, STATUS_MIN 的值是 0。通过求余来确保退出码的范围。
#define INT32_BITFIELD_CHECK_BOUNDS(F, i) do { assert(i >= 0); assert(ARRAY_LEN(F) > (uint) (i / 32)); } while(0)
#define INT32_BITFIELD_TEST(F, i) ( F[(i / 32)] & (1 << (i % 32)) )
static int32_t expect_status[(STATUS_MAX - STATUS_MIN + 1) / 32];// 如果这个退出码 被重新设置,那么就把它设置为0。INT32_BITFIELD_CHECK_BOUNDS(expect_status, *child_exitcode_ptr);if (INT32_BITFIELD_TEST(expect_status, *child_exitcode_ptr)) {*child_exitcode_ptr = 0;}
INT32_BITFIELD_CHECK_BOUNDS() 通过断言子进程退出码在 expect_status 范围中的 (也就是 0- 255)。
INT32_BITFIELD_TEST() 判断子进程退出码是否是之前定义过的,也就是我们在输入命令时,加上 -e
选项后带的参数。如果子进程退出码是我们之前设置过的,就将退出码设置为 0。
else if (warn_on_reap > 0) {PRINT_WARNING("Reaped zombie process with pid=%i", current_pid);}
如果被收割的僵尸进程不是该进程创建的子进程,则打印警告信息,表明已经收割了 pid 为多少的子进程。
进程退出
if (child_exitcode != -1) {PRINT_TRACE("Exiting: child has exited");return child_exitcode;}
换句话说,就是当此进程创建出来的子进程退出后,就会使 child_exitcode != -1
成立,从而跳出死循环,进而此进程也会跟着退出。
到这里,就已经将 Tini 源码分析完了,可能分析的不太全面,请见谅。我会继续努力,后续写出更有价值的博客。
Tini 源码分析(2)相关推荐
- Tini 源码分析(1)
Tini 源码分析(1) (奇怪,我是一个写 Go 的,为什么要来分析 C 代码的项目啊!) Tini 简介 Tini 是一个超轻量级的 init 进程管理器,被设计作为容器的 1 号进程. Tini ...
- 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析
目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...
- SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)
[SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...
- SpringBoot-web开发(二): 页面和图标定制(源码分析)
[SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...
- SpringBoot-web开发(一): 静态资源的导入(源码分析)
目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...
- Yolov3Yolov4网络结构与源码分析
Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...
- ViewGroup的Touch事件分发(源码分析)
Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...
- View的Touch事件分发(二.源码分析)
Android中Touch事件的分发又分为View和ViewGroup的事件分发,先来看简单的View的touch事件分发. 主要分析View的dispatchTouchEvent()方法和onTou ...
- MyBatis原理分析之四:一次SQL查询的源码分析
上回我们讲到Mybatis加载相关的配置文件进行初始化,这回我们讲一下一次SQL查询怎么进行的. 准备工作 Mybatis完成一次SQL查询需要使用的代码如下: Java代码 String res ...
最新文章
- 《iOS 6高级开发手册(第4版)》——1.11节秘诀:获取和使用设备姿势
- APUE读书笔记-09进程关系(04)
- ceph的数据存储之路(6) -----pg的创建
- 2019年计算机考研408历年真题2009-2019下载免费下载
- 如何快速集成短信验证码API[图文教程]
- SylixOS -- 双网卡冗余备份使用说明
- “全球化”是一个漫长过程,海尔智家用了20年
- 服务器怎么直接访问数据库文件路径,如何在服务器中找到数据库文件路径
- 读书笔记-财务报表资本结构分析
- 如何用Python批量提取PPT中含有某关键词的一页,并将这些PPT合并
- 大数据华而不实么?大数据的本质是什么?
- □ 影片名:《樱桃小丸子》(36004) 在线播放
- 微信搭建本地开发测试环境
- linux实训报告内容一万字,Linux实训报告.doc
- numpy 查找 返回索引_numpy中实现ndarray数组返回符合特定条件的索引方法
- 爬虫实战 | 手把手用Python教你采集可视化知乎问题的回答(内附代码)
- FreeSWITCH API常用手册
- UNITY 围绕一个物体做圆周运动
- 2003集群中的域控服务器配置,配置Windows2003集群(MSCS)与iSCSI
- 基于R语言的seasonal包使用手册_10.na.x13(x)