注:此文章主要基于展锐Android R代码加上学习总结自IngresGe大佬的分析

文章目录

  • 一、kthreadd
  • 二、init
  • 三、Init 进程入口
    • 3.1 ueventd_main
    • 3.2 FirstStageMain
    • 3.3 SetupSelinux
    • 3.4 SecondStageMain
  • init.rc文件解析

一、kthreadd

/bsp/kernel/kernel4.14/kernel/kthread.c

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);//允许kthreadd在任意CPU上运行set_cpus_allowed_ptr(tsk, cpu_all_mask);set_mems_allowed(node_states[N_MEMORY]);current->flags |= PF_NOFREEZE;cgroup_init_kthreadd();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;
}

二、init

kernel_init启动后,完成一些init的初始化操作,然后去系统根目录下依次找ramdisk_execute_command和execute_command设置的应用程序,如果这两个目录都找不到,就依次去根目录下找 /sbin/init,/etc/init,/bin/init,/bin/sh 这四个应用程序进行启动,只要这些应用程序有一个启动了,其他就不启动了。

Android系统一般会在根目录下放一个init的可执行文件,也就是说Linux系统的init进程在内核初始化完成后,就直接执行init这个文件。

static int __ref kernel_init(void *unused)
{int ret;//进行init进程的一些初始化操作kernel_init_freeable();/* need to finish all async __init code before freeing the memory */// 等待所有异步调用执行完成,,在释放内存前,必须完成所有的异步 __init 代码async_synchronize_full();ftrace_free_init_mem();// 释放所有init.* 段中的内存free_initmem();mark_readonly();// 设置系统状态为运行状态system_state = SYSTEM_RUNNING;// 设定NUMA系统的默认内存访问策略numa_default_policy();// 释放所有延时的struct file结构体rcu_end_inkernel_boot();pr_emerg("run init\n");//ramdisk_execute_command的值为"/init"if (ramdisk_execute_command) {ret = run_init_process(ramdisk_execute_command);//运行根目录下的init程序if (!ret)return 0;pr_err("Failed to execute %s (error %d)\n",ramdisk_execute_command, ret);}/** We try each of these until one succeeds.** The Bourne shell can be used instead of init if we are* trying to recover a really broken machine.*///execute_command的值如果有定义就去根目录下找对应的应用程序,然后启动if (execute_command) {ret = run_init_process(execute_command);if (!ret)return 0;panic("Requested init %s failed (error %d).",execute_command, ret);}//如果ramdisk_execute_command和execute_command定义的应用程序都没有找到,就到根目录下找 /sbin/init,/etc/init,/bin/init,/bin/sh 这四个应用程序进行启动if (!try_to_run_init_process("/sbin/init") ||!try_to_run_init_process("/etc/init") ||!try_to_run_init_process("/bin/init") ||!try_to_run_init_process("/bin/sh"))return 0;panic("No working init found.  Try passing init= option to kernel. ""See Linux Documentation/admin-guide/init.rst for guidance.");
}

进行init进程的一些初始化操作

