这里开始分析init进程中配置文件的解析,在配置文件中的命令的执行和服务的启动。
  首先init是一个可执行文件,它的对应的Makfile是init/Android.mk。 Android.mk定义了init
程序在编译的时候,使用了哪些源码,以及生成方式。当init程序生成之后,最终会放到/init,
即根目录的init文件。通常所说的init进程就是执行这个init程序。

  执行这个init程序的代码是在KERNEL/init/main.c文件中的kernel_init()函数里,当kernel
把一些基本的工作,比如CPU初始化,内存初始化,输入输出初始化等等完成之后,就会找到
根目录下的init程序,然后执行这个程序。

  在Android系统中,init程序对应的代码在ANDROID/system/core/init/下,用Android.mk,文件
管理编译的。这个程序的入口函数是init.c的main函数。在Android下init进程的主要功能和其它
发行版的Linux的系统差不多,都是通过解析配置文件,完成Linux系统的基本的操作,比如
用户级别,权限的设定,一些安全策略的启动,如selinux的启动,基本的文件(/dev, /sys等)创建,
然后就是其它基本的进程。 init的进程的id永远为1,在系统中最基本的进程之一。

今天我们主要目的目标如下:
  1) init进程是如何解析配置文件的
  2) init进程是如何启动其它基本的进程的

1 init进程是如何解析配置文件

  1.1 了解init.rc文件的语法(见init.rc语法)
  

1.2 init.rc对应的数据结构

  parse_state这个结构体是用来跟踪整个init.rc文件解析过程的。这个在阅读代码时,把这个结构体理解为一个篮子,

这个篮子中放的都是各种各样的rc文件内容即可。

  1. struct parse_state
  2. struct command{
  3. struct listnode clist;//命令数列
  4.  
  5. int (*func)(int nargs, char **args);//当前命令对应的函数,比如write命令,对应do_write(xxx)
  6. int nargs; //参数个数
  7. char *args[]; //参数数组
  8. };

  action就是on xxx对应的部分,action主要是由command组成。Android系统在开机执行的顺序,
是按照这个action顺序执行的。也就是说系统触发不同的trigger,就顺序执行这个trigger对应的action
下的所有命令,这个执行过程是顺序执行的,没有并行进行。而且系统在启动过程中,都是按照action去
执行的。记住,是在开机过程中才是这么执行的。

  1. struct action {
  2. struct listnode alist;//系统中所有的action的队列
  3. struct listnode qlist;//等待触发trigger的队列
  4. struct listnode tlist;//这个队列的意义不明,好在也没人用到这个队列
  5.  
  6. unsigned hash;
  7. const char *name;//这个名字就是triggeer的名字
  8.  
  9. struct listnode commands;//在当前这个action下所有的comman的队列
  10. struct command *current;//当前要执行的命令
  11. };

  service也是系统的一个SECTION,这样的SECTION仅有service, import, on xxxx这三种。对这三种
不同的SECTION解析的函数是不同的。记住on xxxx就是一个action。这个数据结构就是对应一个service,在
系统中service是不能直接执行的,它仅仅是一个service的属性信息的集合,为service的启动提供全面的信息,
但是它不是一个命令,所以不能直接执行。前面这些command可能仅仅就是在init进程中执行,但是service不同。
service启动是在一个新的进程中进行的。也就是说init进程会先fork一个进程,然后通过exec家族函数去启动
这个service。所以service是运行在一个新的进程中的。

  1. struct service

  1.3  init.rc文件的解析

  对于init.rc文件的解析主要是通过以下代码实现的[init.c文件中]:

  1. init_parse_config_file("/init.rc");