static noinline void __init kernel_init_freeable(void)
{/** Wait until kthreadd is all set-up.*/wait_for_completion(&kthreadd_done);/* Now the scheduler is fully set up and can do blocking allocations */gfp_allowed_mask = __GFP_BITS_MASK;/** init can allocate pages on any node*/set_mems_allowed(node_states[N_MEMORY]);cad_pid = task_pid(current);smp_prepare_cpus(setup_max_cpus);workqueue_init();init_mm_internals();do_pre_smp_initcalls();lockup_detector_init();smp_init();sched_init_smp();page_alloc_init_late();/* Initialize page ext after all struct pages are initialized. */page_ext_init();do_basic_setup();test_executor_init();/* Open the /dev/console on the rootfs, this should never fail */if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)pr_err("Warning: unable to open an initial console.\n");(void) sys_dup(0);(void) sys_dup(0);/** check if there is an early userspace init.  If yes, let it do all* the work*/if (!ramdisk_execute_command)ramdisk_execute_command = "/init";if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {ramdisk_execute_command = NULL;prepare_namespace();}/** Ok, we have completed the initial bootup, and* we're essentially up and running. Get rid of the* initmem segments and start the user-mode stuff..** rootfs is available now, try loading the public keys* and default modules*/integrity_load_keys();load_default_modules();
}
/** Ok, the machine is now initialized. None of the devices* have been touched yet, but the CPU subsystem is up and* running, and memory and process management works.** Now we can finally start doing some real work..*/
static void __init do_basic_setup(void)
{//针对SMP系统,初始化内核control group的cpuset子系统。cpuset_init_smp();// 初始化共享内存shmem_init();// 初始化设备驱动   driver_init();//创建/proc/irq目录, 并初始化系统中所有中断对应的子目录init_irq_proc();// 执行内核的构造函数do_ctors();// 启用usermodehelperusermodehelper_enable();//遍历initcall_levels数组,调用里面的initcall函数,这里主要是对设备、驱动、文件系统进行初始化,之所有将函数封装到数组进行遍历,主要是为了好扩展do_initcalls();
}

以上就是init启动的相关操作,接下来看看它启动之后会做哪些操作,从它的主函数入手

三、Init 进程入口

system/core/init/main.cpp

/*1. 1.第一个参数argc表示参数个数,第二个参数是参数列表,也就是具体的参数2. 2.main函数有四个参数入口,*一是参数中有ueventd,进入ueventd_main*二是参数中有subcontext,进入InitLogging 和SubcontextMain*三是参数中有selinux_setup,进入SetupSelinux*四是参数中有second_stage,进入SecondStageMain*3.main的执行顺序如下:3.  (1)ueventd_main    init进程创建子进程ueventd,4.      并将创建设备节点文件的工作托付给ueventd,ueventd通过两种方式创建设备节点文件5.  (2)FirstStageMain  启动第一阶段6.  (3)SetupSelinux     加载selinux规则,并设置selinux日志,完成SELinux相关工作7.  (4)SecondStageMain  启动第二阶段*/
int main(int argc, char** argv) {//当argv[0]的内容为ueventd时,strcmp的值为0,!strcmp为1//1表示true,也就执行ueventd_main,ueventd主要是负责设备节点的创建、权限设定等一些列工作if (!strcmp(basename(argv[0]), "ueventd")) {return ueventd_main(argc, argv);}//当传入的参数个数大于1时,执行下面的几个操作if (argc > 1) {//参数为subcontext,初始化日志系统,if (!strcmp(argv[1], "subcontext")) {android::base::InitLogging(argv, &android::base::KernelLogger);const BuiltinFunctionMap function_map;return SubcontextMain(argc, argv, &function_map);}//参数为“selinux_setup”,启动Selinux安全策略if (!strcmp(argv[1], "selinux_setup")) {return SetupSelinux(argv);}//参数为“second_stage”,启动init进程第二阶段if (!strcmp(argv[1], "second_stage")) {return SecondStageMain(argc, argv);}}// 默认启动init进程第一阶段return FirstStageMain(argc, argv);
}
3.1 ueventd_main

system/core/init/ueventtd.cpp

int ueventd_main(int argc, char** argv) {//设置新建文件的默认值,这个与chmod相反,这里相当于新建文件后的权限为666umask(000); //初始化内核日志,位于节点/dev/kmsg, 此时logd、logcat进程还没有起来,//采用kernel的log系统,打开的设备节点/dev/kmsg, 那么可通过cat /dev/kmsg来获取内核log。android::base::InitLogging(argv, &android::base::KernelLogger);//注册selinux相关的用于打印log的回调函数SelinuxSetupKernelLogging(); SelabelInitialize();//解析xml,根据不同SOC厂商获取不同的hardware rc文件auto ueventd_configuration = ParseConfig({"/ueventd.rc", "/vendor/ueventd.rc","/odm/ueventd.rc", "/ueventd." + hardware + ".rc"});//冷启动if (access(COLDBOOT_DONE, F_OK) != 0) {ColdBoot cold_boot(uevent_listener, uevent_handlers);cold_boot.Run();}for (auto& uevent_handler : uevent_handlers) {uevent_handler->ColdbootDone();}//忽略子进程终止信号signal(SIGCHLD, SIG_IGN);// Reap and pending children that exited between the last call to waitpid() and setting SIG_IGN// for SIGCHLD above.//在最后一次调用waitpid()和为上面的sigchld设置SIG_IGN之间退出的获取和挂起的子级while (waitpid(-1, nullptr, WNOHANG) > 0) {}//监听来自驱动的uevent,进行“热插拔”处理uevent_listener.Poll([&uevent_handlers](const Uevent& uevent) {for (auto& uevent_handler : uevent_handlers) {uevent_handler->HandleUevent(uevent); //热启动,创建设备}return ListenerAction::kContinue;});return 0;
}

由init开启的一个子进程用来创建设备节点文件,有两种方式

  1. “冷插”(Cold Plug):即以预先定义的设备信息为基础,当ueventd启动后,统一创建设备节点文件。这一类设备节点文件也被称为静态节点文件。
  2. “热插拔”(Hot Plug):即在系统运行中,当有设备插入USB端口时,ueventd就会接收到这一事件,为插入的设备动态创建设备节点文件。这一类设备节点文件也被称为动态节点文件。

其中完成的主要操作如下:

  • 设置文件权限
  • 初始化内核日志(/dev/kmsg),可通过cat /dev/kmsg来获取内核log
  • 注册selinux相关的用于打印log的回调函数
  • 解析xml,根据不同SOC厂商获取不同的hardware rc文件
  • 冷启动
  • 监听热启动进行处理
3.2 FirstStageMain

第一阶段主要完成了:

  1. 挂载分区
  2. 创建设备节点和关键目录
  3. 初始化日志系统
  4. 启动selinux安全策略
    system\core\init\first_stage_init.cpp
int FirstStageMain(int argc, char** argv) {//init crash时重启引导加载程序//这个函数主要作用将各种信号量,如SIGABRT,SIGBUS等的行为设置为SA_RESTART,一旦监听到这些信号即执行重启系统if (REBOOT_BOOTLOADER_ON_PANIC) {InstallRebootSignalHandlers();}//清空文件权限umask(0);CHECKCALL(clearenv());CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));//在RAM内存上获取基本的文件系统,剩余的被rc文件所用CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));CHECKCALL(mkdir("/dev/pts", 0755));CHECKCALL(mkdir("/dev/socket", 0755));CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
#define MAKE_STR(x) __STRING(x)CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
#undef MAKE_STR// 非特权应用不能使用Andrlid cmdlineCHECKCALL(chmod("/proc/cmdline", 0440));gid_t groups[] = {AID_READPROC};CHECKCALL(setgroups(arraysize(groups), groups));CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));if constexpr (WORLD_WRITABLE_KMSG) {CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));}CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));//这对于日志包装器是必需的,它在ueventd运行之前被调用CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));//在第一阶段挂在tmpfs、mnt/vendor、mount/product分区。其他的分区不需要在第一阶段加载,//只需要在第二阶段通过rc文件解析来加载。CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,"mode=0755,uid=0,gid=1000"));//创建可供读写的vendor目录CHECKCALL(mkdir("/mnt/vendor", 0755));// /mnt/product is used to mount product-specific partitions that can not be// part of the product partition, e.g. because they are mounted read-write.CHECKCALL(mkdir("/mnt/product", 0755));// 挂载APEX,这在Android 10.0中特殊引入,用来解决碎片化问题,类似一种组件方式,对Treble的增强,// 不写谷歌特殊更新不需要完整升级整个系统版本,只需要像升级APK一样,进行APEX组件升级CHECKCALL(mount("tmpfs", "/apex", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,"mode=0755,uid=0,gid=0"));// /debug_ramdisk is used to preserve additional files from the debug ramdiskCHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,"mode=0755,uid=0,gid=0"));
#undef CHECKCALL//把标准输入、标准输出和标准错误重定向到空设备文件"/dev/null"SetStdioToDevNull(argv);//在/dev目录下挂载好 tmpfs 以及 kmsg //这样就可以初始化 /kernel Log 系统,供用户打印logInitKernelLogging(argv);.../* 初始化一些必须的分区*主要作用是去解析/proc/device-tree/firmware/android/fstab,* 然后得到"/system", "/vendor", "/odm"三个目录的挂载信息*/if (!DoFirstStageMount()) {LOG(FATAL) << "Failed to mount required partitions early ...";}struct stat new_root_info;if (stat("/", &new_root_info) != 0) {PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";old_root_dir.reset();}if (old_root_dir && old_root_info.st_dev != new_root_info.st_dev) {FreeRamdisk(old_root_dir.get(), old_root_info.st_dev);}SetInitAvbVersionInRecovery();static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;setenv("INIT_STARTED_AT", std::to_string(start_ms).c_str(), 1);//启动init进程,传入参数selinux_steup// 执行命令: /system/bin/init selinux_setupconst char* path = "/system/bin/init";const char* args[] = {path, "selinux_setup", nullptr};execv(path, const_cast<char**>(args));PLOG(FATAL) << "execv(\"" << path << "\") failed";return 1;
}
3.3 SetupSelinux