这句代码是解析所有init.rc文件的入口,其他的*.rc文件都是通过init.rc中通过import一级一级地导入的;
所以从这句代码入手是最合适的地方。 init_parse_config_file函数读取init.rc文件,并调用parse_config函数,
这个函数才是真正解析init.rc文件的地方。先看看这个函数的主要部分,如下[init_parce.c文件中]:

  1. static void parse_config(const char *fn, char *s)
  2. {
  3. //第一部分
  4. struct parse_state state;
  5. struct listnode import_list;
  6. struct listnode *node;
  7. char *args[INIT_PARSER_MAXARGS];
  8. int nargs;
  9.  
  10. nargs = ;
  11. state.filename = fn;
  12. state.line = ;
  13. state.ptr = s;
  14. state.nexttoken = ;
  15. state.parse_line = parse_line_no_op;
  16.  
  17. list_init(&import_list);
  18. state.priv = &import_list;
  19. //第二部分
  20. for (;;) {
  21. switch (next_token(&state)) {
  22. case T_EOF:
  23. state.parse_line(&state, , );
  24. goto parser_done;
  25. case T_NEWLINE:
  26. state.line++;
  27. if (nargs) {
  28. int kw = lookup_keyword(args[]);
  29. if (kw_is(kw, SECTION)) {
  30. state.parse_line(&state, , );
  31. parse_new_section(&state, kw, nargs, args);
  32. } else {
  33. state.parse_line(&state, nargs, args);
  34. }
  35. nargs = ;
  36. }
  37. break;
  38. case T_TEXT:
  39. if (nargs < INIT_PARSER_MAXARGS) {
  40. args[nargs++] = state.text;
  41. }
  42. break;
  43. }
  44. }
  45. //第三部分
  46. parser_done:
  47. list_for_each(node, &import_list) {
  48. struct import *import = node_to_item(node, struct import, list);
  49. int ret;
  50.  
  51. INFO("importing '%s'", import->filename);
  52. ret = init_parse_config_file(import->filename);
  53. if (ret)
  54. ERROR("could not import file '%s' from '%s'\n",
  55. import->filename, fn);
  56. }
  57. }

把这个函数分成三部分理解会更方便,首先第一部分只是在解析过程中一些变量的初始化,主要是就是parse_state结构体
的初始化。真正解析rc文件的是在第二部分;在当前rc文件解析完成后,会处理当前文件import的其它rc文件,这些是在
第三部分做的。如果有import其它的rc文件,在第三部分中会调用init_parse_config_file函数,接着解析导入的rc文件;
从整体上看,像是一个递归函数。我们把重点放在第二部分上。

在第二部分中next_token函数相当于一个简单的词法分析器,这个函数对应的状态机如下图:

这个图画的不是很标准,我再大概注释下吧:对于rc文件内容逐个字母输入这个状态机,如果是字符的话就继续输入下一个字符,直到输入的是空格或\r或者\t,

那么就进入TEXT状态,并把这个token返回给parse_config函数去处理;同样,要是遇到换行,或者文件结束,都要返回给parse_config去处理。

从第二部分可以看出,rc文件的解析是以行为单位进行的,在每一行中检查到一个TEXT,就会把这个单词放入args数组中;
这样,当一行结束时,这个args数组和nargs变量就初始化完成;然后开始换行;在换行时,需要在如下代码(第二部分代码中)中进行:

  1. case T_NEWLINE:
  2. state.line++;
  3. //nargs非0,说明这行中有命令需要解析
  4. if (nargs) {
  5. //先看看在本行中第一个词是不是关键词,
  6. //关键词列表在keywords.h文件中
  7. int kw = lookup_keyword(args[]);
  8. //如果第一参数是关键词,那么就会返回关键词的index
  9. //然后判断这个关键词是不是SECTION,只有import,on,service
  10. //才是SECTION
  11. if (kw_is(kw, SECTION)) {
  12. state.parse_line(&state, , );
  13. //在parse_new_section中,会解析这个section,
  14. //在解析过程中,最重要的是给parse_line赋值;
  15. //因为parse_line是个函数指针
  16. parse_new_section(&state, kw, nargs, args);
  17. } else {
  18. //在一个SECTION中,parse_line会保持不变的,
  19. //直到遇到下个SECTION之前,这个parse_line函数
  20. //会保持不变的
  21. state.parse_line(&state, nargs, args);
  22. }
  23. nargs = ;
  24. }
  25. break;