此阶段主要完成了:初始化selinux,加载SELinux规则,配置SELinux相关log输出,并启动第二阶段
system\core\init\selinux.cpp

/*此函数初始化selinux,然后执行init以在init selinux中运行*/
int SetupSelinux(char** argv) {//初始化Kernel日志InitKernelLogging(argv);// Debug版本init crash时重启引导加载程序if (REBOOT_BOOTLOADER_ON_PANIC) {InstallRebootSignalHandlers();}//注册回调,用来设置需要写入kmsg的selinux日志SelinuxSetupKernelLogging();//加载SELinux规则SelinuxInitialize();/**我们在内核域中,希望转换到init域。在其xattrs中存储selabel的文件系统(如ext4)不需要显式restorecon,*但其他文件系统需要。尤其是对于ramdisk,如对于a/b设备的恢复映像,这是必需要做的一步。*其实就是当前在内核域中,在加载Seliux后,需要重新执行init切换到C空间的用户态*/if (selinux_android_restorecon("/system/bin/init", 0) == -1) {PLOG(FATAL) << "restorecon failed of /system/bin/init failed";}//准备启动innit进程,传入参数second_stageconst char* path = "/system/bin/init";const char* args[] = {path, "second_stage", nullptr};execv(path, const_cast<char**>(args));/**执行 /system/bin/init second_stage, 进入第二阶段*/PLOG(FATAL) << "execv(\"" << path << "\") failed";return 1;
}
3.4 SecondStageMain
  1. 创建进程会话密钥并初始化属性系统
  2. 进行SELinux第二阶段并恢复一些文件安全上下文
  3. 新建epoll并初始化子进程终止信号处理函数
  4. 启动匹配属性的服务端
  5. 解析init.rc等文件,建立rc文件的action 、service,启动其他进程

此阶段内容过于繁杂,主要了解了一下rc文件的解析。
之前启动进程都是通过exec传参的方式启动,如果每个都是这样的方式启动就会无比繁琐,所以推出了init.rc这个机制。

init.rc文件解析


init.rc主要包含五种类型语句:Action Command Service Option Import

action由一组command命令组成,包含一个触发器,以on开头

command常用命令:

    class_start <service_class_name>: 启动属于同一个class的所有服务;class_stop <service_class_name> : 停止指定类的服务start <service_name>: 启动指定的服务,若已启动则跳过;stop <service_name>: 停止正在运行的服务setprop <name> <value>:设置属性值mkdir <path>:创建指定目录symlink <target> <sym_link>: 创建连接到<target>的<sym_link>符号链接;write <path> <string>: 向文件path中写入字符串;exec: fork并执行,会阻塞init进程直到程序完毕;exprot <name> <name>:设定环境变量;loglevel <level>:设置log级别hostname <name> : 设置主机名import <filename> :导入一个额外的init配置文件

options:

Options是Service的可选项,与service配合使用disabled: 不随class自动启动,只有根据service名才启动;oneshot: service退出后不再重启;user/group: 设置执行服务的用户/用户组,默认都是root;class:设置所属的类名,当所属类启动/退出时,服务也启动/停止,默认为default;onrestart:当服务重启时执行相应命令;socket: 创建名为/dev/socket/<name>的socketcritical: 在规定时间内该service不断重启,则系统会重启并进入恢复模式default: 意味着disabled=false,oneshot=false,critical=false。

解析init.rc
system/core/init/init.cpp

static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {Parser parser = CreateParser(action_manager, service_list);std::string bootscript = GetProperty("ro.boot.init_rc", "");if (bootscript.empty()) {std::string bootmode = GetProperty("ro.bootmode", "");if (bootmode == "charger") {parser.ParseConfig("/vendor/etc/init/charge.rc");} else {parser.ParseConfig("/init.rc");if (!parser.ParseConfig("/system/etc/init")) {late_import_paths.emplace_back("/system/etc/init");}if (!parser.ParseConfig("/product/etc/init")) {late_import_paths.emplace_back("/product/etc/init");}if (!parser.ParseConfig("/product_services/etc/init")) {late_import_paths.emplace_back("/product_services/etc/init");}if (!parser.ParseConfig("/odm/etc/init")) {late_import_paths.emplace_back("/odm/etc/init");}if (!parser.ParseConfig("/vendor/etc/init")) {late_import_paths.emplace_back("/vendor/etc/init");}}} else {parser.ParseConfig(bootscript);}
}

创建解析对象,service on import

Parser CreateParser(ActionManager& action_manager, ServiceList& service_list) {Parser parser;parser.AddSectionParser("service", std::make_unique<ServiceParser>(&service_list, subcontexts));parser.AddSectionParser("on", std::make_unique<ActionParser>(&action_manager, subcontexts));parser.AddSectionParser("import", std::make_unique<ImportParser>(&parser));return parser;
}

init.rc中===>import /init.${ro.zygote}.rc 通过此值来判断加载哪一个rc文件
在/system/core/rootdir下,存在init.zygoteXXX.rc,此例为init.zygote32.rc

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-serverclass mainpriority -20//设置用户 rootuser root  //访问组支持 root readproc reserved_diskgroup root readproc reserved_disk//创建一个socket,名字叫zygote,以tcp形式  ,可以在/dev/socket 中看到一个 zygote的socketsocket zygote stream 660 root systemsocket usap_pool_primary stream 660 root system// onrestart 指当进程重启时执行后面的命令onrestart write /sys/android_power/request_state wakeonrestart write /sys/power/state ononrestart restart audioserveronrestart restart cameraserveronrestart restart mediaonrestart restart netdonrestart restart wificond// 创建子进程时,向 /dev/cpuset/foreground/tasks 写入pidwritepid /dev/cpuset/foreground/tasks

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server

定义一个名为zygote的service,执行/system/bin/app_process二进制文件,传入四个参数
-Xzygote ---->将作为虚拟机启动时所需的参数
/system/bin ---->代表虚拟机程序所在目录
--zygote ---->指明以ZygoteInit.java类中的main函数作为虚拟机执行入口
--start-system-server ---->启动systemServer进程

*以上就是对init进程启动及主要流程的一个学习记录,大佬的分析思路很清晰,赞一个!还有很多不懂的地方还需跟进学习。