给parse_line赋值是在parse_new_section中进行的,能够赋值给parse_line的函数有parse_line_service,parse_line_action,
对这个两个函数的理解,有助于对init.rc文件解析的理解。对于这两个函数没必要逐行分析,这里不作介绍;
在lookup_keyword函数中,有个地方说明下;可能是我的基础知识不是很牢固,在看这个函数的时候,有些地方卡了一下;
在init_parse.c文件中有如下宏定义

  1. #define KEYWORD(symbol, flags, nargs, func, uev_func) \
  2. [ K_##symbol ] = { #symbol, func, uev_func, nargs + , flags, },

而在keywords.h文件中,还有个宏定义如下:

  1. #define KEYWORD(symbol, flags, nargs, func, uev_func) K_##symbol,

这两个宏是一样的。 C语言中宏的作用范围只是在单文件中。宏是在编译过程中进行的替换,这个过程发生在链接之前,所以
宏的作用范围只能是在当前文件中。这样的话,在init_parse.c文件中的宏定义没人用到,因此哪个宏也是无意义的。

2 init进程是如何启动其它基本的进程的
  2.1 init进程中命令和服务启动顺序
    在正式开始介绍init进程是如何启动其它服务之前,还有一些内容要介绍,就是系统如何确定开机执行顺序的。在init.c文件中有如下代码:

  1. action_for_each_trigger("early-init", action_add_queue_tail);
  2. queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done");
  3. queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");
  4. queue_builtin_action(keychord_init_action, "keychord_init");
  5. queue_builtin_action(console_init_action, "console_init");
  6. action_for_each_trigger("init", action_add_queue_tail);
  7. if (!is_special) {
  8. action_for_each_trigger("early-fs", action_add_queue_tail);
  9. action_for_each_trigger("fs", action_add_queue_tail);
  10. action_for_each_trigger("post-fs", action_add_queue_tail);
  11. action_for_each_trigger("post-fs-data", action_add_queue_tail);
  12. }
  13. /* Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
  14. * wasn't ready immediately after wait_for_coldboot_done
  15. */
  16. queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");
  17. queue_builtin_action(property_service_init_action, "property_service_init");
  18. queue_builtin_action(signal_init_action, "signal_init");
  19. queue_builtin_action(check_startup_action, "check_startup");
  20. if (is_special) {
  21. action_for_each_trigger(bootmode, action_add_queue_tail);
  22. } else {
  23. action_for_each_trigger("early-boot", action_add_queue_tail);
  24. action_for_each_trigger("boot", action_add_queue_tail);
  25. }
  26. queue_builtin_action(queue_property_triggers_action, "queue_property_triggers");

  这段代码中主要的函数就两个,分别是action_for_each_trigger,queue_builtin_action,这两个函数操作同意队列,
这个队列就是action_queue。init进程启动顺序就是action在action_queue这个队列中的顺序。action_for_each_trigger
函数就是把*.rc文件中, 对应trigger的action放入action_queue队列。 而queue_builtin_action函数是临时需要在action_queue中
新增加一个action,只不过这个action仅仅有一个command,这个command就是queue_builtin_action函数第一个参数对应的函数。

  因此从这段代码中,我们可以看出,init执行顺序是:
    early-init,    wait_for_coldboot_done,   mix_hwrng_into_linux_rng,  keychord_init,console_init,   init,
    early-fs,   fs,   post-fs,   post-fs-data,   mix_hwrng_into_linux_rng,  property_service_init,signal_init,
    check_startup,  early-boot,   boot,   queue_property_triggers.
  上面这些代码仅仅是把action_queue队列配置完成,安排好每个action的顺序。 但是现在并没有开始按照这个队列去执行各个
命令或者启动各个服务。

  2.2 在init进程中执行命令和服务

  这里就开始介绍init进程是如何执行命令和启动服务的。这个过程是在如下代码开始执行的:

  1. for(;;) {
  2. int nr, i, timeout = -;
  3. //第一部分
  4. //execute_one_command是开始执行命令的地方
  5. execute_one_command();
  6. //如果有些进程需要重启的话在这里进行
  7. restart_processes();
  8. //第二部分
  9. //接下来是四个socket,使用Linux的poll机制去监听
  10. //这四个文件的状态,与其它进程通信;比如:
  11. //当我们通过命令调用 setprop时候,就会和get_property_set_fd
  12. //指向的socket通信
  13. if (!property_set_fd_init && get_property_set_fd() > ) {
  14. ufds[fd_count].fd = get_property_set_fd();
  15. ufds[fd_count].events = POLLIN;
  16. ufds[fd_count].revents = ;
  17. fd_count++;
  18. property_set_fd_init = ;
  19. }
  20. if (!signal_fd_init && get_signal_fd() > ) {
  21. ufds[fd_count].fd = get_signal_fd();
  22. ufds[fd_count].events = POLLIN;
  23. ufds[fd_count].revents = ;
  24. fd_count++;
  25. signal_fd_init = ;
  26. }
  27. if (!keychord_fd_init && get_keychord_fd() > ) {
  28. ufds[fd_count].fd = get_keychord_fd();
  29. ufds[fd_count].events = POLLIN;
  30. ufds[fd_count].revents = ;
  31. fd_count++;
  32. keychord_fd_init = ;
  33. }
  34.  
  35. if (process_needs_restart) {
  36. timeout = (process_needs_restart - gettime()) * ;
  37. if (timeout < )
  38. timeout = ;
  39. }
  40.  
  41. if (!action_queue_empty() || cur_action)
  42. timeout = ;
  43. //这里就是poll机制的利用,
  44. //接收到socket通信后,对于这些事件的分配
  45. nr = poll(ufds, fd_count, timeout);
  46. if (nr <= )
  47. continue;
  48.  
  49. for (i = ; i < fd_count; i++) {
  50. if (ufds[i].revents == POLLIN) {
  51. if (ufds[i].fd == get_property_set_fd())
  52. handle_property_set_fd();
  53. else if (ufds[i].fd == get_keychord_fd())
  54. handle_keychord();
  55. else if (ufds[i].fd == get_signal_fd())
  56. handle_signal();
  57. }
  58. }
  59. }

便于理解,把上述代码分为两个部分。这两个部分都是重点。
不过第一部分更切合我们这一小节的主题:命令的执行和服务的启动。
execute_one_command函数是命令执行和服务启动主要函数,这个函数的代码如下:

  1. void execute_one_command(void)
  2. {
  3. int ret;
  4.  
  5. if (!cur_action || !cur_command || is_last_command(cur_action, cur_command)) {
  6. //从action_queue中取出第一个action
  7. cur_action = action_remove_queue_head();
  8. cur_command = NULL;
  9. if (!cur_action)
  10. return;
  11. INFO("processing action %p (%s)\n", cur_action, cur_action->name);
  12. //从当前action中取出第一个command
  13. cur_command = get_first_command(cur_action);
  14. } else {
  15. cur_command = get_next_command(cur_action, cur_command);
  16. }
  17.  
  18. if (!cur_command)
  19. return;
  20. //执行这个command
  21. ret = cur_command->func(cur_command->nargs, cur_command->args);
  22. INFO("command '%s' r=%d\n", cur_command->args[], ret);
  23. }

上面这个过程就是命令执行的过程.这些命令对应的函数,可以从kerwords.h中看到。如果前面的rc文件解析中,仔细分析
了整个流程的话,就很容易回忆起前面的这些内容。对于这个func也不会陌生,这些函数的实现都是在builtins.c文件中实现的。
在rc文件中出现的命令,绝大部分都是很常见的,不多做介绍。不过,下面还是会以其中一个命令说明下整个过程,这个命令就是
class_start--这个命令是专门用来启动service的,在其他Linux系统中也是没有的。
对于这个命令,首先到keywords.h中找到其对应的函数,如下:

  1. KEYWORD(class_start, COMMAND, , do_class_start, )

所以class_start命令对应的函数实际上就是do_class_start。前文已经说过,这些函数都是在builtins.c文件中实现的,那么这个
函数实现如下:

  1. int do_class_start(int nargs, char **args){
  2. /* Starting a class does not start services
  3. * which are explicitly disabled. They must
  4. * be started individually.
  5. * */
  6. service_for_each_class(args[], service_start_if_not_disabled);
  7. return ;
  8. }
  9. service_for_each_class函数的实现实在init_parser.c文件中,如下
  10. void service_for_each_class(const char *classname, void (*func)(struct service *svc)){
  11. struct listnode *node;
  12. struct service *svc;
  13. list_for_each(node, &service_list) {
  14. svc = node_to_item(node, struct service, slist);
  15. if (!strcmp(svc->classname, classname)) {
  16. func(svc);
  17. }
  18. }
  19. }

到这里,基本已经可以看出这个命令和service之间的关系来。通过这两个函数,实际上就是通过service_start_if_not_disabled
函数启动所有className与给出的classname相同的service。 这些classname指的就是在定义每一个service的时候,定义在class
之后的部分,比如下面这个service:

  1. service servicemanager /system/bin/servicemanager
  2. class core
  3. user system
  4. group system
  5. critical
  6. onrestart restart healthd
  7. onrestart restart zygote
  8. onrestart restart media
  9. onrestart restart surfaceflinger
  10. onrestart restart drm

servicemanager服务的classname就是 core. 那么调用class_start命令的地方在哪里呢?
调用class_start命令的地方在init.rc中,当执行boot action的时候,顺序执行就能执行到这个命令,首先启动的core一级别的服务,
然后启动才是main级别的服务

  1. on boot
  2. ...
  3. class_start core
  4. class_start main

既然已经找到了命令开始的地方了,那么我们可以继续看看service到底是如何执行的。这就要看service_start_if_not_disabled函数了,
这个函数的代码如下,在builtins.c文件中:

  1. static void service_start_if_not_disabled(struct service *svc){
  2. if (!(svc->flags & SVC_DISABLED)) {
  3. service_start(svc, NULL);
  4. }
  5. }

这里会检查flags中没有disabled选项的启动。在service_start函数是真正启动一个服务的地方。service_start函数在init.c文件中实现的。
当你在这个函数中看到fork()函数时候,你就明白这个服务启动了。而逐个启动每个服务,这个循环过程实在service_for_each_class中的
list_for_each中的,可以回头再看看。

到这里就是介绍完成来通过rc文件启动一个服务的过程。能读到这里,说明你真的很有耐心啊,给自己一个表扬吧。不过这时候,你也许会有
一个疑问,如果一个service中flags中有disabled项时,这样的服务是怎样启动的呢?

下面就以下面这个含有disabled项服务的启动为例,介绍下这类服务的启动过程,这个服务如下:

  1. service bootanim /system/bin/bootanimation
  2. class main
  3. user graphics
  4. group graphics
  5. disabled
  6. oneshot

这个服务是开机动画,如果使用的是模拟器的话,就是开机闪烁android的那个动画。这个服务中含有disabled, 通过上面的解析,我们知道这个服务
肯定不是通过class_start命令启动的。但是在开机过程中,我们的确看到来开机动画,那么这个服务是在何时由谁启动的呢?

启动开机动画是在SurfaceFlinger初始化完成时,由SurfaceFlinger间接启动这个服务的。在SurfceFlinger初始化完成时,调用来函数startBootAnim(),
这个函数通过设置一个系统属性,然后开机动画就开始来。设置这个属性的代码如下:

  1. void SurfaceFlinger::startBootAnim() {
  2. // 首先是先退出开机动画。如果开机动画在进行中,那么就退出这个开机动画;
  3. // 如果开机动画没有运行,那么这个属性设置上也是没有影响的
  4. property_set("service.bootanim.exit", "");
  5. //然后,开始播放动画。init进程会检查这个属性,然后开始动画播放
  6. property_set("ctl.start", "bootanim");
  7. }

property_set的函数,在实现的时候,实际上是通过写socket和init进程通信。在前面也说到过,init进程在启动后,会监视四个文件的状态,这四个
文件都是socket文件,其中一直就是属性socket文件的描述符get_property_set_fd(). 通过Linux的poll机制,当有有系统属性设置进来的时候,就会
有触发调用下面的函数:
handle_property_set_fd()
这函数是在init.c文件中被调用,它的实现实在property_service.c文件中。这个函数从socket中读取出传递过来的信息。传递过来的信息都是按照键值对
封装好的。如果键值对中的name中前4个字母是"crtl.",那么就会调用handle_control_message()函数。关于函数handle_property_set_fd()的代码在property_service.c
文件中,这里就不再拿出来单独看了.
在调用到handle_control_message函数,这个函数如下:

  1. void handle_control_message(const char *msg, const char *arg)
  2. {
  3. if (!strcmp(msg,"start")) {
  4. msg_start(arg);
  5. } else if (!strcmp(msg,"stop")) {
  6. msg_stop(arg);
  7. } else if (!strcmp(msg,"restart")) {
  8. msg_restart(arg);
  9. } else {
  10. ERROR("unknown control msg '%s'\n", msg);
  11. }
  12. }
  13. static void msg_start(const char *name)
  14. {
  15. ...
  16. svc = service_find_by_name(name);
  17. ...
  18. service_start(svc, args);
  19. ...
  20. }

如果命令中的start的话,这里把msg_start函数彻底简写了,仅仅保留这两个最核心的代码。根据service的名字找到这个service,
然后启动service。当你看到service_start()函数的时候,想必你已经明白来整个过程来。service_start()函数在前面有过简略
地介绍,这里也不多说来。

就是类似与这样,init进程在设备启动后,还在忙碌地参与这系统的运行。

Android Init进程命令的执行和服务的启动的更多相关文章

  1. android init进程分析 init脚本解析和处理

    (懒人近期想起我还有csdn好久没打理了.这个android init躺在我的草稿箱中快5年了.略微改改发出来吧) RC文件格式 rc文件是linux中常见的启动载入阶段运行的文件.rc是run co ...

  2. Android init进程概述

    init进程,其程序位于根文件系统中,在kernle自行启动后,其中的 start_kernel 函数把根文件系统挂载到/目录后,在 rest_init 函数中通过 kernel_thread(ker ...

  3. ANDROID init进程

    init简要 init是Android上启动的第一个用户态进程. 执行序列是: start_kernel() -> rest_init() -> kernel_init() -> i ...

  4. android init.rc命令快速对照表

    注1:另外还讲述了怎样输出log: Debugging notes---------------By default, programs executed by init will drop stdo ...

  5. android init进程分析 ueventd

    转自:http://blog.csdn.net/freshui/article/details/2132299 (懒人最近想起我还有csdn好久没打理了,这个Android init躺在我的草稿箱中快 ...

  6. 通过命令窗口控制mysql服务的启动与停止

    mysql服务的启动: 以管理员的身份运行cmd命令窗口,输入命名 net start mysql 如果不是以管理员的身份运行cmd,会提示如下错误 mysql服务的停止: 以管理员的身份运行cmd命 ...

  7. Android跨进程訪问(AIDL服务)

    我将AndroidAIDL的学习知识总结一下和大家共享 在Android开发中,AIDL主要是用来跨进程訪问. Android系统中的进程之间不能共享内存,因此,须要提供一些机制在不同进程之间进行数据 ...

  8. Android开发--Apache服务器安装,解决Apache服务无法启动的问题

    昨天学习Android XML解析的时候,想在自己的电脑上搭建一个最简单的Web服务器来存放一段XML文本,然后在Android程序中解析,查找了一些资料后,看到Apache服务器比较容易上手,使用范 ...

  9. Android系统开机启动流程及init进程浅析

    Android系统启动概述 Android系统开机流程基于Linux系统,总体可分为三个阶段: Boot Loader引导程序启动Linux内核启动Android系统启动,Launcher/app启动 ...

随机推荐

  1. [项目构建 十三]babasport Nginx负载均衡的详细配置及使用案例详解.

    在这里再次说明下, 这个项目是从网上 找到的一套学习资料, 自己在 空闲时间学习了这些东西. 这里面的code当然会有很多不完善的地方, 但是确实也能学到很多新东西.感谢看过这一些列博文和评论的小伙伴 ...

  2. (转载)php反射类 ReflectionClass

    (转载)http://hi.baidu.com/daihui98/item/a67dfb8213055dd75f0ec165   php反射类 ReflectionClass 什么是php反射类,可以 ...

  3. Java习惯用法总结

    在微博中看到的一个不错的帖子,总结的很详细,拷贝过来,一是为了方便自己查阅,也能和大家一起共享,后面有原文的链接地址: 在Java编程中,有些知识 并不能仅通过语言规范或者标准API文档就能学到的.在 ...

  4. Windows打印管理解决方案

    需求 从需求出发,我们的目的是在电脑上提供一个虚拟打印机,然后让用户选择这个虚拟机打印时产生的中间文件被拦截下来,之后进行进一步处理后在执行真实的打印. Windows打印体系 首先附上查找Windo ...

  5. GitHub for Mac

    GitHub for Mac 安装 1.从 mac.github.com 下载最新版本的 GitHub. 2.当你开启软件时,你可以选择用你的 GitHub 账户登录,或者新建一个账户. 3.在左侧, ...

  6. nginx往后端转发时需要注意的两个问题

    1.nginx后端有做redirect和rewrite时,需要要注意以下问题:          (1)nginx本身使用的是非80和443端口,例如8080,并且与后端的端口不一致,例如后端为808 ...

  7. ssh端口映射,本地转发

    应用场景: # HOSTA<-X->HOSTB 表示A,B两机器相互不可以访问,  HOSTA<-->HOSTB 表示A,B两机器可以相互访问# 1.localhost< ...

  8. iOS_15_通过代码自己定义cell_微博UI

    终于效果图: BeyondTableViewController.h // // BeyondTableViewController.h // 15_代码自己定义cell_weibo // // Cr ...

  9. 跟我一起学PCL打印语言(一)

    引言 本人从事打印机开发和打印驱动开发的相关工作,深感资料特别是中文资料的匮乏和不成系统,对新入门的从事该行业的人来说,门槛很高.在这里一方面是将开发中遇到的相关知识点整理出来,另一方面也能够促进自己 ...

  10. Java 编程的动态性,第 8 部分: 用代码生成取代反射--转载

    既然您已经看到了如何使用 Javassist 和 BCEL 框架来进行 classworking (请参阅 本系列以前的一组文章), 我将展示一个实际的 classworking 应用程序.这个应用程 ...