kthreadd和init进程的启动(二)相关推荐

  1. Android系统启动流程--init进程的启动流程

    这可能是个系列文章,用来总结和梳理Android系统的启动过程,以加深对Android系统相对全面的感知和理解(基于Android11).  1.启动电源,设备上电 引导芯片代码从预定义的地方(固化在 ...

  2. 3. Linux系统启动分析-从start_kernel到init进程的启动

    ##################################### 作者:张卓 原创作品转载请注明出处:<Linux操作系统分析>MOOC课程 http://www.xuetang ...

  3. [日更-2019.4.8、4.9、4.12、4.13] cm-14.1 Android系统启动过程分析(一)-init进程的启动、rc脚本解析、zygote启动、属性服务...

    2019独角兽企业重金招聘Python工程师标准>>> 声明 前阶段在项目中涉及到了Android系统定制任务,Android系统定制前提要知道Android系统是如何启动的. 本文 ...

  4. cm-14.1 Android系统启动过程分析(4)-init进程的启动、rc脚本解析、zygote启动、属性服务

    声明 前阶段在项目中涉及到了Android系统定制任务,Android系统定制前提要知道Android系统是如何启动的. 本文参考了一些书籍的若干章节,比如<Android进阶解密-第2章-An ...

  5. 【鸿蒙OS开发入门】06 - 启动流程代码分析之KernelOS:之启动Linux-4.19 Kernel内核 启动init进程

    [鸿蒙OS开发入门]06 - 启动流程代码分析之KernelOS:之启动Linux-4.19 Kernel内核 一.head.S 启动start_kernel() 1.1 start_kernel() ...

  6. Android系统启动流程—— init进程zygote进程SystemServer进程启动流程

    原文地址:https://blog.csdn.net/qq_30993595/article/details/82714409 Android系统启动流程 Android系统启动过程往细了说可以分为5 ...

  7. 从源码解析-Android系统启动流程概述 init进程zygote进程SystemServer进程启动流程

    Android系统启动流程 启动流程 Loader Kernel Native Framework Application init进程 启动 rc文件规则 Actions Commands Serv ...

  8. Android 9 (P)之init进程启动源码分析指南之三

          Android 9 (P)之init进程启动源码分析指南之三 Android 9 (P)系统启动及进程创建源码分析目录: Android 9 (P)之init进程启动源码分析指南之一 An ...

  9. Android系统10 RK3399 init进程启动(三十五) 属性文件介绍和生成过程

    配套系列教学视频链接: 安卓系列教程之ROM系统开发-百问100ask 说明 系统:Android10.0 设备: FireFly RK3399 (ROC-RK3399-PC-PLUS) 前言 ini ...

最新文章

  1. java 删除list元素_JAVA中循环删除list中元素的方法总结
  2. 大数据实训记录(一)
  3. 编程方法学笔记:karel
  4. reactjs jsx语法规则
  5. VS2013编译OBS源码
  6. python远程调用摄像头_Python设置Socket代理及实现远程摄像头控制的例子
  7. C++ Primer 第10章 习题10.24
  8. centos7 python3.7 ssl_centos 解决python3.7 安装时No module named _ssl
  9. 重构 - 美股行情系统APP推送改造
  10. 【OpenCV入门教程之一】 OpenCV 2.4.8 +VS2010的开发环境配置
  11. leetcode题解—1021、删除最外层的括号
  12. lucene 分词实现
  13. 男子因惧内欲退还iPad 2苹果免费赠送
  14. js读取json文件(原生和jQuery)
  15. 清华学姐教你如何用python处理excel数据
  16. 路由器下一跳地址怎么判断_三分钟了解路由器路由表
  17. 什么是数据库的存储过程?
  18. 重庆医科大学赵浏阳教授招收博士、招聘博士后
  19. 大于/小于/等于 的缩写
  20. [高通SDM450][Android9.0]CTA认证--去除某些应用开机使用定位权限

热门文章

  1. 苹果电脑删除linux系统软件,Linux中如何删除CrossOver?CrossOver卸载教程
  2. 云计算自动化运维——saltstack之jinja模块详解
  3. IAR常用快捷键设置
  4. 图片万能居中css图片居中
  5. 使用阿里云 dns sdk 解决电信公网ip自动变化问题;自己动手实现ddns
  6. 三字经 (搞笑英文版 )
  7. windows文件介绍
  8. 【报告分享】 网易知萌:2020酒行业睿享生活消费趋势报告(附下载)
  9. 关于陌陌签名验证机制的研究
  10. IC行业开年喜忧并存,芯片降价还需等待