Android的init过程(二):初始化语言(init.rc)解析

最后更新于:2022-04-01 07:20:48

[Android的init过程(一)](http://blog.csdn.net/nokiaguy/article/details/8800962) 本文使用的软件版本 Android:4.2.2 Linux内核:3.1.10     在上一篇文章中介绍了init的初始化第一阶段,也就是处理各种属性。在本文将会详细分析init最重要的一环:解析init.rc文件。 init.rc文件并不是普通的配置文件,而是由一种被称为“Android初始化语言”(Android Init Language,这里简称为AIL)的脚本写成的文件。在了解init如何解析init.rc文件之前,先了解AIL非常必要,否则机械地分析init.c及其相关文件的源代码毫无意义。      为了学习AIL,读者可以到自己Android手机的根目录寻找init.rc文件,最好下载到本地以便查看,如果有编译好的Android源代码,在out/target/product/generic/root目录也可找到init.rc文件。 AIL由如下4部分组成。 1.  动作(Actions) 2.  命令(Commands) 3. 服务(Services) 4.  选项(Options)      这4部分都是面向行的代码,也就是说用回车换行符作为每一条语句的分隔符。而每一行的代码由多个符号(Tokens)表示。可以使用反斜杠转义符在Token中插入空格。双引号可以将多个由空格分隔的Tokens合成一个Tokens。如果一行写不下,可以在行尾加上反斜杠,来连接下一行。也就是说,可以用反斜杠将多行代码连接成一行代码。      AIL的注释与很多Shell脚本一行,以#开头。      AIL在编写时需要分成多个部分(Section),而每一部分的开头需要指定Actions或Services。也就是说,每一个Actions或Services确定一个Section。而所有的Commands和Options只能属于最近定义的Section。如果Commands和Options在第一个Section之前被定义,它们将被忽略。 Actions和Services的名称必须唯一。如果有两个或多个Action或Service拥有同样的名称,那么init在执行它们时将抛出错误,并忽略这些Action和Service。 下面来看看Actions、Services、Commands和Options分别应如何设置。 Actions的语法格式如下: ~~~ on <trigger> <command> <command> <command> ~~~ 也就是说Actions是以关键字on开头的,然后跟一个触发器,接下来是若干命令。例如,下面就是一个标准的Action ~~~ on boot ifup lo hostname localhost domainname localdomain ~~~ 其中boot是触发器,下面三行是command 那么init.rc到底支持哪些触发器呢?目前init.rc支持如下5类触发器。 1.  boot    这是init执行后第一个被触发Trigger,也就是在 /init.rc被装载之后执行该Trigger  2.  =    当属性被设置成时被触发。例如, on property:vold.decrypt=trigger_reset_main     class_reset main 3.  device-added-     当设备节点被添加时触发 4.  device-removed-    当设备节点被移除时添加 5\. service-exited-    会在一个特定的服务退出时触发 Actions后需要跟若干个命令,这些命令如下: 1.  exec [ ]*   创建和执行一个程序()。在程序完全执行前,init将会阻塞。由于它不是内置命令,应尽量避免使用exec ,它可能会引起init执行超时。     2.  export 在全局环境中将 变量的值设为。(这将会被所有在这命令之后运行的进程所继承) 3.  ifup    启动网络接口 4.  import    指定要解析的其他配置文件。常被用于当前配置文件的扩展 5.  hostname    设置主机名 6.  chdir    改变工作目录 7.  chmod    改变文件的访问权限 8.  chown    更改文件的所有者和组 9.  chroot   改变处理根目录 10.  class_start    启动所有指定服务类下的未运行服务。 11  class_stop   停止指定服务类下的所有已运行的服务。 12.  domainname    设置域名 13.  insmod    加载指定的驱动模块 14.  mkdir [mode][owner] [group]    创建一个目录 ,可以选择性地指定mode、owner以及group。如果没有指定,默认的权限为755,并属于root用户和 root组。 15\. mount [ ]*    试图在目录挂载指定的设备。 可以是mtd@name的形式指定一个mtd块设备。包括 "ro"、"rw"、"re 16.  setkey    保留,暂时未用 17.  setprop    将系统属性的值设为。 18\. setrlimit    设置的rlimit (资源限制) 19.  start    启动指定服务(如果此服务还未运行)。 20.stop    停止指定服务(如果此服务在运行中)。 21\. symlink    创建一个指向的软连接。 22\. sysclktz    设置系统时钟基准(0代表时钟滴答以格林威治平均时(GMT)为准) 23.  trigger   触发一个事件。用于Action排队 24.  wait [ ] 等待一个文件是否存在,当文件存在时立即返回,或到指定的超时时间后返回,如果不指定,默认超时时间是5秒。 25\. write [ ]* 向指定的文件写入一个或多个字符串。   Services (服务)是一个程序,他在初始化时启动,并在退出时重启(可选)。Services (服务)的形式如下: ~~~ service <name> <pathname> [ <argument> ]* <option> <option> ~~~ 例如,下面是一个标准的Service用法 ~~~ service servicemanager /system/bin/servicemanager class core user system group system critical onrestart restart zygote onrestart restart media onrestart restart surfaceflinger onrestart restart drm ~~~ Services的选项是服务的修饰符,可以影响服务如何以及怎样运行。服务支持的选项如下: 1.  critical 表明这是一个非常重要的服务。如果该服务4分钟内退出大于4次,系统将会重启并进入 Recovery (恢复)模式。     2\. disabled  表明这个服务不会同与他同trigger (触发器)下的服务自动启动。该服务必须被明确的按名启动。 3.  setenv 在进程启动时将环境变量设置为。 4.  socket [ [ ] ]    Create a unix domain socketnamed /dev/socket/ and pass    its fd to the launchedprocess.  must be"dgram", "stream" or "seqpacket".    User and group default to0.    创建一个unix域的名为/dev/socket/ 的套接字,并传递它的文件描述符给已启动的进程。 必须是 "dgram","stream" 或"seqpacket"。用户和组默认是0。 5.  user 在启动这个服务前改变该服务的用户名。此时默认为 root。 6.  group [ ]* 在启动这个服务前改变该服务的组名。除了(必需的)第一个组名,附加的组名通常被用于设置进程的补充组(通过setgroups函数),档案默认是root。 7.  oneshot    服务退出时不重启。 8.  class    指定一个服务类。所有同一类的服务可以同时启动和停止。如果不通过class选项指定一个类,则默认为"default"类服务。 9\. onrestart     当服务重启,执行一个命令(下详)。      现在接着分析一下init是如何解析init.rc的。现在打开system/core/init/init.c文件,找到main函数。在上一篇文章中分析了main函数的前一部分(初始化属性、处理内核命令行等),现在找到init_parse_config_file函数,调用代码如下: init_parse_config_file("/init.rc"); 这个方法主要负责初始化和分析init.rc文件。init_parse_config_file函数在init_parser.c文件中实现,代码如下: ~~~ int init_parse_config_file(const char *fn) { char *data; data = read_file(fn, 0); if (!data) return -1; /* 实际分析init.rc文件的代码 */ parse_config(fn, data); DUMP(); return 0; } ~~~       init_parse_config_file方法开始调用了read_file函数打开了/init.rc文件,并返回了文件的内容(char*类型),然后最核心的函数是parse_config。该函数也在init_parser.c文件中实现,代码如下: ~~~ static void parse_config(const char *fn, char *s) { struct parse_state state; struct listnode import_list; struct listnode *node; char *args[INIT_PARSER_MAXARGS]; int nargs; nargs = 0; state.filename = fn; state.line = 0; state.ptr = s; state.nexttoken = 0; state.parse_line = parse_line_no_op; list_init(&import_list); state.priv = &import_list; /* 开始获取每一个token,然后分析这些token,每一个token就是有空格、字表符和回车符分隔的字符串 */ for (;;) { /* next_token函数相当于词法分析器 */ switch (next_token(&state)) { case T_EOF: /* init.rc文件分析完毕 */ state.parse_line(&state, 0, 0); goto parser_done; case T_NEWLINE: /* 分析每一行的命令 */ /* 下面的代码相当于语法分析器 */ state.line++; if (nargs) { int kw = lookup_keyword(args[0]); if (kw_is(kw, SECTION)) { state.parse_line(&state, 0, 0); parse_new_section(&state, kw, nargs, args); } else { state.parse_line(&state, nargs, args); } nargs = 0; } break; case T_TEXT: /* 处理每一个token */ if (nargs < INIT_PARSER_MAXARGS) { args[nargs++] = state.text; } break; } } parser_done: /* 最后处理由import导入的初始化文件 */ list_for_each(node, &import_list) { struct import *import = node_to_item(node, struct import, list); int ret; INFO("importing '%s'", import->filename); /* 递归调用 */ ret = init_parse_config_file(import->filename); if (ret) ERROR("could not import file '%s' from '%s'\n", import->filename, fn); } } ~~~        parse_config方法的代码就比较复杂了,现在先说说该方法的基本处理流程。首先会调用  list_init(&import_list)初始化一个链表,该链表是用于存储通过import语句导入的初始化文件名。然后开始开始在for循环中分析init.rc文件中的每一行代码。最后将init.rc文件分析完后,就会进入parser_done部分,并递归调用init_parse_config_file方法分析通过import导入的初始化文件。       通过分析parse_config方法的原理,感觉也并不是很复杂。不过分析parse_config方法的具体代码,还需要点编译原理的知识(只是概念上的就可以)。在for循环中调用了一个next_token方法不断从init.rc文件中获取token。这里的token,就是一种编程语言的最小单元,也就是不可再分。例如,对于传统的编程语言,if、then等关键字、变量名等标识符都属于一个token。而对于init.rc文件来说,import、on、以及触发器的参数值,都属于一个token。      一个完整的编译器(或解析器)最开始需要进行词法和语法分析,词法分析就是在源代码文件中挑出一个个的Token,也就是说,词法分析器的返回值是Token,而语法分析器的输入就是词法分析器的输出。也就是说,语法分析器需要分析一个个的token,而不是一个个的字符。由于init解析语言很简单,所以就将词法和语法分析器放到了一起。词法分析器就是next_token函数,而语法分析器就是T_NEWLINE分支中的代码。这些就清楚多了。现在先看看next_token函数(在parser.c文件中实现)是如何获取每一个token的。 ~~~ int next_token(struct parse_state *state) { char *x = state->ptr; char *s; if (state->nexttoken) { int t = state->nexttoken; state->nexttoken = 0; return t; } /* 在这里开始一个字符一个字符地分析 */ for (;;) { switch (*x) { case 0: state->ptr = x; return T_EOF; case '\n': x++; state->ptr = x; return T_NEWLINE; case ' ': case '\t': case '\r': x++; continue; case '#': while (*x && (*x != '\n')) x++; if (*x == '\n') { state->ptr = x+1; return T_NEWLINE; } else { state->ptr = x; return T_EOF; } default: goto text; } } textdone: state->ptr = x; *s = 0; return T_TEXT; text: state->text = s = x; textresume: for (;;) { switch (*x) { case 0: goto textdone; case ' ': case '\t': case '\r': x++; goto textdone; case '\n': state->nexttoken = T_NEWLINE; x++; goto textdone; case '"': x++; for (;;) { switch (*x) { case 0: /* unterminated quoted thing */ state->ptr = x; return T_EOF; case '"': x++; goto textresume; default: *s++ = *x++; } } break; case '\\': x++; switch (*x) { case 0: goto textdone; case 'n': *s++ = '\n'; break; case 'r': *s++ = '\r'; break; case 't': *s++ = '\t'; break; case '\\': *s++ = '\\'; break; case '\r': /* \ <cr> <lf> -> line continuation */ if (x[1] != '\n') { x++; continue; } case '\n': /* \ <lf> -> line continuation */ state->line++; x++; /* eat any extra whitespace */ while((*x == ' ') || (*x == '\t')) x++; continue; default: /* unknown escape -- just copy */ *s++ = *x++; } continue; default: *s++ = *x++; } } return T_EOF; } ~~~       next_token函数的代码还是很多的,不过原理到很简单。就是逐一读取init.rc文件(还有import导入的初始化文件)的字符,并将由空格、“/t”和“/r”分隔的字符串挑出来,并通过state->text返回。如果返回了正常的token,next_token函数就返回T_TEXT。如果一行结束,就返回T_NEWLINE,如果init.rc文件的内容已读取完,就返回T_EOF。当返回T_NEWLINE时,开始语法分析(由于init初始化语言是基于行的,所以语言分析实际上就是分析init.rc文件的每一行,只是这些行已经被分解成一个个token了)。感兴趣的读者可以详细分析一下next_token函数的代码,尽管代码很多,但并不复杂。而且还很有意思。       现在回到parse_config函数,先看一下T_TEXT分支。该分支将获得的每一行的token都存储在args数组中。现在来看T_NEWLINE分支。该分支的代码涉及到一个state.parse_line函数指针,该函数指针指向的函数负责具体的分析工作。但我们发现,一看是该函数指针指向了一个空函数parse_line_no_op,实际上,一开始该函数指针什么都不做,只是为了使该函数一开始不至于为null,否则调用出错。      现在来回顾一下T_NEWLINE分支的完整代码。 ~~~ case T_NEWLINE: state.line++; if (nargs) { int kw = lookup_keyword(args[0]); if (kw_is(kw, SECTION)) { state.parse_line(&state, 0, 0); parse_new_section(&state, kw, nargs, args); } else { state.parse_line(&state, nargs, args); } nargs = 0; } break; ~~~     在上面的代码中首先调用了lookup_keyword方法搜索关键字。该方法的作用是判断当前行是否合法,也就是根据Init初始化语言预定义的关键字查询,如果未查到,返回K_UNKNOWN。lookup_keyword方法在init_parser.c文件中实现,代码如下: ~~~ int lookup_keyword(const char *s) { switch (*s++) { case 'c': if (!strcmp(s, "opy")) return K_copy; if (!strcmp(s, "apability")) return K_capability; if (!strcmp(s, "hdir")) return K_chdir; if (!strcmp(s, "hroot")) return K_chroot; if (!strcmp(s, "lass")) return K_class; if (!strcmp(s, "lass_start")) return K_class_start; if (!strcmp(s, "lass_stop")) return K_class_stop; if (!strcmp(s, "lass_reset")) return K_class_reset; if (!strcmp(s, "onsole")) return K_console; if (!strcmp(s, "hown")) return K_chown; if (!strcmp(s, "hmod")) return K_chmod; if (!strcmp(s, "ritical")) return K_critical; break; case 'd': if (!strcmp(s, "isabled")) return K_disabled; if (!strcmp(s, "omainname")) return K_domainname; break; … … case 'o': if (!strcmp(s, "n")) return K_on; if (!strcmp(s, "neshot")) return K_oneshot; if (!strcmp(s, "nrestart")) return K_onrestart; break; case 'r': if (!strcmp(s, "estart")) return K_restart; if (!strcmp(s, "estorecon")) return K_restorecon; if (!strcmp(s, "mdir")) return K_rmdir; if (!strcmp(s, "m")) return K_rm; break; case 's': if (!strcmp(s, "eclabel")) return K_seclabel; if (!strcmp(s, "ervice")) return K_service; if (!strcmp(s, "etcon")) return K_setcon; if (!strcmp(s, "etenforce")) return K_setenforce; if (!strcmp(s, "etenv")) return K_setenv; if (!strcmp(s, "etkey")) return K_setkey; if (!strcmp(s, "etprop")) return K_setprop; if (!strcmp(s, "etrlimit")) return K_setrlimit; if (!strcmp(s, "etsebool")) return K_setsebool; if (!strcmp(s, "ocket")) return K_socket; if (!strcmp(s, "tart")) return K_start; if (!strcmp(s, "top")) return K_stop; if (!strcmp(s, "ymlink")) return K_symlink; if (!strcmp(s, "ysclktz")) return K_sysclktz; break; case 't': if (!strcmp(s, "rigger")) return K_trigger; break; case 'u': if (!strcmp(s, "ser")) return K_user; break; case 'w': if (!strcmp(s, "rite")) return K_write; if (!strcmp(s, "ait")) return K_wait; break; } return K_UNKNOWN; } ~~~      lookup_keyword方法按26个字母顺序(关键字首字母)进行处理。      现在回到parse_config方法的T_NEWLIEN分支,接下来调用了kw_is宏具体判断当前行是否合法,该宏以及SECTION宏的定义如下。根据这些代码。明显是keyword_info数组中的某个元素的flags成员变量的值取最后一位。 ~~~ #define SECTION 0x01 #define kw_is(kw, type) (keyword_info[kw].flags & (type)) ~~~ 现在问题又转到keyword_info数组了。该数组也在init_parser.c文件中定义,代码如下: ~~~ #include "keywords.h" #define KEYWORD(symbol, flags, nargs, func) \ [ K_##symbol ] = { #symbol, func, nargs + 1, flags, }, struct { const char *name; int (*func)(int nargs, char **args); unsigned char nargs; unsigned char flags; } keyword_info[KEYWORD_COUNT] = { [ K_UNKNOWN ] = { "unknown", 0, 0, 0 }, #include "keywords.h" }; ~~~        从表面上看,keyword_info数组是一个struct数组,但本质上,是一个map。为每一个数组元素设置了一个key,例如,数组元素{ "unknown", 0, 0,0 }的key是K_UNKNOWN,而#include “keywords.h”大有玄机。上面的代码中引用了两次keywords.h文件,现在可以看一下keywords.h文件的代码。 ~~~ #ifndef KEYWORD int do_chroot(int nargs, char **args); … … int do_export(int nargs, char **args); int do_hostname(int nargs, char **args); int do_rmdir(int nargs, char **args); int do_loglevel(int nargs, char **args); int do_load_persist_props(int nargs, char **args); int do_wait(int nargs, char **args); #define __MAKE_KEYWORD_ENUM__ /* "K_chdir", ENUM */ #define KEYWORD(symbol, flags, nargs, func) K_##symbol, enum { K_UNKNOWN, #endif KEYWORD(capability, OPTION, 0, 0) KEYWORD(chdir, COMMAND, 1, do_chdir) KEYWORD(chroot, COMMAND, 1, do_chroot) KEYWORD(class, OPTION, 0, 0) KEYWORD(class_start, COMMAND, 1, do_class_start) KEYWORD(class_stop, COMMAND, 1, do_class_stop) KEYWORD(class_reset, COMMAND, 1, do_class_reset) KEYWORD(console, OPTION, 0, 0) … … KEYWORD(critical, OPTION, 0, 0) KEYWORD(load_persist_props, COMMAND, 0, do_load_persist_props) KEYWORD(ioprio, OPTION, 0, 0) #ifdef __MAKE_KEYWORD_ENUM__ KEYWORD_COUNT, }; #undef __MAKE_KEYWORD_ENUM__ #undef KEYWORD #endif ~~~       从keywords.h文件的代码可以看出,如果未定义KEYWORD宏,则在keywords.h文件中定义一个KEYWORD宏,以及一个枚举类型,其中K_##symbol的##表示连接的意思。而这个KEYWORD宏只用了第一个参数(symbol)。例如,KEYWORD(chdir,       COMMAND, 1, do_chdir)就会生成K_chdir。      而在keyword_info结构体数组中再次导入keywords.h文件,这是KEYWORD宏已经在init_parser.c文件中重新定义,所以第一次导入keywords.h文件使用的是如下的宏。 ~~~ #define KEYWORD(symbol, flags, nargs, func) \ [ K_##symbol ] = { #symbol, func, nargs + 1, flags, }, ~~~      这下就明白了,如果不使用keywords.h文件,直接将所有的代码都写到init_parser.c文件中,就会有下面的代码。 ~~~ int do_chroot(int nargs, char **args); … … enum { K_UNKNOWN, K_ capability, K_ chdir, … … } #define KEYWORD(symbol, flags, nargs, func) \ [ K_##symbol ] = { #symbol, func, nargs + 1, flags, }, struct { const char *name; int (*func)(int nargs, char **args); unsigned char nargs; unsigned char flags; } keyword_info[KEYWORD_COUNT] = { [ K_UNKNOWN ] = { "unknown", 0, 0, 0 }, [K_ capability] = {" capability ", 0, 1, OPTION }, [K_ chdir] = {"chdir", do_chdir ,2, COMMAND}, … … #include "keywords.h" }; ~~~       可能我们还记着lookup_keyword方法,该方法的返回值就是keyword_info数组的key。       在keywords.h前面定义的函数指针都是处理init.rc文件中service、action和command的。现在就剩下一个问题了,在哪里为这些函数指针赋值呢,也就是说,具体处理每个部分的函数在哪里呢。现在回到前面的语法分析部分。如果当前行合法,则会执行parse_new_section函数(在init_parser.c文件中实现),该函数将为section和action设置处理这两部分的函数。parse_new_section函数的代码如下: ~~~ void parse_new_section(struct parse_state *state, int kw, int nargs, char **args) { printf("[ %s %s ]\n", args[0], nargs > 1 ? args[1] : ""); switch(kw) { case K_service: // 处理service state->context = parse_service(state, nargs, args); if (state->context) { state->parse_line = parse_line_service; return; } break; case K_on: // 处理action state->context = parse_action(state, nargs, args); if (state->context) { state->parse_line = parse_line_action; return; } break; case K_import: // 单独处理import导入的初始化文件。 parse_import(state, nargs, args); break; } state->parse_line = parse_line_no_op; } ~~~       现在看一下处理service的函数(parse_line_service)。 ~~~ static void parse_line_service(struct parse_state *state, int nargs, char **args) { struct service *svc = state->context; struct command *cmd; int i, kw, kw_nargs; if (nargs == 0) { return; } svc->ioprio_class = IoSchedClass_NONE; kw = lookup_keyword(args[0]); // 下面处理每一个option switch (kw) { case K_capability: break; … … case K_group: if (nargs < 2) { parse_error(state, "group option requires a group id\n"); } else if (nargs > NR_SVC_SUPP_GIDS + 2) { parse_error(state, "group option accepts at most %d supp. groups\n", NR_SVC_SUPP_GIDS); } else { int n; svc->gid = decode_uid(args[1]); for (n = 2; n < nargs; n++) { svc->supp_gids[n-2] = decode_uid(args[n]); } svc->nr_supp_gids = n - 2; } break; case K_keycodes: if (nargs < 2) { parse_error(state, "keycodes option requires atleast one keycode\n"); } else { svc->keycodes = malloc((nargs - 1) * sizeof(svc->keycodes[0])); if (!svc->keycodes) { parse_error(state, "could not allocate keycodes\n"); } else { svc->nkeycodes = nargs - 1; for (i = 1; i < nargs; i++) { svc->keycodes[i - 1] = atoi(args[i]); } } } break; … … } …… } ~~~       Action的处理方式与service类似,读者可以自行查看相应的函数代码。现在一切都清楚了。处理service的函数是parse_line_service,处理action的函数是parse_line_action。而前面的state.parse_line根据当前是service还是action,指向这两个处理函数中的一个,并执行相应的函数处理actioncommand和serviceoption。      综合上述,实际上分析init.rc文件的过程就是通过一系列地处理,最终转换为通过parse_line_service或parse_line_action函数分析Init.rc文件中每一行的行为。
';

Android双机(网络和USB)调试及其完美ROOT

最后更新于:2022-04-01 07:20:46

本文主要讲如下两个内容。 1.  如何让一部Android手机带两台PC进行调试。 2.  如何完美Root Android手机。     我曾经遇到过这样的问题,Mac OS X上测试Android的系统程序,大家都知道,Android源代码的编译通常需要Ubuntu Linux,所以在Mac OS X上安装了Vmware ubuntu。这样本没什么问题。将Android手机与MBP相连,系统会提示连接Mac OS X,还是Ubuntu。不过这有一个问题,就是ubuntu和mac os x同时只能连接一个。由于某些需要,要用到Mac OS X上的Eclipse编写Android应用,也能安装在同一部Android手机上,这就很郁闷,单单通过USB线只能连接一台PC(包括虚拟机)。    从技术上说,adb完全有这个能力使一部Android手机连接到两台PC上,不过可能大多数读者用的ROM是官方,为了安全起见,将该功能关了。如果读者使用了最新的CM ROM(cm10.1,可能老的版本也有该功能,这个还没测试),就会发现在设置的“开发者选项”中多了一个“网络ADB调试”。如图1所示。这可是梦寐以求的功能。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8ac4a708.jpg)                                                           图1       选择“网络ADB调试”选项。然后记住下面的ip和端口号。在一台机器上用USB线连接手机,Eclipse的devices列表就会显示该手机已连接成功。如果仍然用当前PC通过网络连接ADB,需要执行下面的命令(PC与手机在同一网段)。    adb kill-server    adb connect 192.168.17.103:5555    如果一切正常,就已经连上了,打开Eclipse,会在devices列表看到如图2的设备。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8ac5efcd.jpg)                                                           图2       其实这两个设备是一个,只是上边那个通过网络连接的手机,下边那个通过USB线连接的手机。现在运行程序,仍然会看到要求选择运行设备,如图3所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8ac70975.jpg)                                                                                              图3       其实现在选哪个都行,因为这两个设备只是通过不同方式与PC连接。       如果在不同的PC上(或PC和虚拟机),一个通过USB线,一个通过网络。两台机器中的eclipse就可以在同一部手机上调试程序了,是不是很爽呢!      如果要断开网络adb连接,执行adb disconnect命令。     要注意,这一操作比较危险,一旦选择“网络adb调试”,只要同一网段的其他用户获知IP和端口号,就可以任意操作你的手机,而且不会有任何提示。甚至是在地球的另一端(只要你们在同一个网段,包括VPN)。 现在进入第二个主题,如果完美获取Android手机的ROM。由于前面使用的是CMROM(不知道CM ROM是什么的上网自己查),所以这里仍然讨论CM ROM的root过程。      可能刚一接触Android手机的读者会感觉root一部手机很复杂,其实再简单不过了。Root的基本原理就是在Android系统的/system/xbin目录放一个su命令。用过su的linux用户都知道su是什么东西,一个提取root权限的命令。如果读者用的是CM ROM,在/system/xbin目录已经有了su命令。而且在设置里可以打开root权限(cm的低版本直接就打开了root权限),所以使用CM ROM,这一步可以省了。当然,如果使用的不是CM ROM,也好吧。下一个CM ROM压缩包,将里面的杂七杂八的东西都删掉(包括内核镜像boot.img),只保留system/xbin目录中的su文件(该文件也可以上网找一个现成的,或直接编译android源代码中的su也可以,默认是不带这个命令的)和META-INF目录,然后重新打包成update.zip。然后最好刷一个clockworkmod recovery,这个比较好,update.zip不需要签名验证就可以刷。之后就可以在recovery模式下刷update.zip了。这个update.zip并不是完整的ROM,只是一个升级包,目的是将su文件放到/system/xbin目录中(该目录没有root权限是只读的)。刷完后,进入手机的shell(adb shell),然后执行su,发现原来的$变成了#,表明已经是root了,当然,有些目录还是不可写,例如,为了使system及其子目录可写,需要再次执行下面的命令。 mount –o rw,remount /system       现在还有一个问题,就是通过adb shell进入Shell,默认仍然是$,这就有些麻烦,因为对于程序员来说,需要调试系统程序,要用adbremount命令使/system/app目录变成可写的,然后可用adb push命令直接将apk文件上传到该目录。但如果默认不是root,是不能执行该命令的(权限不允许)。所以我们还需要做另外一件事,就是修改Android系统根目录的default.prop的内容,通常可以改成如下内容。 ro.adb.secure=0 ro.secure=0 ro.allow.mock.location=1 ro.debuggable=1 persist.sys.usb.config=mass_storage,adb persist.service.adb.enable=1        最大的问题就是default.prop是内存文件,改了也没用,一重启就会恢复原样,而且只有重启才能生效(好像进入了二难推理)。所以修改default.prop文件的方法是直接修改boot.img文件。该文件由两部分组成:zImage和ramdisk.img。其中zImage就是linux内核的二进制文件。ramdisk.img是内存磁盘镜像。该镜像中就包含了被称为Android第一个运行的程序init。default.prop文件也包含在该镜像中。下面就需要android源代码和linux内核源代码了。强烈建议使用CM提供的源代码,因为CM团队已经为我们进行了完美的适配,所以只需要修改与业务相关的内容即可。现在让我们向CM团队致敬。 Android源代码编译后,在out目录的相关子目录录生成了一个root目录,该目录中的内容就是ramdisk.img文件解压后的内容。现在找到default.prop文件,并安着上面的内容修改该文件。然后使用下面的命令重新生成ramdisk.img文件。 mkbootfs root | minigzip > /ramdisk.img 接下来使用下面的命令重新生成boot.img文件。 mkbootimg --kernel kernel  --ramdisk ramdisk.img -o boot.img 其中kernel是内核二进制文件,与zImage完全一样。只是编译android源代码时将其命名为 ok,现在可以进入bootloader模式,然后重新执行fastboot flash bootboot.img刷内核镜像,然后再重启手机,现在进入shell,ok,默认就是#了。退出shell,执行adb remount。就可以用adb push上传文件到/system的其他目录了,如/system/app。
';

Android的init过程详解(一)

最后更新于:2022-04-01 07:20:43

[Android的init过程(二);初始化语言(init.rc)解析](http://blog.csdn.net/nokiaguy/article/details/9109491) 本文使用的软件版本 Android:4.2.2 Linux内核:3.1.10      本文及后续几篇文章将对Android的初始化(init)过程进行详细地、剥丝抽茧式地分析,并且在其中穿插了大量的知识,希望对读者了解Android的启动过程又所帮助。本章主要介绍了与硬件相关初始化文件名的确定以及属性服务的原理和实现。      Android本质上就是一个基于Linux内核的操作系统。与Ubuntu Linux、Fedora Linux类似。只是Android在应用层专门为移动设备添加了一些特有的支持。既然Android是Linux内核的系统,那么基本的启动过程也应符合Linux的规则。如果研究过其他Linux系统应该了解,一个完整的Linux系统首先会将一个Linux内核装载到内存,也就是编译Linux内核源代码生成的bzImage文件,对于为Android优化的Linux内核源代码会生成zImage文件。该文件就是Linux内核的二进制版本。由于zImage在内核空间运行,而我们平常使用的软件都是在应用空间运行(关于内核空间和应用空间的详细描述,可以参考[《Android深度探索(卷1):HAL与驱动开发》](http://product.dangdang.com/main/product.aspx?product_id=23043311&_ddclickunion=P-263982%7Cad_type=0%7Csys_id=1#dd_refer=http%3A%2F%2Fblog.csdn.net%2Fnokiaguy)一书的内容,在后续的各卷中将会对Android的整体体系进行全方位的剖析)。内核空间和应用空间是不能直接通过内存地址级别访问的,所以就需要建立某种通讯机制。      目前Linux有很多通讯机制可以在用户空间和内核空间之间交互,例如设备驱动文件(位于/dev目录中)、内存文件(/proc、/sys目录等)。了解Linux的同学都应该知道Linux的重要特征之一就是一切都是以文件的形式存在的,例如,一个设备通常与一个或多个设备文件对应。这些与内核空间交互的文件都在用户空间,所以在Linux内核装载完,需要首先建立这些文件所在的目录。而完成这些工作的程序就是本文要介绍的init。Init是一个命令行程序。其主要工作之一就是建立这些与内核空间交互的文件所在的目录。当Linux内核加载完后,要做的第一件事就是调用init程序,也就是说,init是用户空间执行的第一个程序。 在分析init的核心代码之前,还需要初步了解init除了建立一些目录外,还做了如下的工作 1\. 初始化属性 2\. 处理配置文件的命令(主要是init.rc文件),包括处理各种Action。 3\. 性能分析(使用bootchart工具)。 4\. 无限循环执行command(启动其他的进程)。      尽管init完成的工作不算很多,不过代码还是非常复杂的。Init程序并不是由一个源代码文件组成的,而是由一组源代码文件的目标文件链接而成的。这些文件位于如下的目录。 /system/core/init      其中init.c是init的主文件,现在打开该文件,看看其中的内容。由于init是命令行程序,所以分析init.c首先应从main函数开始,现在好到main函数,代码如下: ~~~ int main(int argc, char **argv) { int fd_count = 0; struct pollfd ufds[4]; char *tmpdev; char* debuggable; char tmp[32]; int property_set_fd_init = 0; int signal_fd_init = 0; int keychord_fd_init = 0; bool is_charger = false; if (!strcmp(basename(argv[0]), "ueventd")) return ueventd_main(argc, argv); if (!strcmp(basename(argv[0]), "watchdogd")) return watchdogd_main(argc, argv); /* clear the umask */ umask(0); // 下面的代码开始建立各种用户空间的目录,如/dev、/proc、/sys等 mkdir("/dev", 0755); mkdir("/proc", 0755); mkdir("/sys", 0755); mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"); mkdir("/dev/pts", 0755); mkdir("/dev/socket", 0755); mount("devpts", "/dev/pts", "devpts", 0, NULL); mount("proc", "/proc", "proc", 0, NULL); mount("sysfs", "/sys", "sysfs", 0, NULL); /* 检测/dev/.booting文件是否可读写和创建*/ close(open("/dev/.booting", O_WRONLY | O_CREAT, 0000)); open_devnull_stdio(); klog_init(); // 初始化属性 property_init(); get_hardware_name(hardware, &revision); // 处理内核命令行 process_kernel_cmdline(); … … is_charger = !strcmp(bootmode, "charger"); INFO("property init\n"); if (!is_charger) property_load_boot_defaults(); INFO("reading config file\n"); // 分析/init.rc文件的内容 init_parse_config_file("/init.rc"); … …// 执行初始化文件中的动作 action_for_each_trigger("init", action_add_queue_tail); // 在charger模式下略过mount文件系统的工作 if (!is_charger) { action_for_each_trigger("early-fs", action_add_queue_tail); action_for_each_trigger("fs", action_add_queue_tail); action_for_each_trigger("post-fs", action_add_queue_tail); action_for_each_trigger("post-fs-data", action_add_queue_tail); } queue_builtin_action(property_service_init_action, "property_service_init"); queue_builtin_action(signal_init_action, "signal_init"); queue_builtin_action(check_startup_action, "check_startup"); if (is_charger) { action_for_each_trigger("charger", action_add_queue_tail); } else { action_for_each_trigger("early-boot", action_add_queue_tail); action_for_each_trigger("boot", action_add_queue_tail); } /* run all property triggers based on current state of the properties */ queue_builtin_action(queue_property_triggers_action, "queue_property_triggers"); #if BOOTCHART queue_builtin_action(bootchart_init_action, "bootchart_init"); #endif // 进入无限循环,建立init的子进程(init是所有进程的父进程) for(;;) { int nr, i, timeout = -1; // 执行命令(子进程对应的命令) execute_one_command(); restart_processes(); if (!property_set_fd_init && get_property_set_fd() > 0) { ufds[fd_count].fd = get_property_set_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; property_set_fd_init = 1; } if (!signal_fd_init && get_signal_fd() > 0) { ufds[fd_count].fd = get_signal_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; signal_fd_init = 1; } if (!keychord_fd_init && get_keychord_fd() > 0) { ufds[fd_count].fd = get_keychord_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; keychord_fd_init = 1; } if (process_needs_restart) { timeout = (process_needs_restart - gettime()) * 1000; if (timeout < 0) timeout = 0; } if (!action_queue_empty() || cur_action) timeout = 0; // bootchart是一个性能统计工具,用于搜集硬件和系统的信息,并将其写入磁盘,以便其 // 他程序使用 #if BOOTCHART if (bootchart_count > 0) { if (timeout < 0 || timeout > BOOTCHART_POLLING_MS) timeout = BOOTCHART_POLLING_MS; if (bootchart_step() < 0 || --bootchart_count == 0) { bootchart_finish(); bootchart_count = 0; } } #endif // 等待下一个命令的提交 nr = poll(ufds, fd_count, timeout); if (nr <= 0) continue; for (i = 0; i < fd_count; i++) { if (ufds[i].revents == POLLIN) { if (ufds[i].fd == get_property_set_fd()) handle_property_set_fd(); else if (ufds[i].fd == get_keychord_fd()) handle_keychord(); else if (ufds[i].fd == get_signal_fd()) handle_signal(); } } } return 0; } ~~~ 我们可以看到main函数是非常复杂的,不过我们也不需要每条语句都弄得非常清楚(因为这样弄是非常困难的),通常只需要了解init的主线即可。其实从init的main函数可以看出。Init实际上就分为如下两部分。 1.  初始化(包括建立/dev、/proc等目录、初始化属性、执行init.rc等初始化文件中的action等)。 2.  使用for循环无限循环建立子进程。      第一项工作很好理解。而第二项工作是init中的核心。在Linux系统中init是一切应用空间进程的父进程。所以我们平常在Linux终端执行的命令,并建立进程。实际上都是在这个无限的for循环中完成的。也就是说,在Linux终端执行ps –e 命令后,看到的所有除了init外的其他进程,都是由init负责创建的。而且init也会常驻内容。当然,如果init挂了,Linux系统基本上就崩溃了。     由于init比较复杂,所以本文只分析其中的一部分,在后续文章中将详细分析init的各个核心组成部分。       对于main函数最开始完成的建立目录的工作比较简单,这部分也没什么可以分析的。就是调用了一些普通的API(mkdir)建立一些目录。现在说一些题外话,由于Android的底层源代码(包括init)实际上是属于Linux应用编程领域,所以要想充分理解Android源代码,除了Linux的基本结构要了解外,Linux应用层的API需要熟悉。为了满足这些读者的需要,后续我会写一些关于Linux应用编程的文章。Ok,现在言归正传,接下来分析一个比较重要的部分:配置文件的解析。       这里的配置文件主要指init.rc。读者可以进到Android的shell,会看到根目录有一个init.rc文件。该文件是只读的,即使有了root权限,可以修改该文件也没有。因为我们在根目录看到的文件只是内存文件的镜像。也就是说,android启动后,会将init.rc文件装载到内存。而修改init.rc文件的内容实际上只是修改内存中的init.rc文件的内容。一旦重启android,init.rc文件的内容又会恢复到最初的装载。想彻底修改init.rc文件内容的唯一方式是修改Android的ROM中的内核镜像(boot.img)。其实boot.img名曰内核镜像,不过该文件除了包含完整的Linux内核文件(zImage)外,还包括另外一个镜像文件(ramdisk.img)。ramdisk.img就包含了init.rc文件和init命令。所以只有修改ramdisk.img文件中的init.rc文件,并且重新打包boot.img文件,并刷机,才能彻底修改init.rc文件。如果读者有Android源代码,编译后,就会看到out目录中的相关子目录会生成一个root目录,该目录实际上就是ramdisk.img解压后的内容。会看到有init命令和init.rc文件。在后续的文章中将会讨论具体如何修改init.rc文件,如何刷机。不过这些内容与本文关系不大,所以不做详细的讨论。 现在回到main函数,在创建完目录后,会看到执行了如下3个函数。     property_init();     get_hardware_name(hardware, &revision);     process_kernel_cmdline();      其中property_init主要是为属性分配一些存储空间,该函数并不是核心。不过当我们查看init.rc文件时会发现该文件开始部分用一些import语句导入了其他的配置文件,例如,/init.usb.rc。大多数配置文件都直接使用了确定的文件名,只有如下的代码使用了一个变量(${ro.hardware})执行了配置文件名的一部分。那么这个变量值是从哪获得的呢? import /init.${ro.hardware}.rc      首先要了解init.${ro.hardware}.rc配置文件的内容通常与当前的硬件有关。现在我们先来关注get_hardware_name函数,代码如下: ~~~ void get_hardware_name(char *hardware, unsigned int *revision) { char data[1024]; int fd, n; char *x, *hw, *rev; /* 如果hardware已经有值了,说明hardware通过内核命令行提供,直接返回 */ if (hardware[0]) return; // 打开/proc/cpuinfo文件 fd = open("/proc/cpuinfo", O_RDONLY); if (fd < 0) return; // 读取/proc/cpuinfo文件的内容 n = read(fd, data, 1023); close(fd); if (n < 0) return; data[n] = 0; // 从/proc/cpuinfo文件中获取Hardware字段的值 hw = strstr(data, "\nHardware"); rev = strstr(data, "\nRevision"); // 成功获取Hardware字段的值 if (hw) { x = strstr(hw, ": "); if (x) { x += 2; n = 0; while (*x && *x != '\n') { if (!isspace(*x)) // 将Hardware字段的值都转换为小写,并更新hardware参数的值 // hardware也就是在init.c文件中定义的hardware数组 hardware[n++] = tolower(*x); x++; if (n == 31) break; } hardware[n] = 0; } } if (rev) { x = strstr(rev, ": "); if (x) { *revision = strtoul(x + 2, 0, 16); } } } ~~~       从get_hardware_name方法的代码可以得知,该方法主要用于确定hardware和revision的变量的值。Revision这里先不讨论,只要研究hardware。获取hardware的来源是从Linux内核命令行或/proc/cpuinfo文件中的内容。Linux内核命令行暂且先不讨论(因为很少传递该值),先看看/proc/cpuinfo,该文件是虚拟文件(内存文件),执行cat /proc/cpuinfo命令会看到该文件中的内容,如图1所示。在白框中就是Hardware字段的值。由于该设备是Nexus 7,所以值为grouper。如果程序就到此位置,那么与硬件有关的配置文件名是init.grouper.rc。有Nexus 7的读者会看到在根目录下确实有一个init.grouper.rc文件。说明Nexus 7的原生ROM并没有在其他的地方设置配置文件名,所以配置文件名就是从/proc/cpuinfo文件的Hardware字段中取的值。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8ac130fd.jpg)                                                                                                       图1 现在来看在get_hardware_name函数后面调用的process_kernel_cmdline函数,代码如下: ~~~ static void process_kernel_cmdline(void) { /* don't expose the raw commandline to nonpriv processes */ chmod("/proc/cmdline", 0440); // 导入内核命令行参数 import_kernel_cmdline(0, import_kernel_nv); if (qemu[0]) import_kernel_cmdline(1, import_kernel_nv); // 用属性值设置内核变量 export_kernel_boot_props(); } ~~~       在process_kernel_cmdline函数中除了使用import_kernel_cmdline函数导入内核变量外,主要的功能就是调用export_kernel_boot_props函数通过属性设置内核变量,例如,通过ro.boot.hardware属性设置hardware变量,也就是说可以通过ro.boot.hardware属性值可以修改get_hardware_name函数中从/proc/cpuinfo文件中得到的hardware字段值。下面看一下export_kernel_boot_props函数的代码。 ~~~ static void export_kernel_boot_props(void) { char tmp[PROP_VALUE_MAX]; const char *pval; unsigned i; struct { const char *src_prop; const char *dest_prop; const char *def_val; } prop_map[] = { { "ro.boot.serialno", "ro.serialno", "", }, { "ro.boot.mode", "ro.bootmode", "unknown", }, { "ro.boot.baseband", "ro.baseband", "unknown", }, { "ro.boot.bootloader", "ro.bootloader", "unknown", }, }; // 通过内核的属性设置应用层配置文件的属性 for (i = 0; i < ARRAY_SIZE(prop_map); i++) { pval = property_get(prop_map[i].src_prop); property_set(prop_map[i].dest_prop, pval ?: prop_map[i].def_val); } // 根据ro.boot.console属性的值设置console变量 pval = property_get("ro.boot.console"); if (pval) strlcpy(console, pval, sizeof(console)); /* save a copy for init's usage during boot */ strlcpy(bootmode, property_get("ro.bootmode"), sizeof(bootmode)); /* if this was given on kernel command line, override what we read * before (e.g. from /proc/cpuinfo), if anything */ // 获取ro.boot.hardware属性的值 pval = property_get("ro.boot.hardware"); if (pval) // 这里通过ro.boot.hardware属性再次改变hardware变量的值 strlcpy(hardware, pval, sizeof(hardware)); // 利用hardware变量的值设置设置ro.hardware属性 // 这个属性就是前面提到的设置初始化文件名的属性,实际上是通过hardware变量设置的 property_set("ro.hardware", hardware); snprintf(tmp, PROP_VALUE_MAX, "%d", revision); property_set("ro.revision", tmp); /* TODO: these are obsolete. We should delete them */ if (!strcmp(bootmode,"factory")) property_set("ro.factorytest", "1"); else if (!strcmp(bootmode,"factory2")) property_set("ro.factorytest", "2"); else property_set("ro.factorytest", "0"); } ~~~       从export_kernel_boot_props函数的代码可以看出,该函数实际上就是来回设置一些属性值,并且利用某些属性值修改console、hardware等变量。其中hardware变量(就是一个长度为32的字符数组)在get_hardware_name函数中已经从/proc/cpuinfo文件中获得过一次值了,在export_kernel_boot_props函数中又通过ro.boot.hardware属性设置了一次值,不过在Nexus 7中并没有设置该属性,所以hardware的值仍为grouper。最后用hardware变量设置ro.hardware属性,所以最后的初始化文件名为init.grouper.rc。       这里还有一个问题,前面多次提到属性或属性文件,那么这些属性文件指的是什么呢?是init.rc?当然不是。实际上这些属性文件是一些列位于不同目录,系统依次读取的配置文件。 **属性服务(Property Service)** 在研究这些配置文件之前应先了解init是如何处理这些属性的。编写过Windows本地应用的读者都应了解,在windows中有一个注册表机制,在注册表中提供了大量的属性。在Linux中也有类似的机制,这就是属性服务。init在启动的过程中会启动属性服务(Socket服务),并且在内存中建立一块存储区域,用来存储这些属性。当读取这些属性时,直接从这一内存区域读取,如果修改属性值,需要通过Socket连接属性服务完成。在init.c文件中的一个action函数中调用了start_property_service函数来启动属性服务,action是init.rc及其类似文件中的一种执行机制,由于内容比较多,所以关于init.rc文件中的执行机制将在下一篇文章中详细讨论。      现在顺藤摸瓜,找到start_property_service函数,该函数在Property_service.c文件中,该文件与init.c文件中同一个目录。 ~~~ void start_property_service(void) { int fd; // 装载不同的属性文件 load_properties_from_file(PROP_PATH_SYSTEM_BUILD); load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT); load_override_properties(); /* Read persistent properties after all default values have been loaded. */ load_persistent_properties(); // 创建socket服务(属性服务) fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0); if(fd < 0) return; fcntl(fd, F_SETFD, FD_CLOEXEC); fcntl(fd, F_SETFL, O_NONBLOCK); // 开始服务监听 listen(fd, 8); property_set_fd = fd; } ~~~       现在我们已经知道属性服务的启动方式了,那么在start_property_service函数中还涉及到如下两个宏。 PROP_PATH_SYSTEM_BUILD PROP_PATH_SYSTEM_DEFAULT       这两个宏都是系统预定义的属性文件名的路径。为了获取这些宏的定义,我们先进行另外一个函数的分析。       在前面读取属性值时使用过一个property_get函数,该函数在Property_service.c中实现,代码如下: ~~~ const char* property_get(const char *name) { prop_info *pi; if(strlen(name) >= PROP_NAME_MAX) return 0; pi = (prop_info*) __system_property_find(name); if(pi != 0) { return pi->value; } else { return 0; } } ~~~       可以看到,在property_get函数中调用了一个核心函数__system_property_find,该函数真正实现了获取属性值的功能。该函数属于bionic的一个library,在system_properties.c文件中实现,读者可以在如下的目录找到该文件。 /bionic/libc/bionic __system_property_find函数的代码如下: ~~~ const prop_info *__system_property_find(const char *name) { // 获取属性存储内存区域的首地址 prop_area *pa = __system_property_area__; unsigned count = pa->count; unsigned *toc = pa->toc; unsigned len = strlen(name); prop_info *pi; while(count--) { unsigned entry = *toc++; if(TOC_NAME_LEN(entry) != len) continue; pi = TOC_TO_INFO(pa, entry); if(memcmp(name, pi->name, len)) continue; return pi; } return 0; } ~~~       从__system_property_find函数的代码很容易看出,第一行使用了一个__system_property_area__变量,该变量是全局的。在前面分析main函数时涉及到一个property_init函数,该函数调用了init_property_area函数,该函数用于初始化属性内存区域,也就是__system_property_area__变量。 ~~~ static int init_property_area(void) { prop_area *pa; if(pa_info_array) return -1; if(init_workspace(&pa_workspace, PA_SIZE)) return -1; fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC); pa_info_array = (void*) (((char*) pa_workspace.data) + PA_INFO_START); pa = pa_workspace.data; memset(pa, 0, PA_SIZE); pa->magic = PROP_AREA_MAGIC; pa->version = PROP_AREA_VERSION; /* 初始化属性内存区域,属性服务会使用该区域 */ __system_property_area__ = pa; property_area_inited = 1; return 0; } ~~~   在前面涉及到的system_properties.c文件对应的头文件system_properties.h中定义了前面提到的两个表示属性文件路径的宏,其实还有另外两个表示路径的宏,一共4个属性文件。system_properties.h文件可以在/bionic/libc/include/sys目录中找到。这4个宏定义如下: ~~~ #define PROP_PATH_RAMDISK_DEFAULT "/default.prop" #define PROP_PATH_SYSTEM_BUILD "/system/build.prop" #define PROP_PATH_SYSTEM_DEFAULT "/system/default.prop" #define PROP_PATH_LOCAL_OVERRIDE "/data/local.prop" ~~~       现在读者可以进入Android设备的相应目录,通常可以找到上述4个文件,如一般会在根目录,会发现一个default.prop文件,cat default.prop会看到该文件的内容。而属性服务就是装载所有这4个属性文件中的所有属性以及使用property_set设置的属性。在Android设备的终端可以直接使用getprop命令从属性服务获取所有的属性值。如图2所示。getprop命令还可以直接根属性名还获取具体的属性值,例如,getprop ro.build.product。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8ac29b12.jpg)                                                                                         图2         如果读者感兴趣,可以看一下getprop是如何通过属性服务读写属性的。getprop命令的源代码文件是getprop.c。读者可以在/system/core/toolbox目录中找到该文件。实际上,getprop获取属性值也是通过property_get函数完成的。在前面分析过该函数,实际上调用了__system_property_find函数从__system_property_area__变量指定的内存区域获取相应的属性值。       此外在system_properties.c文件中还有如下两个函数用于通过属性服务修改或添加某个属性的值。 ~~~ static int send_prop_msg(prop_msg *msg) { struct pollfd pollfds[1]; struct sockaddr_un addr; socklen_t alen; size_t namelen; int s; int r; int result = -1; // 创建用于连接属性服务的socket s = socket(AF_LOCAL, SOCK_STREAM, 0); if(s < 0) { return result; } memset(&addr, 0, sizeof(addr)); // property_service_socket是Socket设备文件名称 namelen = strlen(property_service_socket); strlcpy(addr.sun_path, property_service_socket, sizeof addr.sun_path); addr.sun_family = AF_LOCAL; alen = namelen + offsetof(struct sockaddr_un, sun_path) + 1; if(TEMP_FAILURE_RETRY(connect(s, (struct sockaddr *) &addr, alen)) < 0) { close(s); return result; } r = TEMP_FAILURE_RETRY(send(s, msg, sizeof(prop_msg), 0)); if(r == sizeof(prop_msg)) { pollfds[0].fd = s; pollfds[0].events = 0; r = TEMP_FAILURE_RETRY(poll(pollfds, 1, 250 /* ms */)); if (r == 1 && (pollfds[0].revents & POLLHUP) != 0) { result = 0; } else { result = 0; } } close(s); return result; } // 用户可以直接调用该函数设置属性值 int __system_property_set(const char *key, const char *value) { int err; int tries = 0; int update_seen = 0; prop_msg msg; if(key == 0) return -1; if(value == 0) value = ""; if(strlen(key) >= PROP_NAME_MAX) return -1; if(strlen(value) >= PROP_VALUE_MAX) return -1; memset(&msg, 0, sizeof msg); msg.cmd = PROP_MSG_SETPROP; strlcpy(msg.name, key, sizeof msg.name); strlcpy(msg.value, value, sizeof msg.value); // 设置属性值 err = send_prop_msg(&msg); if(err < 0) { return err; } return 0; } ~~~ 在send_prop_msg函数中涉及到一个property_service_socket变量,定义如下: ~~~ static const char property_service_socket[] = "/dev/socket/" PROP_SERVICE_NAME; ~~~   实际上,send_prop_msg通过这个设备文件与属性服务通讯的。读者可以在Android设备的终端进入/dev/socket目录,通常会看到一个property_service文件,该文件就是属性服务映射的设备文件。      现在已经分析完了init如何确定与硬件相关的初始化文件名(init.grouper.rc),并且讨论了4个属性文件及其装载过程,以及属性服务实现的基本原理。在下一篇文章中将讨论更深入的内容,例如,init.rc文件中提供了很多action,那么什么是aciton呢,init有是如何解析init.rc文件呢?这些内容都将在下一篇文章中揭晓。
';

使用Android NDK和Java测试Linux驱动

最后更新于:2022-04-01 07:20:41

### [在Android模拟器和Ubuntu上测试Linux驱动](http://blog.csdn.net/nokiaguy/article/details/8635795) **三、使用AndroidNDK测试Linux驱动** 在Android系统中Linux驱动主要的使用者是APK程序。因此,Linux驱动做完后必须要用APK程序进行测试才能说明Linux驱动可以正常使用。由于上一节在Android虚拟机上使用C语言编写的可执行程序测试了Linux驱动,因此很容易想到可以利用Android NDK来测试Linux驱动, 由于Android NDK也使用C/C++来编写程序,因此可以利用上一节的C语言代码,当然,还得加上一些AndroidNDK特有的代码。在使用AndroidNDK测试Linux驱动之前需要做如下两件事。 1\. 由于Linux驱动模块不会随Android系统启动而装载,因此必须执行build.sh脚本文件安装word_count驱动。 2\. 不能使用默认方式启动Android模拟器,而要使用我们自己编译的Linux内核启动Android模拟器,启动模拟器的命令如下:  # emulator-avd myavd -kernel /root/kernel/goldfish/arch/arm/boot/zImage 为了方便,读者也可以在随书光盘带的Ubuntu Linux虚拟环境中直接执行如下的命令来异步启动Android模拟器。其中emulator.sh文件在/root/drivers目录中。  # sh emulator.sh & 本节的例子已经包含在随书光盘和虚拟环境中,路径如下: **随书光盘:**/sources/ch06/word_count/word_count_ndk **虚拟环境:**/root/drivers/ch06/word_count/word_count_ndk word_count_ndk工程的代码部分由WordCountNDKTestMain.java和ndk_test_word_count.c文件组成。工程结构如图6-17所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8abad5cb.jpg)        ndk_test_word_count.c文件用于访问word_count驱动。该文件包含两个供Java访问的函数,分别用来读取/dev/wordcount设备文件中的单词数和向/dev/wordcount设备文件写入字符串。下面先看看ndk_test_word_count.c文件的完整代码。 ~~~ #include <string.h> #include <jni.h> #include <fcntl.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdlib.h> // JNI函数:readWordCountFromDev // 用于从/dev/wordcount设备文件读取单词数 jint Java_mobile_android_word_count_ndk_WordCountNDKTestMain_readWordCountFromDev( JNIEnv* env, jobject thiz) { int dev; // open函数打开/dev/wordcount设备文件后返回的句柄,打开失败返回-1 jint wordcount = 0; // 单词数 unsigned char buf[4]; // 以4个字节形式存储的单词数 // 以只读方式打开/dev/wordcount设备文件 dev = open("/dev/wordcount", O_RDONLY); // 从dev/wordcount设备文件中读取单词数 read(dev, buf, 4); int n = 0; // 存储单词数的int类型变量 // 将由4个字节表示的单词数转换成int类型的值 n = ((int) buf[0]) << 24 | ((int) buf[1]) << 16 | ((int) buf[2]) << 8 | ((int) buf[3]); // 将int类型的单词数转换成jint类型的单词数 wordcount = (jint) n; // 关闭/dev/wordcount设备文件 close(dev); // 返回单词数 return wordcount; } // 将jstring类型的值转换成char *类型的值 char* jstring_to_pchar(JNIEnv* env, jstring str) { char* pstr = NULL; // 下面的代码会调用Java中的String.getBytes方法获取字符串的字节数 // 获取java.lang.String类 jclass clsstring = (*env)->FindClass(env, "java/lang/String"); // 将字符串“utf-8”转换成jstring类型的值 jstring strencode = (*env)->NewStringUTF(env, "utf-8"); // 获取java.lang.String.getBytes方法 jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B"); // 调用String.getBytes方法将str变量的值转换成jbytearray类型的值 jbyteArray byteArray = (jbyteArray)( (*env)->CallObjectMethod(env, str, mid, strencode)); // 获取字节长度 jsize size = (*env)->GetArrayLength(env, byteArray); // 将jbytearray类型的值转换成jbyte*类型的值 jbyte* pbyte = (*env)->GetByteArrayElements(env, byteArray, JNI_FALSE); if (size > 0) { // 为char*类型变量pstr分配空间 pstr = (char*) malloc(size); // 将pbyte变量中的值复制到pstr变量中 memcpy(pstr, pbyte, size); } // 返回转换后的值 return pstr; } // JNI函数:writeStringToDev // 用于向/dev/wordcount设备文件写入字符串 void Java_mobile_android_word_count_ndk_WordCountNDKTestMain_writeStringToDev( JNIEnv* env, jobject thiz, jstring str) { int dev; // open函数打开/dev/wordcount设备文件后返回的句柄,打开失败返回-1 // 以只写方式打开/dev/wordcount设备文件 dev = open("/dev/wordcount", O_WRONLY); // 将jstring类型字符串转换成char* 类型的值 char* pstr = jstring_to_pchar(env, str); if (pstr != NULL) { // 向/dev/wordcount设备文件写入字符串 write(dev,pstr, strlen(pstr)); } // 关闭/dev/wordcount设备文件 close(dev); } ~~~      编写上面的代码有一个重点就是jstring_to_pchar函数。该函数可以将jstring类型的数据转换成char*类型的数据。转换的基本思想就是调用Java方法String.getBytes,获取字符串对应的字节数组(jbyteArray)。由于write函数需要的是char *类型的数据,因此,还必须将jbyteArray类型的数据转换成char *类型的数据。采用的方法是先将jbyteArray类型的数据转换成jbyte类型的数据,然后调用memcpy函数将jbyte类型的数据复制到使用malloc函数分配的char *指针空间中。在jstring_to_pchar函数中有如下的一行代码。 jmethodID mid = (*env)->GetMethodID(env,clsstring, "getBytes", "(Ljava/lang/String;)[B"];      看到getMethodID方法最后一个参数的值是"(Ljava/lang/String;)[B",可能Android NDK初学者会对此感到困惑,以为是写错了。实际上这是JNI(Android NDK程序实际上就是遵循JNI规则的程序)对方法参数和返回类型的描述。在JNI程序中为了方便描述Java数据类型,将简单类型使用了一个大写英文字母表示,如表6-1所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8abbf5c2.jpg)     除了表6-1所示的Java简单类型外,还有一些数据类型需要在JNI代码中与其对应。表6-2是这些数据类型在JNI中的描述符。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8abd0346.jpg)      从表6-2所示的数据类型对照关系很容易想到本例中的"(Ljava/lang/String;)[B"是什么意思。jstring_to_pchar函数调用的是如下的getBytes方法的重载形式。 public byte[] getBytes(String charsetName) throwsUnsupportedEncodingException 在JNI中调用Java方法需要指定方法参数和返回值的数据类型。在JNI中的格式如下: "(参数类型)返回值类型"      getBytes方法的参数类型是String,根据表6-2的描述,String类型中JNI在的描述符是"Ljava/lang/String; "。getBytes方法的返回值类型是byte[]。这里就涉及到一个数组的表示法。在JNI中数组使用左中括号([]表示,后面是数组中元素的类型。每一维需要使用一个“[”。byte[]是一维字节数组,所以使用"[B"表示。如果是byte[][][],应使用"[[[B"表示。如果Java方法未返回任何值(返回值类型是void),则用V表示。如void mymethod(int value)的参数和返回值类型可表示为"(I)V"。 Android NDK程序还需要一个Android.mk文件,代码如下: ~~~ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := ndk_test_word_count LOCAL_SRC_FILES := ndk_test_word_count.c include $(BUILD_SHARED_LIBRARY) ~~~ 在编写Java代码调用JNI函数之前,先看一下本例的界面,如图6-18所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8abdf50e.jpg)        读者需要先在PC上运行build.sh脚本文件安装word_count驱动。然后单击“从/dev/wordcount读取单词数”按钮,会在按钮下方输出当前/dev/wordcount设备文件中统计出的单词数。读者也可以在输入框中输入一个由空格分隔的字符串,然后单击“向/dev/wordcount写入字符串”按钮,再单击“从/dev/wordcount读取单词数”按钮,就会统计出字符串中包含的单词数,效果如图6-19所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8abf182e.jpg) 下面看一下本例中Java部分(WordCountNDKTestMain.java)的完整代码。 ~~~ package mobile.android.word.count.ndk; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; public class WordCountNDKTestMain extends Activity { private TextView tvWordCount; private EditText etString; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); tvWordCount = (TextView) findViewById(R.id.textview_wordcount); etString = (EditText) findViewById(R.id.edittext_string); } // “从/dev/wordcount读取单词数”按钮的执行代码 public void onClick_ReadWordCountFromDev(View view) { // 显示单词数 tvWordCount.setText("单词数:" + String.valueOf(readWordCountFromDev())); } // “向/dev/wordcount写入字符串”按钮的执行代码 public void onClick_WriteStringToDev(View view) { // 向/dev/wordcount设备文件写入字符串 writeStringToDev(etString.getText().toString()); Toast.makeText(this, "已向/dev/wordcount写入字符串", Toast.LENGTH_LONG).show(); } // native方法 public native int readWordCountFromDev(); public native void writeStringToDev(String str); static { System.loadLibrary("ndk_test_word_count"); } } ~~~ WordCountNDKTestMain.java中的代码只是简单地调用了JNI函数来操作/dev/wordcount文件。其他的代码都是常规的Android应用级别的代码。如果读者对这部分不熟悉,可以参阅笔者所著的《Android开发权威指南》。 **四、使用Java代码直接操作设备文件来测试Linux驱动**       如果Android拥有root权限,完全可以直接使用Java代码操作/dev/wordcount设备文件(没有root权限,Linux驱动模块是无法安装的)。本节将介绍如何使用Java代码来测试Linux驱动(测试程序不使用一行C/C++代码)。本节示例的路径如下: **随书光盘:**/sources/ch06/word_count/word_count_java **虚拟环境:**/root/drivers/ch06/word_count/word_count_java word_count_java工程中只有一个源代码文件WordCountJavaTestMain.java。该文件的内容如下: ~~~ package mobile.android.word.count.java; import java.io.FileInputStream; import java.io.FileOutputStream; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; public class WordCountJavaTestMain extends Activity { private TextView tvWordCount; private EditText etString; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); tvWordCount = (TextView) findViewById(R.id.textview_wordcount); etString = (EditText) findViewById(R.id.edittext_string); } // “从/dev/wordcount读取单词数”按钮的执行代码 public void onClick_ReadWordCountFromDev(View view) { // 显示单词数 tvWordCount.setText("单词数:" + String.valueOf(readWordCountFromDev())); } // “向/dev/wordcount写入字符串”按钮的执行代码 public void onClick_WriteStringToDev(View view) { // 向/dev/wordcount设备文件写入字符串 writeStringToDev(etString.getText().toString()); Toast.makeText(this, "已向/dev/wordcount写入字符串", Toast.LENGTH_LONG).show(); } // 下面是用Java实现的操作/dev/wordcount设备文件的代码 // 读取/dev/wordcount设备文件中的单词数 private int readWordCountFromDev() { int n = 0; byte[] buffer = new byte[4]; try { // 打开/dev/wordcount设备文件 FileInputStream fis = new FileInputStream("/dev/wordcount"); // 从设备文件中读取4个字节 fis.read(buffer); // 将4个字节转换成int类型的值 n = ((int) buffer[0]) << 24 | ((int) buffer[1]) << 16 | ((int) buffer[2]) << 8 | ((int) buffer[3]); fis.close(); } catch (Exception e) { } return n; } // 向/dev/wordcount设备文件中写入字符串 private void writeStringToDev(String str) { try { // 打开/dev/wordcount设备文件 FileOutputStream fos = new FileOutputStream("/dev/wordcount"); // 写入字符串 fos.write(str.getBytes("iso-8859-1")); fos.close(); } catch (Exception e) { } } } ~~~ 本例的运行效果和使用方法与上一节的例子类似。读者可以运行随书光盘或虚拟环境中的例子与上一节的例子进行比较。 ### [在Android模拟器和Ubuntu上测试Linux驱动](http://blog.csdn.net/nokiaguy/article/details/8635795) 本文节选至[《Android深度探索(卷1):HAL与驱动开发》](http://product.dangdang.com/main/product.aspx?product_id=23043311),接下来几篇文章将详细阐述如何开发ARM架构的Linux驱动,并分别利用android程序、NDK、可执行文件测试Linux驱动。可在ubuntu Linux、Android模拟器和S3C6410开发板(可以选购OK6410-A开发板,需要刷Android)
';

在Android模拟器和Ubuntu上测试Linux驱动

最后更新于:2022-04-01 07:20:39

[ 开发可统计单词个数的Android驱动程序](http://blog.csdn.net/nokiaguy/article/details/8523272)      在上一节已经实现了一个简单的Linux驱动程序,该驱动的功能是统计给定字符串中的单词数。并且在最后已经将该Linux驱动的源代码成功编译成动态Linux驱动模块word_count.ko。下一步就是测试该模块。测试的方法很多,最常用的就是直接在UbuntuLinux中测试。当然,这对于本章实现的Linux驱动是没问题的,但是对于需要直接访问硬件的驱动在Ubuntu Linux上测试就不太方便。在这种情况下就需要在相应的硬件上进行测试。      对于一个Linux驱动程序,一开始可以在UbuntuLinux上做前期开发和测试。对于访问硬件的部分也可以在Ubuntu Linux用软件进行模拟。当基本开发完成后,就需要在开发板或工程样机上使用真实的硬件进行测试。当然,最后还需要在最终销售的手机上进行测试。最终测试通过,Linux驱动才能算真正开发完成。在开发Linux驱动的过程中一个重要的步骤就是测试。本节将结合实际的开发流程介绍在不同平台上测试Linux驱动程序。这些测试平台包括UbuntuLinux、Android模拟器和S3C6410开发板。 **一、使用Ubuntu Linux测试Linux驱动** 本节将介绍如何在Ubuntu Linux下测试驱动程序。由于上一节编写的Linux驱动程序通过4个字节从设备文件(/dev/wordcount)返回单词数,所以不能使用cat命令测试驱动程序(cat命令不会将这4个字节还原成int类型的值显示)。但可以使用如下命令从日志中查看单词数。 ~~~ # sh build.sh # echo 'I love you.' > /dev/wordcount # dmesg ~~~ 执行上面的命令后,如果输出如图6-13所示白框中的信息,说明驱动程序成功统计了单词数。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8ab3c3cb.jpg)        虽然使用echo和dmesg命令可以测试Linux驱动程序,但这种方式并不是真正的测试。为了使测试效果更接近真实环境,一般需要编写专门用于测试的程序。本节将为word_count驱动编写一个专门的测试程序(test_word_count.c)。test_word_count.c通过直接操作/dev/wordcount设备文件与word_count驱动进行交互。测试程序的代码如下: ~~~ #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { int testdev; // 打开设备文件(/dev/wordcount)的句柄 unsigned char buf[4]; // 表示单词数的4个字节 // 打开设备文件 testdev = open("/dev/wordcount", O_RDWR); // 如果open函数返回-1,表示打开设备文件失败 if (testdev == -1) { printf("Cann't open file \n"); return 0; } // 如果test_word_count后面跟有命令行参数,程序会将第1个参数值当作待统计的字符串 // 如果没有命令行参数,则只读取设备文件中的值 if (argc > 1) { // 向设备文件写入待统计的字符串 write(testdev, argv[1], strlen(argv[1])); // 输出待统计的字符串 printf("string:%s\n", argv[1]); } // 读取设备文件中的单词数(4个字节) read(testdev, buf, 4); int n = 0; // 单词数 // 将4个字节还原成int类型的值 n = ((int) buf[0]) << 24 | ((int) buf[1]) << 16 | ((int) buf[2]) << 8 | ((int) buf[3]); // 分别输出从设备文件获取的4个字节的值 printf("word byte display:%d,%d,%d,%d\n", buf[0], buf[1], buf[2], buf[3]); // 输出统计出的单词数 printf("word count:%d\n", n); // 关闭设备文件 close(testdev); return 0; } ~~~       test_word_count程序可以跟1个命令行参数(多个命令行参数只会使用第1个命令行参数)。如果命令行参数值含有空格,需要使用单引号(')或双引号(")将参数值括起来。可以使用下面的一组命令测试word_count驱动程序。 ~~~ # gcc test_word_count.c -o test_word_count # test_word_count # test_word_count "I love you." ~~~     执行上面的命令后,如果输出如图6-14所示的信息(假设word_count以前统计过一个含有4个单词的字符串),表示word_count驱动成功测试。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8ab5bed6.jpg) **二、在Android模拟器上通过原生(Native)C程序测试Linux驱动**       虽说我们开发的是Linux驱动,但本书主要介绍的是Android版的Linux内核,因此,Linux驱动只在Ubuntu Linux上测试成功还不能保证在Android设备上一定能正常工作,所以必须在Android设备上进行测试。Android设备有很多种类,如安装了Android的开发板,运行Android系统的手机或平板电脑等。但离我们最近的并不是这些硬件设备,而是Android模拟器。Android模拟器可以模拟绝大多数真实的环境,所以可以利用Android模拟器测试Linux内核。       在Android模拟器上测试Linux驱动首先应该想到的,也是最先应该做的就是将word_count.ko驱动模块安装在模拟器上。可能读者使用过adb shell命令。如果进入Android模拟器的命令提示符为“#”,说明通过命令行方式进入Android模拟器直接就是root权限(命令提示符为“$”,表示非root权限),因此从理论上可以使用insmod命令将word_count.ko驱动模块直接安装在Android模拟器中。现在我们来测试一下,看看是否可以将word_count.ko安装在Android模拟器上。现在执行build.sh脚本,并选择“Android模拟器”,脚本会自动将word_count.ko文件上传到Android模拟器的/data/local目录,并进行安装。如果读者选择的是S3C6410开发板,在安装word_count.ko时就会输出如下的错误信息,表示编译Linux驱动的Linux内核版本与当前Android模拟器的版本不相同,无法安装。所以在编译Linux驱动时,必须选择与当前运行的Linux内核版本相同的Linux内核进行编译,否则就无法安装Linux驱动。 ~~~ insmod:init_module ‘/data/local/word_count.ko’ failed(Function not implemented) ~~~ 注意:建议上传文件到Android模拟器或开发板时,将文件放到/data/local目录,系统很多其他的目录,如/system/bin,都是只读的,除非将word_count.ko文件打包进system.img,否则无法向这些目录写数据,即使有root权限也不行      用于Android模拟器的goldfish内核默认不允许动态装载Linux驱动模块,因此需要在编译Linux内核之前执行如下命令配置Linux内核。 ~~~ # cd ~/kernel/goldfish # make menuconfig ~~~      执行上面的命令后,会出现如图6-15所示的设置界面。按“空格”键将第二项“Enable loadable module support”选中(前面是[*]),然后按“回车”进入子菜单,选中前3项,如图6-16所示。否则Linux驱动模块仍然无法安装和卸载。当退出设置菜单时保持设置。最后按4.2.3节的方法重新编译Linux内核, ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8ab72567.jpg) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8ab90548.jpg)        成功编译内核后,Android模拟器可以使用新生成的zImage内核文件动态装载Linux驱动模块。 现在执行build.sh脚本文件完成对word_count驱动的编译、上传和安装的工作,然后进入Android模拟器的终端,使用echo和dmesg命令可以测试word_count驱动和查看测试结果,方法与上一节相同。 注意:编译可在Android模拟器上运行的Linux驱动模块要使用goldfish内核,使用其他的内核编译word_count.c,安装时会出现如下错误。 ~~~ insmod: error inserting 'word_count.ko': -1Invalid module format ~~~      在Android模拟器上不仅可以使用Linux命令测试驱动,也可以像UbuntuLinux一样使用本地C/C++程序进行测试。可能有的读者要问,Android不是只能运行由Java编写的APK程序吗?顶多是在APK程序中嵌入NDK代码。还能直接运行普通的Linux程序吗?答案是肯定的。不过要满足如下两个条件。 1.  Android模拟器、开发板或手机需要有root权限。 2.  可执行文件需要使用交叉编译器进行编译,以便支持ARM处理器。      现在使用交叉编译器来编译在上一节编写的test_word_count.c文件。为了使编译步骤尽可能简单,我们使用Android.mk设置编译参数,并使用make命令进行编译。首先在/root/drivers/ch06/word_count目录中建立一个Android.mk文件,并输入如下的内容。 ~~~ LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) # 指定要编译的源代码文件 LOCAL_SRC_FILES:= test_word_count.c # 指定模块名,也是编译后生成的可执行文件名 LOCAL_MODULE := test_word_count LOCAL_MODULE_TAGS := optional include $(BUILD_EXECUTABLE) ~~~ Android.mk文件中有如下两个地方需要说明一下。 **LOCAL_MODULE_TAGS** 表示当前工程(Android.mk文件所在的目录)在什么模式下编译。如果设为optional,表示不考虑模式,也就是说在任何模式下都会编译。该变量可以设置的值有user、userdebug、eng、optional。其中eng是默认值。 1.  user:限制用户对Android系统的访问,适合于发布产品。 2.  userdebug:类似于user模式,但拥有root访问权限,并且可以从日志中获取大量的调试信息。 3.  eng:一般在开发的过程中设置该模式。除了拥有userdebug的全部功能外,还会带有大量的调试工具。 LOCAL_MODULE_TAGS的值与TARGET_BUILD_VARIANT变量有关。TARGET_BUILD_VARIANT变量用于设置当前的编译模式,可设置的值包括user、userdebug和eng。如果想改变编译模式,可以在编译Android源代码之前执行如下命令。  # export TARGET_BUILD_VARIANT = user 或使用lunch命令设置编译模式。  # lunch full-eng       其中full表示建立的目标,除了full目标(为所有的平台建立)外,还有专门为x86建立的full-x86。详细的建立目标执行lunch命令后就会列出。在图4-8已经显示了Android4支持的建立目标的编译模式。读者可以到第4章查看该图。 **include $(BUILD_EXECUTABLE)**        BUILD_EXECUTABLE表示建立可执行的文件。可执行文件路径是/ out/target/product/generic/system/bin/test_word_count。如果想编译成动态库(.so)文件,可以使用include $(BUILD_SHARED_LIBRARY)。动态库的路径是/ out/target/product/generic/system/lib/test_word_count.so。如果想编译成静态库(.a)文件,可以使用include $(BUILD_STATIC_LIBRARY)。静态库的路径是/ out/target/product/generic/obj/STATIC_LIBRARIES/test_word_count_intermediates/test_word_count.       为了将test_word_count.c文件编译成可在Android模拟器上运行的可执行程序,可以将word_count目录复制到的某个子目录,也可以在目录中为word_count目录建立一个符号链接。为了方便,我们采用如下命令为word_count目录在/development目录建立一个符号链接(假设Android源代码的目录是/sources/android/android4/development/word_count)。  # ln -s  /root/drivers/ch06/word_count /sources/android/android4/development/word_count 现在进入/sources/android/android4目录,执行下面的命令初始化编译命令。  # source ./build/envsetup.sh 可以使用下面两种方法编译test_word_count.c。 1.     进入/sources/android/android4/development/word_count目录,并执行如下的命令。  # mm  2.     在/sources/android/android4目录下执行如下的命令。  # mmm  development/word_count      成功编译后可以在/out/target/product/generic/system/bin目录中找到test_word_count文件。在随书光盘和模拟环境中已经带了编译好的test_word_count程序(包括Emulator版本和Ubuntu Linux版本),可执行程序一般不需要考虑Linux内核的版本,用交叉编译器编译的支持ARM处理器的程序即可以在Android模拟器上运行,也可以在S3C6410开发板或其他有root权限的手机中运行。 **Emulator版本的路径** **随书光盘:**/sources/ch06/word_count/emulator/test_word_count **模拟环境:**/root/drivers/ch06/word_count/emulator/test_word_count **Ubuntu Linux版本的路径** **随书光盘:**/sources/ch06/word_count/ubuntu/test_word_count **模拟环境:**/root/drivers/ch06/word_count/ubuntu/test_word_count 现在执行下面的命令将test_word_count文件上传到Android模拟器。  # adb push  ./emulator/test_word_count /data/local 然后进入Android模拟器的终端,并执行下面的命令测试word_count驱动(需要先使用chmod命令设置test_word_count的可执行权限)。  # chmod 777 /data/local/test_word_count  # /data/local/test_word_count  # /data/local/test_word_count  'a bb ccc ddd eee' 执行上面的命令后,如果输出的单词个数是5,表示程序测试成功。 ### [使用Android NDK和Java测试Linux驱动](http://blog.csdn.net/nokiaguy/article/details/8671397) 本文节选至[《Android深度探索(卷1):HAL与驱动开发》](http://product.dangdang.com/main/product.aspx?product_id=23043311),接下来几篇文章将详细阐述如何开发ARM架构的Linux驱动,并分别利用android程序、NDK、可执行文件测试Linux驱动。可在ubuntu Linux、Android模拟器和S3C6410开发板(可以选购OK6410-A开发板,需要刷Android)
';

Android驱动程序开发和调试环境配置

最后更新于:2022-04-01 07:20:36

 本文用《Android深度探索(卷1):HAL与驱动开发》的随书源代码为例详细说明如何配置Android驱动开发和测试环境,并且如何使用源代码中的build.sh脚本文件在各种平台(Ubuntu Linux、Android模拟器和S3C6410开发板)上编译、安装和测试Linux驱动。建议读者使用Ubuntu Linux12.04或更高版本实验本文的方法。最好用root账号登录Linux。 **一、安装交叉编译器**     如果只是在Ubuntu Linux上测试Linux驱动就不需要安装交叉编译器了,但要在Android模拟器或S3C6410开发板上进行测试,就必须安装交叉编译器。 首先下载交叉编译器(分卷压缩) [分卷1](http://vdisk.weibo.com/s/rH2PH) [分卷2](http://vdisk.weibo.com/s/rHcNo) [分卷3](http://vdisk.weibo.com/s/rHfpy) 下载后解压,会发现有两个tgz文件,可以将这两个文件放到/root/compilers目录中,在Linux终端进入该目录,执行如下命令安装交叉编译器。 ~~~ # tar zxvf arm-linux-gcc-4.3.2.tgz -C / # tar jxvf arm-none-linux-gnueabi-arm-2008q3-72-for-linux.tar.bz2 -C / ~~~ **二、编译和测试Linux内核**      这里的Linux内核有两个,一个是goldfish,也就是Android模拟器使用的Linux内核、另外一个是S3C6410开发板使用的Linux内核(Linux2.6.36)。读者首先要下载这两个Linux内核。 Android模拟器用的Linux内核源代码(分卷压缩) [goldfish(卷1)](http://vdisk.weibo.com/s/rGgna) [goldfish(卷2)](http://vdisk.weibo.com/s/rGtsz) [goldfish(卷3)](http://vdisk.weibo.com/s/rGEK3) [goldfish(卷4)](http://vdisk.weibo.com/s/rGFbP) 用于S3C6410开发板的Linux内核源代码(分卷压缩) [分卷1](http://vdisk.weibo.com/s/rGP7W) [分卷2](http://vdisk.weibo.com/s/rGUCi)      由于随书代码中的word_count驱动已经在goldfish和linux2.6.36中分别建立了符号链接,以便在编译linux内核时同时也会编译word_count驱动,所以linux内核与源代码目录应与作者机器上的目录相同。也就是两个linux内核目录与源代码目录如下: linux内核目录 /root/kernel/goldfish /root/kernel/linux_kernel_2.6.36 [源代码](http://vdisk.weibo.com/s/rCmcW/1361708452)目录 /root/drivers 注意/root/drivers目录下就直接是每一章的源代码了,例如/root/drivers/ch06、/root/drivers/ch07 现在需要将/usr/local/arm/arm-none-linux-gnueabi/bin路径加到Linux的PATH环境变量中(不会加的上网查,这是Linux的基本功)      最后进入/root/compilers/goldfish目录,执行make命令编译linux内核,如果完全编译,大概20分钟左右。编译完成后,会在/root/kernel/goldfish/arch/arm/boot目录中生成一个zImage文件,代码1.7MB,这就是用于Android模拟器的Linux内核文件。 **三、编译Linux驱动**      现在来编译随书光盘的驱动程序,这里以word_count驱动为例。在Linux终端进入/root/drivers/ch06/word_count目录。先别忙着编译。首先要设置打开/root/drivers/common.sh文件,修改第一行UBUNTU_KERNEL_PATH变量值为自己机器上安装的Ubuntu Linux内核路径,只要执行“ls /usr/src”命令即可查看当前机器可用的linux内核。如可以设置下面的路径。 UBUNTU_KERNEL_PATH=/usr/src/linux-headers-3.2.0-23-generic      剩下的两个(S3C6410_KERNEL_PATH和/root/kernel/goldfish)只要按着前面的路径解压Linux内核源代码,就不用设置了。 在word_count目录中执行“source build.sh”命令,会允许选择在哪个平台上编译驱动,直接按回车会在Ubuntu Linux上编译。如果编译成功,会发现当前目录多一个word_count.ko文件(驱动文件)。      现在来编译S3C6410上运行的word_count驱动。先别忙,在编译之前,需要Android中的adb命令。因为build.sh足够只能,在编译完后,如果有多个Android设备连接到PC,会允许用户选择上传到哪个设备装载,这里需要选择S3C6410开发板,然后会直接上传到开发板上,如图1所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8ab1aa95.png) 图1        可以直接使用adb shell命令进入开发板,也可以使用/root/drivers/shell.sh脚本完成同样的工作,只是后者如果有多个android设备,会允许用选择,而不是输入相应的设备ID。使操作更方便。在/root/drivers目录中提供了很多这样的脚本(shell.sh、push.sh、pull.sh等),这些脚本都会允许用户选择操作的Android设备。 我们通常使用Android SDK中的adb命令,到官方网站下载装载linux版本的Android SDK,然后将根目录> /platform-tools加到PATH环境变量中。        现在再次执行“source build.sh”命令,选择第2项(S3C6410开发板),如果系统没找到开发板,需要将USB线拔下重插一下。然后就可以进入开发板的终端,输入lsmod命令查看驱动的安装情况了。 如果在模拟器上测试,选第3项。具体测试的方法请参见书中相应的章节。 **四、测试Linux驱动**       测试word_count驱动的方法很多,通过命令行测试的方法请参见书中相应的章节,在word_count目录中有一个test_word_count程序,通过执行如下的命令可以测试word_count驱动,编译test_word_count.c程序的方法书中已详细描述。 test_word_count “abc  bb  cc” 上面的命令会输出单词数为3。 如果要编译Android HAL,需要Android源代码。购买S3C6410开发板时商家通常会带一些光盘,里面有用于开发板的Android源代码,如果商家没给光盘,别忘了要哦!
';

开发可统计单词个数的Android驱动程序(3)

最后更新于:2022-04-01 07:20:34

[开发可统计单词个数的Android驱动程序(2)](http://blog.csdn.net/nokiaguy/article/details/8584632) **八、 指定回调函数**       本节讲的内容十分关键。不管Linux驱动程序的功能多么复杂还是多么“酷”,都必须允许用户空间的应用程序与内核空间的驱动程序进行交互才有意义。而最常用的交互方式就是读写设备文件。通过file_operations.read和file_operations.write成员变量可以分别指定读写设备文件要调用的回调函数指针。      在本节将为word_count.c添加两个函数:word_count_read和word_count_write。这两个函数分别处理从设备文件读数据和向设备文件写数据的动作。本节的例子先不考虑word_count要实现的统计单词数的功能,先用word_count_read和word_count_write函数做一个读写设备文件数据的实验,以便让读者了解如何与设备文件交互数据。本节编写的word_count.c文件是一个分支,读者可在word_count/read_write目录找到word_count.c文件。可以用该文件覆盖word_count目录下的同名文件测试本节的例子。      本例的功能是向设备文件/dev/wordcount写入数据后,都可以从/dev/wordcount设备文件中读出这些数据(只能读取一次)。下面先看看本例的完整的代码。 ~~~ #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> #define DEVICE_NAME "wordcount" // 定义设备文件名 static unsigned char mem[10000]; // 保存向设备文件写入的数据 static char read_flag = 'y'; // y:已从设备文件读取数据 n:未从设备文件读取数据 static int written_count = 0; // 向设备文件写入数据的字节数 // 从设备文件读取数据时调用该函数 // file:指向设备文件、buf:保存可读取的数据 count:可读取的字节数 ppos:读取数据的偏移量 static ssize_t word_count_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { // 如果还没有读取设备文件中的数据,可以进行读取 if(read_flag == 'n') { // 将内核空间的数据复制到用户空间,buf中的数据就是从设备文件中读出的数据 copy_to_user(buf, (void*) mem, written_count); // 向日志输出已读取的字节数 printk("read count:%d", (int) written_count); // 设置数据已读状态 read_flag = 'y'; return written_count; } // 已经从设备文件读取数据,不能再次读取数据 else { return 0; } } // 向设备文件写入数据时调用该函数 // file:指向设备文件、buf:保存写入的数据 count:写入数据的字节数 ppos:写入数据的偏移量 static ssize_t word_count_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { // 将用户空间的数据复制到内核空间,mem中的数据就是向设备文件写入的数据 copy_from_user(mem, buf, count); // 设置数据的未读状态 read_flag = 'n'; // 保存写入数据的字节数 written_count = count; // 向日志输出已写入的字节数 printk("written count:%d", (int)count); return count; } // 描述与设备文件触发的事件对应的回调函数指针 // 需要设置read和write成员变量,系统才能调用处理读写设备文件动作的函数 static struct file_operations dev_fops = { .owner = THIS_MODULE, .read = word_count_read, .write = word_count_write }; // 描述设备文件的信息 static struct miscdevice misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops }; // 初始化Linux驱动 static int word_count_init(void) { int ret; // 建立设备文件 ret = misc_register(&misc); // 输出日志信息 printk("word_count_init_success\n"); return ret; } // 卸载Linux驱动 static void word_count_exit(void) { // 删除设备文件 misc_deregister(&misc); // 输出日志信息 printk("word_init_exit_success\n"); } // 注册初始化Linux驱动的函数 module_init( word_count_init); // 注册卸载Linux驱动的函数 module_exit( word_count_exit); MODULE_AUTHOR("lining"); MODULE_DESCRIPTION("statistics of word count."); MODULE_ALIAS("word count module."); MODULE_LICENSE("GPL"); ~~~ 编写上面代码需要了解如下几点。 1\. word_count_read和word_count_write函数的参数基本相同,只有第2个参数buf稍微一点差异。word_count_read函数的buf参数类型是char*,而word_count_write函数的buf参数类型是const char*,这就意味着word_count_write函数中的buf参数值无法修改。word_count_read函数中的buf参数表示从设备文件读出的数据,也就是说,buf中的数据都可能由设备文件读出,至于可以读出多少数据,取决于word_count_read函数的返回值。如果word_count_read函数返回n,则可以从buf读出n个字符。当然,如果n为0,表示无法读出任何的字符。如果n小于0,表示发生了某种错误(n为错误代码)。word_count_write函数中的buf表示由用户空间的应用程序写入的数据。buf参数前有一个“__user”宏,表示buf的内存区域位于用户空间。 2\. 由于内核空间的程序不能直接访问用户空间中的数据,因此,需要在word_count_read和word_count_write函数中分别使用copy_to_user和copy_from_user函数将数据从内核空间复制到用户空间或从用户空间复制到内核空间。 3\. 本例只能从设备文件读一次数据。也就是说,写一次数据,读一次数据后,第二次无法再从设备文件读出任何数据。除非再次写入数据。这个功能是通过read_flag变量控制的。当read_flag变量值为n,表示还没有读过设备文件,在word_count_read函数中会正常读取数据。如果read_flag变量值为y,表示已经读过设备文件中的数据,word_count_read函数会直接返回0。应用程序将无法读取任何数据。 4\. 实际上word_count_read函数的count参数表示的就是从设备文件读取的字节数。但因为使用cat命令测试word_count驱动时。直接读取了32768个字节。因此count参数就没什么用了(值总是32768)。所以要在word_count_write函数中将写入的字节数保存,在word_count_read函数中直接使用写入的字节数。也就是说,写入多少个字节,就读出多少个字节。 5.  所有写入的数据都保存在mem数组中。该数组定义为10000个字符,因此写入的数据字节数不能超过10000,否则将会溢出。       为了方便读者测试本节的例子,笔者编写了几个Shell脚本文件,允许在UbuntuLinux、S3C6410开发板和Android模拟器上测试word_count驱动。其中有一个负责调度的脚本文件build.sh。本书所有的例子都会有一个build.sh脚本文件,执行这个脚本文件就会要求用户选择将源代码编译到那个平台,选择菜单如图6-11所示。用户可以输入1、2或3选择编译平台。如果直接按回车键,默认值会选择第1个编译平台(UbuntuLinux)。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8aae32a8.jpg) build.sh脚本文件的代码如下: ~~~ source /root/drivers/common.sh # select_target是一个函数,用语显示图6-11所示的选择菜单,并接收用户的输入 # 改函数在common.sh文件中定义 select_target if [ $selected_target == 1 ]; then source ./build_ubuntu.sh # 执行编译成Ubuntu Linux平台驱动的脚本文件 elif [ $selected_target == 2 ]; then source ./build_s3c6410.sh # 执行编译成s3c6410平台驱动的脚本文件 elif [ $selected_target == 3 ]; then source ./build_emulator.sh # 执行编译成Android模拟器平台驱动的脚本文件 fi ~~~       在build.sh脚本文件中涉及到了3个脚本文件(build_ubuntu.sh、build_s3c6410.sh和build_emulator.sh),这3个脚本文件的代码类似,只是选择的Linux内核版本不同。对于S3C6410和Android模拟器平台,编译完后Linux驱动,会自动将编译好的Linux驱动文件(*.so文件)上传到相应平台的/data/local目录,并安装Linux驱动。例如,build_s3c6410.sh脚本文件的代码如下: ~~~ source /root/drivers/common.sh # S3C6410_KERNEL_PATH变量是适用S3C6410平台的Linux内核源代码的路径, # 该变量以及其它类似变量都在common.sh脚本文件中定义 make -C $S3C6410_KERNEL_PATH M=${PWD} find_devices # 如果什么都选择,直接退出 if [ "$selected_device" == "" ]; then exit else # 上传驱动程序(word_count.ko) adb -s $selected_device push ${PWD}/word_count.ko /data/local # 判断word_count驱动是否存在 testing=$(adb -s $selected_device shell lsmod | grep "word_count") if [ "$testing" != "" ]; then # 删除已经存在的word_count驱动 adb -s $selected_device shell rmmod word_count fi # 在S3C6410开发板中安装word_count驱动 adb -s $selected_device shell "insmod /data/local/word_count.ko" fi ~~~ 使用上面的脚本文件,需要在read_write目录建立一个Makefile文件,内容如下: obj-m := word_count.o 现在执行build.sh脚本文件,选择要编译的平台,并执行下面的命令向/dev/word_count设备文件写入数据。  # echo ‘hello lining’ > /dev/wordcount 然后执行如下的命令从/dev/word_count设备文件读取数据。  # cat /dev/wordcount 如果输出“hello lining”,说明测试成功。 ** 注意:**如果在S3C6410开发板和Android模拟器上测试word_count驱动,需要执行shell.sh脚本文件或adb shell命令进入相应平台的终端。其中shell.sh脚本在/root/drivers目录中。这两种方式的区别是如果有多个Android设备和PC相连时,shell.sh脚本会出现一个类似图6-11所示的选择菜单,用户可以选择进入哪个Android设备的终端,而adb shell命令必须要加-s命令行参数指定Android设备的ID才可以进入相应Android设备的终端。 **九、实现统计单词数的算法**       本节开始编写word_count驱动的业务逻辑:统计单词数。本节实现的算法将由空格、制表符(ASCII:9)、回车符(ASCII:13)和换行符(ASCII:10)分隔的字符串算做一个单词,该算法同时考虑了有多个分隔符(空格符、制表符、回车符和换行符)的情况。下面是word_count驱动完整的代码。在代码中包含了统计单词数的函数get_word_count。 ~~~ #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> #define DEVICE_NAME "wordcount" // 定义设备文件名 static unsigned char mem[10000]; // 保存向设备文件写入的数据 static int word_count = 0; // 单词数 #define TRUE -1 #define FALSE 0 // 判断指定字符是否为空格(包括空格符、制表符、回车符和换行符) static char is_spacewhite(char c) { if(c == ' ' || c == 9 || c == 13 || c == 10) return TRUE; else return FALSE; } // 统计单词数 static int get_word_count(const char *buf) { int n = 1; int i = 0; char c = ' '; char flag = 0; // 处理多个空格分隔的情况,0:正常情况,1:已遇到一个空格 if(*buf == '\0') return 0; // 第1个字符是空格,从0开始计数 if(is_spacewhite(*buf) == TRUE) n--; // 扫描字符串中的每一个字符 for (; (c = *(buf + i)) != '\0'; i++) { // 只由一个空格分隔单词的情况 if(flag == 1 && is_spacewhite(c) == FALSE) { flag = 0; } // 由多个空格分隔单词的情况,忽略多余的空格 else if(flag == 1 && is_spacewhite(c) == TRUE) { continue; } // 当前字符为空格时单词数加1 if(is_spacewhite(c) == TRUE) { n++; flag = 1; } } // 如果字符串以一个或多个空格结尾,不计数(单词数减1) if(is_spacewhite(*(buf + i - 1)) == TRUE) n--; return n; } // 从设备文件读取数据时调用的函数 static ssize_t word_count_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { unsigned char temp[4]; // 将单词数(int类型)分解成4个字节存储在buf中 temp[0] = word_count >> 24; temp[1] = word_count >> 16; temp[2] = word_count >> 8; temp[3] = word_count; copy_to_user(buf, (void*) temp, 4); printk("read:word count:%d", (int) count); return count; } // 向设备文件写入数据时调用的函数 static ssize_t word_count_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { ssize_t written = count; copy_from_user(mem, buf, count); mem[count] = '\0'; // 统计单词数 word_count = get_word_count(mem); printk("write:word count:%d", (int)word_count); return written; } // 描述与设备文件触发的事件对应的回调函数指针 static struct file_operations dev_fops = { .owner = THIS_MODULE, .read = word_count_read, .write = word_count_write }; // 描述设备文件的信息 static struct miscdevice misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops }; // 初始化Linux驱动 static int word_count_init(void) { int ret; // 建立设备文件 ret = misc_register(&misc); // 输出日志信息 printk("word_count_init_success\n"); return ret; } // 卸载Linux驱动 static void word_count_exit(void) { // 删除设备文件 misc_deregister(&misc); // 输出日志信息 printk("word_init_exit_success\n"); } // 注册初始化Linux驱动的函数 module_init( word_count_init); // 注册卸载Linux驱动的函数 module_exit( word_count_exit); MODULE_AUTHOR("lining"); MODULE_DESCRIPTION("statistics of word count."); MODULE_ALIAS("word count module."); MODULE_LICENSE("GPL"); ~~~ 编写word_count驱动程序需要了解如下几点。 1.  get_word_count函数将mem数组中第1个为“\0”的字符作为字符串的结尾符,因此在word_count_write函数中将mem[count]的值设为“\0”,否则get_word_count函数无法知道要统计单词数的字符串到哪里结束。由于mem数组的长度为10000,而字符串最后一个字符为“\0”,因此待统计的字符串最大长度为9999。 2.  单词数使用int类型变量存储。在word_count_write函数中统计出了单词数(word_count变量的值),在word_count_read函数中将word_count整型变量值分解成4个字节存储在buf中。因此,在应用程序中需要再将这4个字节组合成int类型的值。 **十、编译、安装、卸载Linux驱动程序**       在上一节word_count驱动程序已经全部编写完成了,而且多次编译测试该驱动程序。安装和卸载word_count驱动也做过多次。word_count驱动与read_write目录中的驱动一样,也有一个build.sh和3个与平台相关的脚本文件。这些脚本文件与6.3.5节的实现类似,这里不再详细介绍。现在执行build.sh脚本文件,并选择要编译的平台。然后执行下面两行命令查看日志输出信息和word_count驱动模块(word_count.ko)的信息。  # dmesg |tail -n 1  # modinfo word_count.ko 如果显示如图6-12所示的信息,表明word_count驱动工作完全正常。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8aaf287d.jpg)      本书的脚本文件都是使用insmod命令安装Linux驱动的,除了该命令外,使用modprobe命令也可以安装Linux驱动。insmod和modprobe的区别是modprobe命令可以检查驱动模块的依赖性。如A模块依赖于B模块(装载A之前必须先装载B)。如果使用insmod命令装载A模块,会出现错误。而使用modprobe命令装载A模块,B模块会现在装载。在使用modprobe命令装载驱动模块之前,需要先使用depmod命令检测Linux驱动模块的依赖关系。  # depmod  /root/drivers/ch06/word_count/word_count.ko depmod命令实际上将Linux驱动模块文件(包括其路径)添加到如下的文件中。 /lib/modules/3.0.0-16-generic/modules.dep 使用depmod命令检测完依赖关系后,就可以调用modprobe命令装载Linux驱动。  # modprobe word_count 使用depmod和modprobe命令需要注意如下几点: 1\. depmod命令必须使用Linux驱动模块(.ko文件)的绝对路径。 2\. depmod命令会将内核模块的依赖信息写入当前正在使用的内核的modules.dep文件。例如,笔者的Ubuntu Linux使用的是Linux3.0.0.16,所以应到3.0.0-16-generic目录去寻找modules.dep文件。如果读者使用了其他Linux内核,需要到相应的目录去寻找modules.dep文件。 3\. modprobe命令只需使用驱动名称即可,不需要跟.ko。 ### [使用Android、S3C6410开发板和Ubuntu测试Linux驱动](http://blog.csdn.net/nokiaguy/article/details/8635795) 本文节选至[《Android深度探索(卷1):HAL与驱动开发》](http://product.dangdang.com/main/product.aspx?product_id=23043311),接下来几篇文章将详细阐述如何开发ARM架构的Linux驱动,并分别利用android程序、NDK、可执行文件测试Linux驱动。可在ubuntu Linux、Android模拟器和S3C6410开发板(可以选购OK6410-A开发板,需要刷Android)
';

开发可统计单词个数的Android驱动程序(2)

最后更新于:2022-04-01 07:20:32

[开发可统计单词个数的Android驱动程序(1)](http://blog.csdn.net/nokiaguy/article/details/8523272) **五、指定与驱动相关的信息** 虽然指定这些信息不是必须的,但一个完整的Linux驱动程序都会指定这些与驱动相关的信息。一般需要为Linux驱动程序指定如下信息。 1\. 模块作者:使用MODULE_AUTHOR宏指定。 2\. 模块描述:使用MODULE_DESCRIPTION宏指定。 3\. 模块别名:使用MODULE_ALIAS宏指定。 4\. 开源协议:使用MODULE_LICENSE宏指定。 除了这些信息外,Linux驱动模块自己还会包含一些信息。读者可以执行下面的命令查看word_count.ko的信息。  # modinfo word_count.ko     执行上面的命令后,会输出如图6-6所示的信息。其中depends表示当前驱动模块的依赖,word_count并没有依赖什么,因此该项为空。vermagic表示当前Linux驱动模块在那个Linux内核版本下编译的。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8aa31c2d.jpg)     现在使用下面的代码指定上述4种信息。一般会将这些代码放在word_count.c文件的最后。 MODULE_AUTHOR("lining"); MODULE_DESCRIPTION("statistics of wordcount."); MODULE_ALIAS("word count module."); MODULE_LICENSE("GPL");      现在使用上一节的方法重新编译word_count.c文件。然后再执行modinfo命令,就会显示如图6-7所示的信息。从图6-7可以看出,上面的代码设置的信息都包含在了word_count.ko文件中。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8aa5549d.jpg) **六、开源协议**        虽然很多个人开发者或小公司并不太考虑开源协议的限制,但对于较大的公司如果违反开源协议,可能会有被起诉的风险。所以对有一定规模和影响力的公司使用开源软件时一定要注意这些软件使用的开源协议。        为了降低发布Linux驱动的难度和安装包尺寸,很多Linux驱动都是开放源代码的。在Linux驱动源代码中使用MODULE_LICENSE宏指定开源协议。例如,word_count驱动使用了GPL协议。那么我们要编写Linux'驱动程序应采取什么协议呢?目前开源协议比较多。读者可以到下面的页面查看所有的开源协议。 http://www.opensource.org/licenses/alphabetical     下面将介绍最常用的5种开源协议的基本情况。这5种开源协议以及其他的开源协议的详细情况请参阅Open SourceInitiative组织的相关页面。 **GPL协议**          对于喜欢钻研技术的程序员来说应该很喜欢GPL协议。因为GPL协议强迫使用该开源协议的软件开源。例如,Linux内核就采用了GPL协议。GPL的出发点就是免费/开源。但与其他开源协议(如BSD、Apache Licence)不同的是GPL协议开源的更彻底。不仅要求采用GPL协议的软件开源/免费,还要求其衍生代码开源/免费。例如,A软件采用了GPL协议,B软件使用了A软件,那么B软件也必须免费/开源。而其B软件必须也采用GPL协议。C软件又使用了B软件,C软件也必须开源/免费,当然,C软件也必须采用GPL协议。这就是所谓的“传染性”。这也是目前有很多Linux发行版及其他使用GPL协议的软件开源的原因,         由于GPL协议严格要求使用了GPL协议的软件产品必须使用GPL协议,而且必须开源/免费。对于商业软件或者对代码有保密要求的部门就非常不适合使用GPL协议发布软件,或引用基于GPL协议的类库。为了满足商业公司及保密的需要,在GPL协议的基础上又出现了LGPL协议。 **LGPL协议**       LGPL主要是为类库使用设计的开源协议。与GPL要求任何使用/修改/衍生的GPL类库的的软件必须采用GPL协议不同。LGPL 允许商业软件通过类库引用(link)方式使用LGPL类库而不需要开源商业软件的代码。这使得采用LGPL协议的开源代码可以被商业软件作为类库引用并发布和销售。       但是如果修改LGPL协议的代码或者衍生,则所有修改的代码,涉及修改部分的额外代码和衍生的代码都必须采用LGPL协议。因此LGPL协议的开源代码很适合作为第三方类库被商业软件引用,但不适合希望以LGPL协议代码为基础,通过修改和衍生的方式做二次开发的商业软件采用。 **BSD协议**       BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以“为所欲为”,可以自由的使用,修改源代码,也可以将修改后的代码作为开源或者专有软件再发布。但“为所欲为”的前提是当你发布使用了BSD协议的代码,或则以BSD协议代码为基础做二次开发自己的产品时,需要满足如下3个条件。 1\. 如果再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议。 2\. 如果再发布的只是二进制类库/软件,则需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议。 3\. 不可以用开源代码的作者/机构名字和原来产品的名字做市场推广。      BSD 协议鼓励代码共享,但需要尊重源代码作者的著作权。BSD由于允许使用者修改和重新发布代码,也允许使用或在BSD代码上开发商业软件发布和销售,因此是对商业集成很友好的协议。而很多的公司企业在选用开源产品的时候都首选BSD协议,因为可以完全控制这些第三方的代码,在必要的时候可以修改或者二次开发。 **Apache Licence 2.0协议**  Apache Licence是著名的非盈利开源组织Apache采用的协议。该协议和BSD类似,同样鼓励代码共享和尊重原作者的著作权,同样允许代码修改,再发布(作为开源或商业软件)。需要满足的条件也和BSD类似。 1\. 需要给代码的用户一份Apache Licence 2\. 如果你修改了代码,需要在被修改的文件中说明。 3\. 在延伸的代码中(修改和由源代码衍生的代码中)需要带有原来代码中的协议,商标,专利声明和其他原来作者规定需要包含的说明。 4\. 如果再次发布的产品中包含一个Notice文件,则在Notice文件中需要带有Apache Licence。你可以在Notice中增加自己的许可,但不可以表现为Apache Licence。 Apache Licence也是对商业应用友好的许可。使用者也可以在需要的时候修改代码来满足需要并作为开源或商业产品发布/销售。 **MIT协议**         MIT是和BSD一样限制宽松的许可协议,作者只想保留版权,而无任何其他了限制.也就是说,你必须在你的发行版里包含原许可协议的声明,无论你是以二进制发布的还是以源代码发布的。 七、注册和注销设备文件        本节将为word_count驱动建立一个设备文件,该设备文件的名称是wordcount,位于/dev目录中。设备文件与普通文件不同,不能使用IO函数建立,需要使用misc_register函数建立设备文件,使用misc_deregister函数注销(移除)设备文件。这两个函数的定义如下: ~~~ extern int misc_register(struct miscdevice * misc); extern int misc_deregister(struct miscdevice*misc); ~~~        一般需要在初始化Linux驱动时建立设备文件,在卸载Linux驱动时删除设备文件。而且设备文件还需要一个结构体(miscdevice)来描述与其相关的信息。miscdevice结构体中有一个重要的成员变量fops,用于描述设备文件在各种可触发事件的函数指针。该成员变量的数据类型也是一个结构体file_operations。       本节需要修改word_count.c文件的word_count_init和word_count_exit函数,并定义一些宏和变量。修改部分的代码如下: ~~~ // 定义设备文件名 #define DEVICE_NAME "wordcount" // 描述与设备文件触发的事件对应的回调函数指针 // owner:设备事件回调函数应用于哪些驱动模块,THIS_MODULE表示应用于当前驱动模块 static struct file_operations dev_fops = { .owner = THIS_MODULE}; // 描述设备文件的信息 // minor:次设备号 MISC_DYNAMIC_MINOR,:动态生成次设备号 name:设备文件名称 // fops:file_operations结构体变量指针 static struct miscdevice misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME,.fops = &dev_fops }; // 初始化Linux驱动 static int word_count_init(void) { int ret; // 建立设备文件 ret = misc_register(&misc); // 输出日志信息 printk("word_count_init_success\n"); return ret; } // 卸载Linux驱动 static void word_count_exit(void) { // 注销(移除)设备文件 misc_deregister(&misc); // 输出日志信息 printk("word_init_exit_success\n"); } ~~~ 编写上面代码需要注意如下几点: 1.  设备文件由主设备号和次设备号描述。而使用misc_register函数只能设置次设备号。主设备号统一设为10。主设备号为10的设备是Linux系统中拥有共同特性的简单字符设备。这类设备称为misc设备。如果读者实现的驱动的功能并不复杂,可以考虑使用10作为其主设备号,而次设备号可以自己指定,也可以动态生成(需要指定MISC_DYNAMIC_MINOR常量)。因为采用这样的方式可以使用misc_register和misc_deregister函数简化注册和注销设备文件的步骤。在后面的章节会详细介绍如何使用register_chrdev_region和alloc_chrdev_region函数同时指定主设备号和次设备号的方式注册和注销设备文件。 2.  miscdevice.name变量的值就是设备文件的名称。在本例中设备文件名称为wordcount。 3.  虽然file_operations结构体中定义了多个回调函数指针变量,但本节并未初始化任何一个回调函数指针变量。只初始化了file_operations.owner变量。如果该变量的值为module结构体,表示file_operations可被应用在这些由module指定的驱动模块中。如果owner变量的值为THIS_MODULE,表示file_operations只应用于当前驱动模块。 4\. 如果成功注册了设备文件,misc_register函数返回非0的整数,如果注册设备文件失败,返回0。 5.  可能有的读者注意到了。word_count.c中的所有函数、变量都声明成了static。这是因为在C语言中用static声明函数、变量等资源,系统会将这些函数和变量单独放在内存的某一个区域,直到程序完全退出,否则这些资源不会被释放。Linux驱动一旦装载,除非手动卸载或关机,驱动会一直驻留内存,因此这些函数和变量资源会一直在内存中。也就是说多次调用这些资源不用再进行压栈、出栈操作了。有利于提高驱动的运行效率。 现在重新编译word_count.c文件并使用如下的命令安装word_count驱动。  # insmod word_count.ko 如果word_count驱动已经被安装,应先使用下面的命令下载word_count驱动,然后再使用上面的命令安装word_count驱动。  # rmmod word_count 安装完word_count驱动后,使用下面的命令查看/dev目录中的设备。  # ls –a /dev 执行上面的命令后,会输出如图6-8所示的信息,其中多了一个wordcount文件(在白框中)。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8aa6f034.jpg)         如果想查看wordcount设备文件的主设备号和次设备号,可以使用如下的命令。  # ls –l /dev       执行上面的命令会输出如图6-9所示的信息,白框中的第一个数字是主设备号,第二个数字是从设备号。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8aa9dec3.jpg) 使用下面的命令可获显示当期系统中有哪些主设备以及主设备号。  # cat /proc/devices 执行上面的命令后会输出如图6-10所示的信息,从中可以找到misc设备以及主设备编号10。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8aac4eeb.jpg) [开发可统计单词个数的Android驱动程序(3)](http://blog.csdn.net/nokiaguy/article/details/8608994) 本文节选至[《Android深度探索(卷1):HAL与驱动开发》](http://product.dangdang.com/main/product.aspx?product_id=23043311),接下来几篇文章将详细阐述如何开发ARM架构的Linux驱动,并分别利用android程序、NDK、可执行文件测试Linux驱动。可在ubuntu Linux、Android模拟器和S3C6410开发板(可以选购OK6410-A开发板,需要刷Android)
';

《Android深度探索(卷1):HAL与驱动开发》新书发布

最后更新于:2022-04-01 07:20:29

《 Android深度探索(卷1):HAL与驱动开发》分为4篇,分别从搭建开发环境,Linux驱动和Android HAL的基础知识,开发Linux驱动的高级技术和分析典型的Linux驱动源代码4个方面介绍Android和Linux的底层开发。本书使用的试验环境是Ubuntu Linux12.04 LTS、Android模拟器和S3C6410开发板。在第1篇详细介绍了如何搭建和使用这3个试验环境。第2篇通过3个Linux驱动的完整案例(统计单词个数驱动、LED驱动和蜂鸣器驱动)从不同角度来讨论如何开发一个完整的Linux驱动。并且通过完整的案例介绍了如何编写Android HAL,以及如何与Linux驱动交互。第3篇则介绍了开发Linux驱动所需要的高级技术,这些技术包括并发控制、阻塞和非阻塞I/O、异步编程、Linux中断和底半部、时间管理、内存管理和I/O访问。最后一部分分析了一些典型Linux驱动的源代码(RTC驱动、LCD驱动、音频驱动、块设备驱动、网络设备驱动和USB驱动)。   《Android深度探索(卷1):HAL与驱动开发》注重理论和实践相结合。在介绍了大量的基础知识的同时,为每一个知识点提供了完整的案例,使读者可以通过实际的代码更好地理解Linux驱动和Android底层技术。      个人微博 [http://weibo.com/androidguy](http://weibo.com/androidguy)   为了使读者更好地实践本书提供的实例代码,在随书光盘中除了提供源代码文件外,还提供了一个VMWare Ubuntu Linux12.04 LTS的虚拟环境(如过在win8下使用vmware,请参阅《[Windows8与VMWare不兼容的解决方案](http://blog.csdn.net/nokiaguy/article/details/8565658)》。如将虚拟机文件复制到PC上无法启动Ubuntu Linux12.04,请看这篇文章《[Vmware虚拟机的复制后无法使用的问题和解决 ](http://blog.csdn.net/nokiaguy/article/details/8573308)》,读者可以在Windows、Linux和Mac OS X上,通过VMWare打开该虚拟机文件来学习和测试本书的例子(虚拟环境中也带了一套本书提供的例子代码)。   《Android深度探索(卷1):HAL与驱动开发》适合底层开发的程序员和编程爱好者使用,也适合作为相关培训学校的Android底层开发培训教材。 [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8aa20696.JPG)](http://book.360buy.com/11155655.html) 目录 第一篇 Android驱动开发前的准备 第1章 Android系统移植与驱动开发概述 1.1 Android系统架构 1.2 Android系统移植的主要工作 1.3 查看Linux内核版本 1.4 Linux内核版本号的定义规则 1.5 如何学习Linux驱动开发 1.6 Linux设备驱动 1.6.1 设备驱动的发展和作用 1.6.2 设备的分类及特点 1.7 见识一下什么叫Linux驱动:LED 1.8 小结 第2章 搭建Android开发环境 2.1 Android底层开发需要哪些工具 2.2 安装JDK 2.3 搭建Android应用程序开发环境 2.3.1 安装Android SDK 2.3.2 安装Eclipse 2.3.3 安装ADT 2.3.4 配置ADT 2.3.5 建立AVD 2.4 安装Android NDK开发环境 2.4.1 下载Android NDK 2.4.2 安装CDT 2.4.3 命令行方式编译Android NDK程序 2.4.4 导入Android NDK的例子 2.4.5 配置Android NDK的集成开发环境 2.5 安装交叉编译环境 2.6 小结 第3章 Git使用入门 3.1 安装Git 3.2 查看Git文档 3.3 源代码的提交与获取 3.3.1 创建版本库:git init 3.3.2 将文件提交到本地版本库:git commit 3.3.3 创建本地分支:git branch 3.3.4 切换本地分支:git checkout 3.3.5 在GitHub上创建开源项目 3.3.6 上传源代码到GitHub:git push 3.3.7 从GitHub下载源代码:git clone 3.4 小结 第4章 源代码的下载和编译 4.1 下载、编译和测试Android源代码 4.1.1 配置Android源代码下载环境 4.1.2 Android源代码目录结构解析 4.1.3 下载Android源代码中的一部分 4.1.4 编译Android 源代码 4.1.5 out目录结构分析 4.1.6 将自己的APK作为Android内置程序发布 4.1.7 用模拟器测试system.img文件 4.2 下载和编译Linux内核源代码 4.2.1 下载Linux内核源代码 4.2.2 Linux内核源代码的目录结构 4.2.3 安装Android内核的编译环境 4.2.4 配置和编译Linux内核 4.3 小结 第5章 搭建S3C6410开发板的测试环境 5.1 S3C6410开发板简介 5.2 安装串口调试工具:minicom 5.3 烧写Android系统 5.4 配置有线网络 5.5 小结 第二篇 Android底层开发入门 第6章 第一个Linux驱动程序:统计单词个数 6.1 Linux驱动到底是个什么东西 6.2 编写Linux驱动程序的步骤 6.3 第一个Linux驱动:统计单词个数 6.3.1 编写Linux驱动程序前的准备工作 6.3.2 编写Linux驱动程序的骨架(初始化和退出驱动) 6.3.3 指定与驱动相关的信息 6.3.4 注册和注销设备文件 6.3.5 指定回调函数 6.3.6 实现统计单词数的算法 6.3.7 编译、安装、卸载Linux驱动程序 6.4 使用多种方式测试Linux驱动 6.4.1 使用Ubuntu Linux测试Linux驱动 6.4.2 在Android模拟器上通过原生(Native)C程序测试Linux驱动 6.4.3 使用Android NDK测试Linux驱动 6.4.4 使用Java代码直接操作设备文件来测试Linux驱动 6.4.5 使用S3C6410开发板测试Linux驱动 6.4.6 将驱动编译进Linux内核进行测试 6.5 使用Eclipse开发和测试Linux驱动程序 6.5.1 在Eclipse中开发Linux驱动程序 6.5.2 在Eclipse中测试Linux驱动 6.6 小结 第7章 LED将为我闪烁:控制发光二级管 7.1 LED驱动的实现原理 7.2 编写LED驱动 7.2.1 体验LED驱动的奇妙 7.2.2 创建LED驱动的设备文件 7.2.3 卸载LED驱动的设备文件 7.2.4 设置寄存器与初始化LED驱动 7.2.5 控制LED 7.2.6 LED驱动的模块参数 7.2.7 LED驱动的完整代码 7.3 测试LED驱动 7.3.1 编写测试I/O控制命令的通用程序 7.3.2 使用NDK测试LED驱动 7.3.3 使用Java测试LED驱动 7.4 LED驱动的移植 7.5 小结 第8章 让开发板发出声音:蜂鸣器驱动 8.1 Linux驱动的代码重用 8.1.1 编译是由多个文件组成的Linux驱动 8.1.2 Linux驱动模块的依赖(导出符号) 8.2 强行卸载Linux驱动 8.3 蜂鸣器(PWM)驱动 8.3.1 蜂鸣器驱动的原理 8.3.2 实现蜂鸣器驱动 8.3.3 测试蜂鸣器驱动 8.4 小结 第9章 硬件抽象层:HAL 9.1 为什么要在Android中加入HAL 9.2 Android HAL架构 9.3 为LED驱动增加HAL 9.3.1 编写一款支持HAL的Linux驱动程序的步骤 9.3.2 颠覆Linux驱动的设计理念:精简LED驱动 9.3.3 测试读写寄存器操作 9.3.4 编写调用LED驱动的HAL模块 9.3.5 编写调用HAL模块的Service 9.3.6 HAL模块的存放路径和命名规则 9.3.7 编写调用Service的Java库 9.3.8 测试LED驱动 9.4 小结 第10章 嵌入式Linux的调试技术 10.1 打印内核调试信息:printk 10.2 防止printk函数降低Linux 驱动性能 10.3 通过虚拟文件系统(/proc)进行数据交互 10.4 调试工具 10.4.1 用gdb调试用户空间程序 10.4.2 用gdbserver远程调试用户空间程序 10.4.3 用kgdb远程调试内核程序 10.5 小结 第三篇 Linux驱动开发高级技术 第11章 Linux驱动程序中的并发控制 11.1 并发和竞态 11.2 原子操作 11.2.1 整型原子操作 11.2.2 64位整型原子操作 11.2.3 位原子操作 11.2.4 用原子操作阻止设备文件被多个进程打开 11.3 自旋锁(Spin Lock) 11.3.1 自旋锁的使用方法 11.3.2 使用自旋锁保护临界区 11.3.3 读写自旋锁 11.3.4 使用读写自旋锁保护临界区 11.3.5 顺序锁(seqlock) 11.3.6 使用顺序锁写入正在读取的共享资源 11.4 读-复制-更新(RCU)机制 11.4.1 RCU的原理 11.4.2 RCU API 11.4.3 RCU的应用 11.5 信号量(Semaphore) 11.5.1 信号量的使用 11.5.2 信号量用于同步 11.5.3 读写信号量 11.5.4 使用读写信号量保护临界区 11.6 互斥体(Mutex) 11.7 完成量(Completion) 11.8 小结 第12章 Linux驱动程序中的阻塞和非阻塞I/O 12.1 等待队列 12.1.1 等待队列原理 12.1.2 等待队列的API 12.1.3 等待队列的使用方法 12.1.4 支持休眠和唤醒的Linux驱动 12.2 轮询操作 12.2.1 用户空间的select函数 12.2.2 内核空间的poll函数 12.2.3 以非阻塞的方式访问Linux驱动 12.3 小结 第13章 Linux驱动程序中的异步编程 13.1 信号与异步通知 13.1.1 Linux信号 13.1.2 接收Linux信号 13.1.3 发送信号 13.2 异步I/O(AIO) 13.2.1 异步操作的API 13.2.2 异步读写本地文件 13.2.3 Linux驱动中的异步函数(aio_read和aio_write) 13.2.4 接收信号时异步读取数据 13.2.5 AIO中的回调函数 13.3 小结 第14章 Linux中断和底半部 14.1 什么是中断 14.2 中断处理程序 14.3 Linux 中断处理的核心:顶半部和底半部 14.4 获取Linux 系统的中断统计信息 14.5 Linux 中断编程 14.5.1 注册中断处理程序 14.5.2 注销中断处理程序 14.5.3 编写中断处理函数 14.5.4 共享中断处理程序 14.5.5 禁止和激活中断 14.5.6 禁止和激活中断线 14.5.7 获取中断系统的状态 14.5.8 与中断编程相关的函数和宏 14.6 实例:S3C6410实时钟中断 14.7 中断中下文 14.8 中断的实现原理 14.9 底半部 14.9.1 为什么要使用底半部 14.9.2 实现底半部的机制 14.9.3 软中断 14.9.4 Tasklet 14.9.5 实例:Tasklet演示 14.9.6 软中断处理线程(ksoftirqd) 14.9.7 工作队列(work queue) 14.9.8 与工作队列相关的API 14.9.9 实例:工作队列演示 14.10 小结 第15章 时间管理 15.1 Linux内核中的时间概念 15.1.1 时钟频率 15.1.2 提高时钟频率的优点和缺点 15.2 节拍总数(jiffies) 15.2.1 访问jiffies 15.2.2 jiffies、时间和时钟频率之间的转换 15.2.3 jiffies的回绕 15.2.4 用户空间和时钟频率 15.3 实时时钟和定时器 15.4 时钟中断处理程序的实现 15.5 读写本地时间 15.6 内核定时器 15.6.1 如何使用内核定时器 15.6.2 实例:秒表定时器 15.7 内核延迟 15.7.1 忙等待 15.7.2 短延迟 15.7.3 休眠延迟(schedule_timeout) 15.8 小结 第16章 内存管理与I/O访问 16.1 内存管理模式 16.1.1 内存的基本单位:页(Page) 16.1.2 页的逻辑划分:区(zone) 16.1.3 获取页 16.1.4 释放页 16.2 分配连续的内存空间(Kmalloc) 16.2.1 gfp_mask标志 16.2.2 释放内存(kfree) 16.3 分配不连续的内存空间(vmalloc) 16.4 全局缓存(slab) 16.4.1 Slab层的实现原理 16.4.2 Slab分配器 16.4.3 示例:从Slab高速缓存中分配和释放对象 16.5 Linux内存池 16.5.1 内存池的实现原理 16.5.2 示例:从内存池获取对象 16.6 虚拟地址与物理地址之间的转换 16.7 设备I/O端口与I/O内存 16.7.1 读写I/O端口 16.7.2 读写I/O内存 16.7.3 将I/O端口映射为I/O内存 16.7.4 申请和释放设备I/O端口和I/O内存 16.7.5 使用设备I/O端口和I/O内存的一般步骤 16.8 内核空间与用户空间共享数据 16.8.1 内存映射与VMA 16.8.2 示例:用户程序读取内核空间数据 16.9 I/O内存静态映射 16.10 小结 第四篇 Linux设备驱动与Android底层开发 第17章 RTC驱动 17.1 实时时钟(RTC)结构与移植内容 17.1.1 RTC系统的结构 17.1.2 RTC驱动主要的移植工作 17.2 RTC系统中的Android部分 17.2.1 警报管理:AlarmManager 17.2.2 警报服务:AlarmManagerService 17.2.3 直接与Alarm驱动交互的JNI代码 17.3 Alarm驱动的分析与移植 17.3.1 Alarm驱动简介 17.3.2 Alarm驱动中的关键数据结构 17.3.3 Alarm驱动的应用层接口(alarm_dev.c)代码分析 17.3.4 Alarm驱动的通用文件(alarm.c)代码分析 17.4 RTC驱动的分析与移植 17.4.1 实时时钟(RTC)的特性 17.4.2 RTC的结构 17.4.3 RTC芯片的寄存器 17.4.4 RTC驱动的用户空间接口 17.4.5 RTC系统组件之间的调用关系 17.4.6 设备文件(/dev/rtc0)的I/O命令 17.4.7 sysfs虚拟文件处理函数 17.4.8 proc虚拟文件处理函数 17.5 小结 第18章 LCD驱动 18.1 LCD简介 18.1.1 液晶的工作原理 18.1.2 LCD的种类 18.1.3 LCD的技术参数 18.1.4 LCD时序图 18.2 LCD驱动结构分析和移植要点 18.3 帧缓冲(FrameBuffer)驱动设计与实现 18.3.1 FrameBuffer设备 18.3.2 示例:通过dd命令与FrameBuffer设备文件交互 18.3.3 示例:编写访问FrameBuffer设备文件的程序 18.3.4 FrameBuffer驱动的架构 18.3.5 FrameBuffer驱动主要的数据结构 18.3.6 如何在Linux内核中查找指定的内容 18.3.7 FrameBuffer驱动设备事件的处理(fbmem.c) 18.3.8 FrameBuffer驱动源代码分析与移植 18.4 FrameBuffer驱动的HAL层分析 18.4.1 Gralloc库 18.4.2 初始化HAL Gralloc的核心结构体 18.4.3 获取Gralloc HAL模块 18.4.4 与FrameBuffer设备文件交互 18.5 调用Gralloc HAL库 18.6 小结 第19章 音频驱动 19.1 音频驱动基础 19.1.1 数字音频简介 19.1.2 ALSA架构简介 19.1.3 ALSA设备文件 19.1.4 数字采样与数字录音 19.1.5 混音器 19.1.6 音频驱动的目录结构 19.1.7 音频设备硬件接口 19.1.8 ALSA架构支持的声卡芯片 19.2 AC97芯片的寄存器 19.2.1 控制寄存器 19.2.2 状态寄存器 19.2.3 编解码器命令寄存器 19.2.4 编解码器状态寄存器 19.2.5 PCM输出/输入通道FIFO数据寄存器 19.2.6 MIC输入通道FIFO地址寄存器 19.2.7 PCM输出/输入通道FIFO数据寄存器 19.2.8 MIC输入通道FIFO数据寄存器 19.3 创建声卡 19.3.1 声卡的顶层数据结构 19.3.2 创建声卡的步骤 19.3.3 示例:基于ARM的AC97音频驱动 19.4 音频逻辑设备 19.4.1 创建PCM设备 19.4.2 创建录音和播放设备文件节点 19.4.3 创建Control设备数据结构 19.4.4 创建Control设备 19.4.5 注册与打开音频字符设备 19.5 嵌入式设备中的ALSA(ASoC) 19.5.1 什么是ASoC 19.5.2 ASoC的硬件架构 19.5.3 ASoC的软件架构 19.5.4 如何确定S3C开发板使用了哪个音频驱动 19.5.5 ASoC架构中的Machine 19.5.6 ASoC架构中的Codec 19.5.7 ASoC架构中的Platform 19.6 音频驱动的HAL分析 19.6.1 实现HAL Library 19.6.2 调用HAL Library 19.7 小结 第20章 Linux块设备驱动 20.1 块设备简介 20.2 块设备的体系架构 20.3 块设备的数据结构与相关操作 20.3.1 磁盘设备(gendisk结构体) 20.3.2 block_device_operations结构体 20.3.3 I/O请求(request结构体) 20.3.4 请求队列(request_queue结构体) 20.3.5 块I/O(bio结构体) 20.4 块设备的加载和卸载 20.5 块设备的打开和释放 20.6 块设备的ioctl函数 20.7 块设备驱动的I/O请求处理 20.7.1 依赖请求队列 20.7.2 不依赖请求队列 20.8 实例1:依赖请求队列的RamDisk 20.9 在嵌入式设备上测试块设备驱动 20.9.1 编译、配置和安装Busybox 20.9.2 测试块设备驱动 20.10 实例2:不依赖请求队列的RamDisk 20.11 扇区与磁盘碎片整理 20.12 小结 第21章 网络设备驱动 21.1 Linux网络设备驱动的结构 21.1.1 网络协议接口层 21.1.2 网络设备接口层 21.1.3 设备驱动功能层 21.1.4 网络设备与媒介层 21.2 网络设备驱动设计与实现 21.2.1 网络设备的注册与注销 21.2.2 网络设备的初始化 21.2.3 网络设备的打开与释放 21.2.4 发送数据 21.2.5 接收数据 21.2.6 网络连接状态 21.3 示例:DM9000网卡设备驱动 21.3.1 如何确定S3C6410开发板使用的网络设备 21.3.2 DM9000网卡硬件描述 21.3.3 网络设备驱动的定义与安装 21.3.4 初始化DM9000网卡设备驱动 21.3.5 移出网络设备 21.3.6 打开和停止DM9000网卡 21.3.7 发送数据 21.3.8 接收数据 21.3.9 设置广播地址 21.4 小结 第22章 USB驱动 22.1 USB设备简介 22.2 USB驱动与USB核心之间的交互 22.2.1 端点(Endpoint) 22.2.2 接口(Interfaces) 22.2.3 配置(Config) 22.3 USB设备的核心数据结构 22.3.1 USB设备:usb_device结构体 22.3.2 USB驱动:usb_driver结构体 22.3.3 识别USB设备:usb_device_id结构体 22.3.4 USB端点:usb_host_endpoint结构体 22.3.5 USB接口:usb_interface结构体 22.3.6 USB配置:usb_host_config结构体 22.4 描述符数据结构 22.4.1 设备描述符 22.4.2 配置描述符 22.4.3 接口描述符 22.4.4 端点描述符 22.4.5 字符串描述符 22.4.6 查看描述符信息 22.5 USB和sysfs 22.6 URB(USB请求块) 22.6.1 URB结构体 22.6.2 URB的处理流程 22.6.3 简单的批量与控制URB 22.7 USB驱动程序的结构 22.8 鼠标驱动分析 22.9 小结
';

开发可统计单词个数的Android驱动程序(1)

最后更新于:2022-04-01 07:20:27

Android本质上是基于Linux内核的系统,也就是说Android就是一种Linux操作系统。只不过大多数时候都会运行在ARM架构的设备上,例如,Android手机、平板等。Android驱动实际上就是Linux驱动,只是这里使用[Android深度探索(卷1):安装C/C++交叉编译环境 ](http://blog.csdn.net/nokiaguy/article/details/8509739)介绍的交叉编译器将Linux驱动编译成了ARM架构的,所以驱动可以安装在Android模拟器、Android手机(需要root)或平板上(这些设备都要使用给予ARM架构的CPU),当然,使用传统的GCC也可以编译成X86架构的驱动(并不需要修改代码),这样也可以在Ubuntu Linux上安装Linux驱动。        本文及后面几篇文章主要介绍如何利用Android模拟器和S3C6410开发板开发给予ARM架构的Linux驱动,当然,测试的环境是Android,而不是我们通常使用的Ubuntu Linux等X86架构的系统。最后会介绍通过多种方式测试这个驱动,测试方法包括命令行、NDK、Android程序(Java代码)等,当然,在最最后还会介绍如果将驱动嵌入到LInux内核中,这样Android在启动是就自动拥有了这个驱动。       想学习Android底层开发的童鞋可以通过本文完全掌握开发基于Android的LInux驱动的完整步骤。在[《Android深度探索(卷1):HAL与驱动开发》](http://product.dangdang.com/main/product.aspx?product_id=23043311)随书光盘上有完整的实验环境(VMWare Ubuntu Linux12.04LTS),如果嫌自己配置麻烦,可以从光盘中复制该虚拟环境,虚拟文件太大(3.6G),传不上去,只能发文章了! **一、Linux驱动到底是个什么东西**        对于从未接触过驱动开发的程序员可能会感觉Linux驱动很神秘。感觉开发起来会很复杂。其实这完全是误解。实际上Linux驱动和普通的LinuxAPI没有本质的区别。只是使用Linux驱动的方式与使用Linux API的方式不同而已。        在学习Linux驱动之前我们先来介绍一下Linux驱动的工作方式。如果读者以前接触过Windows或其他非Unix体系的操作系统,最好将它们的工作方式暂时忘掉,因为这些记忆会干扰我们理解Linux底层的一些细节。       Linux驱动的工作和访问方式是Linux的亮点之一,同时受到了业界的广泛好评。Linux系统将每一个驱动都映射成一个文件。这些文件称为设备文件或驱动文件,都保存在/dev目录中。这种设计理念使得与Linux驱动进行交互就像与普通文件进行交互一样容易。当然,也比访问LinuxAPI更容易。由于大多数Linux驱动都有与其对应的设备文件,因此与Linux驱动交换数据就变成了与设备文件交换数据。例如,向Linux打印机驱动发送一个打印命令,可以直接使用C语言函数open打开设备文件,再使用C语言函数ioctl向该驱动的设备文件发送打印命令。      当然,要编写Linux驱动程序还需要更高级的功能。如向打印机驱动写入数据时,对于打印机驱动来说,需要接收这些被写入的数据,并将它们通过PC的并口、USB等端口发送给打印机。要实现这一过程就需要Linux驱动可以响应应用程序传递过来的数据。这就是Linux驱动的事件,虽然在C语言里没有事件的概念,但却有与事件类似的概念,这就是回调(callback)函数。因此,编写Linux驱动最重要的一步就是编写回调函数,否则与设备文件交互的数据将无法得到处理。图6-1是应用软件、设备文件、驱动程序、硬件之间的关系。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8a9b7382.jpg) **二、编写Linux驱动程序的步骤** Linux驱动程序与其他类型的Linux程序一样,也有自己的规则。对于刚开始接触Linux驱动开发的读者可能对如何开发一个LInux驱动程序还不是太了解。为了解决这部分读者的困惑,本节给出了编写一个基本的Linux驱动的一般步骤。读者可以按着这些步骤循序渐进地学习Linux驱动开发。 **第1步:建立Linux驱动骨架(装载和卸载Linux驱动)** 任何类型的程序都有一个基本的结构,例如,C语言需要有一个入口函数main。Linux驱动程序也不例外。Linux内核在使用驱动时首先需要装载驱动。在装载过程中需要进行一些初始化工作,例如,建立设备文件,分配内存地址空间等。当Linux系统退出时需要卸载Linux驱动,在卸载的过程中需要释放由Linux驱动占用的资源,例如,删除设备文件、释放内存地址空间等。在Linux驱动程序中需要提供两个函数来分别处理驱动初始化和退出的工作。这两个函数分别用module_init和module_exit宏指定。Linux驱动程序一般都都需要指定这两个函数,因此包含这两个函数以及指定这两个函数的两个宏的C程序文件也可看作是Linux驱动的骨架。 **第2步:注册和注销设备文件** 任何一个Linux驱动都需要有一个设备文件。否则应用程序将无法与驱动程序交互。建立设备文件的工作一般在第1步编写的处理Linux初始化工作的函数中完成。删除设备文件一般在第1步编写的处理Linux退出工作的函数中完成。可以分别使用misc_register和misc_deregister函数创建和移除设备文件。  **第3步:指定与驱动相关的信息** 驱动程序是自描述的。例如,可以通过modinfo命令获取驱动程序的作者姓名、使用的开源协议、别名、驱动描述等信息。这些信息都需要在驱动源代码中指定。通过MODULE_AUTHOR、MODULE_LICENSE 、MODULE_ALIAS 、MODULE_DESCRIPTION等宏可以指定与驱动相关的信息。 **第4步:指定回调函数** Linux驱动包含了多种动作,也可称为事件。例如,向设备文件写入数据时会触发“写”事件,Linux系统会调用对应驱动程序的write回调函数,从设备文件读数据时会触发“读”事件,Linux系统会调用对应驱动程序的read回调函数。一个驱动程序并不一定要指定所有的回调函数。回调函数会通过相关机制进行注册。例如,与设备文件相关的回调函数会通过misc_register函数进行注册。 **第5步:编写业务逻辑** 这一步是Linux驱动的核心部分。光有骨架和回调函数的Linux驱动是没有任何意义的。任何一个完整的Linux驱动都会做一些与其功能相关的工作,如打印机驱动会向打印机发送打印指令。COM驱动会根据传输数率进行数据交互。具体的业务逻辑与驱动的功能有关。业务逻辑可能有多个函数、多个文件甚至是多个Linux驱动模块组成。具体的实现读者可以根据实际情况而定。 **第6步:编写Makefile文件** Linux内核源代码的编译规则是通过Makefile文件定义的。因此编写一个新的Linux驱动程序必须要有一个Makefile文件。 **第7步:编译Linux驱动程序** Linux驱动程序可以直接编译进内核,也可以作为模块单独编译。 **第8步:安装和卸载Linux驱动** 如果将Linux驱动编译进内核,只要Linux使用该内核,驱动程序就会自动装载。如果Linux驱动程序以模块单独存在,需要使用insmod或modprobe命令装载Linux驱动模块,使用rmmod命令卸载Linux驱动模块。 上面8步中的前5步是关于如何编写Linux驱动程序的,通过后3步可以使Linux驱动正常工作。 **三、编写Linux驱动程序前的准备工作** 本例的Linux驱动源代码并未与linux内核源代码放在一起,而是单独放在一个目录。首先使用下面的命令建立存放Linux驱动程序的目录。  # mkdir –p  /root/drivers/ch06/word_count  # cd  /root/drivers/ch06/word_count 然后使用下面的命令建立驱动源代码文件(word_count.c)  # echo '' > word_count.c 最后编写一个Makefile文件,实际上这是6.2节介绍的编写Linux驱动程序的第6步。当熟悉编写Linux驱动程序的步骤后可以不按6.2节介绍的顺序来编写Linux驱动。  # echo 'obj-m := word_count.o'  > Makefile 其中obj-m表示将Linux驱动作为模块(.ko文件)编译。如果使用obj-y,则将Linux驱动编译进Linux内核。obj-m或obj-y需要使用“:=”赋值。如果obj-m或obj-y的值为word_count.o,表示make命令会把Linux驱动源代码目录中的word_count.c或word_count.s文件编译成word_count.o文件。如果使用obj-m,word_count.o会被连接进word_count.ko文件,然后使用insmod或modprobe命令装载word_count.ko。如果使用obj-y,word_count.o会被连接进built-in.o文件,最终会被连接进内核。其中built-in.o文件是连接同一类程序的.o文件生成的中间目标文件。例如,所有的字符设备驱动程序会最终生成一个built-in.o文件。读者可以在/drivers/char目录找到一个built-in.o文件。该目标文件包含了所有可连接进Linux内核的字符驱动(通过make menuconfig命令可以配置每一个驱动及其他内核程序是否允许编译进内核,关于配置Linux内核的技术详见4.2.4节介绍)。 如果Linux驱动依赖其他程序,如process.c、data.c。需要按如下方式编写Makefile文件。 obj-m := word_count.o word_count-y := process.o  data.o 其中依赖文件要使用module-y或module-objs指定。module表示模块名,如word_count。 **四、编写Linux驱动程序的骨架** 现在编写Linux驱动程序的骨架部分,也就是前面介绍的第1步。骨架部分主要是Linux驱动的初始化和退出函数,代码如下: ~~~ #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> // 初始化Linux驱动 static int word_count_init(void) { // 输出日志信息 printk("word_count_init_success\n"); return 0; } // 退出Linux驱动 static void word_count_exit(void) { // 输出日志信息 printk("word_count_init_exit_success\n"); } // 注册初始化Linux驱动的函数 module_init(word_count_init); // 注册退出Linux驱动的函数 module_exit(word_count_exit); ~~~       在上面的代码中使用了printk函数。该函数用于输出日志信息(关于printk函数的详细用法将在10.1节详细介绍)。printk函数与printf函数的用法类似。有的读者可能会有疑问,为什么不用printf函数呢?这里就涉及到一个Linux内核程序可以调用什么,不可以调用什么的问题。Linux系统将内存分为了用户空间和内核空间,这两个空间的程序不能直接访问。printf函数运行在用户空间,printk函数运行在内核空间。因此,属于内核程序的Linux驱动是不能直接访问printf函数的。就算包含了stdio.h头文件,在编译Linux驱动时也会抛出stdio.h文件没找到的错误。当然,运行在用户空间的程序也不能直接调用printk函数。那么是不是用户空间和内核空间的程序就无法交互了呢?答案是否定的。否则这两块内存不就成了孤岛了吗。运行在这两块内存中的程序之间交互的方法很多。其中设备文件就是一种主要的交互方式(在后面的章节还会介绍/proc虚拟文件的交互方式)。如果用户空间的程序要访问内核空间,只要做一个可以访问内核空间的驱动程序,然后用户空间的程序通过设备文件与驱动程序进行交互即可。       看到这可能有的读者疑问更大了。Linux驱动程序无法直接访问运行在用户空间的程序,那么很多功能就都得自己实现了。例如,在C语言中会经常使用malloc函数动态分配内存空间,该函数在Linux驱动程序中是无法使用的。那么如何在Linux驱动程序中动态分配内存空间呢?解决类似的问题也很简单。既然Linux驱动无法直接调用运行在用户空间的函数,那么在Linux内核中就必须要提供替代品。读者可以进入/include目录,该目录的各个子目录中包含了大量的C语言头文件。这些头文件中定义的函数、宏等资源就是运行在用户空间的程序的替代品。运行在用户空间的函数库对应的头文件在/usr/include目录中。刚才提到的malloc函数在内核空间的替代品是kmalloc(需要包含slab.h头文件,#include  )。 **注意:**用户空间与内核空间完成同样或类似功能的函数、宏等资源的名称并不一定相同,有的名称类似,如malloc和kmalloc,有的完全是两个不同的名字:如atoi(用户空间)和simple_strtol(内核空间)、itoa(用户空间)和snprintf(内核空间)。读者在使用内核相关资源时要注意在一点。 如果读者想看看前面编写的程序的效果,可以使用下面的命令编译Linux驱动源代码(X86架构)。  # make  -C /usr/src/linux-headers-3.0.0-15-generic M=/root/drivers/ch06/word_count      在测试Linux驱动未必一定在Android设备上完成。因为Android系统和Ubuntu Linux以及其他Linux发行版本都是基于Linux内核的,大多数Linux驱动程序可以在Ubuntu Linux或其他Linux发行版上测试完再重新用交叉编译器编译成基于ARM架构的目标文件,然后再安装到Android上即可正常运行。由于编译Linux内核源代码需要使用Linux内核的头文件。为了在Ubuntu Linux上测试驱动程序,需要使用-C命令行参数指定Linux内核头文件的目录(/usr/src/linux-headers-3.0.0-15-generic)。其中linux-headers-3.0.0-15-generic目录是Linux内核源代码目录,在该目录中只有include子目录有实际的头文件,其他目录只有Makefile和其他一些配置文件,并不包含Linux内核源代码。该目录就是为了开发当前Linux内核版本的驱动及其他内核程序而提供的(因为在编译Linux驱动时生成目标文件只需要头文件,在进行目标文件链接时只要有相关的目标文件即可,并不需要源代码文件)。如果以模块方式编译Linux驱动程序,需要使用M指定驱动程序所在的目录(M= root/drivers/ch06/word_count)。 **注意:**如果读者使用的Linux发行版采用了其他Linux内核,需要为-C命令行参数设置正确的路径。 执行上面的命令后,会输出如图6-2所示信息。从这些信息可以看出,已经将word_count.c文件编译成了Linux驱动模块文件word_count.ko。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8a9cb06e.jpg)          使用ls命令列出/root/drivers/ch06/word_count目录中的文件后发现,除了多了几个.o和.ko文件,还多了一些其他的文件,如图6-3所示。这些文件是有编译器自动生成的,一般并不需要管这些文件的内容。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8a9e0dc7.jpg)       本文编写的Linux驱动程序虽然什么实际的功能都没有,但已经可以作为驱动程序安装在Linux内核空间了。读者可以使用下面的命令安装、查看、卸载Linux驱动,也可以查看由驱动程序输出的日志信息(执行下面命令时需要先进入word_count目录)。 **安装Linux驱动**  # insmod word_count.ko **查看word_count是否成功安装**  # lsmod | grep word_count **卸载Linux驱动**  # rmmod word_count **查看由Linux驱动输出的日志信息**  # dmesg | grep word_count | tail –n 2 执行上面的命令后,如果输出如图6-4所示的信息说明读者已成功完成本节的学习,可以继续看下一节了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8a9f1993.jpg)         dmesg命令实际上是从/var/log/messages(Ubuntu Linux 10.04)或/var/log/syslog(Ubuntu Linux11.10)文件中读取的日志信息,因此也可以执行下面的命令获取由Linux驱动输出的日志信息。  # cat /var/log/syslog | grep word_count | tail –n 2 执行上面的命令后会输出更多的信息,如图6-5所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8aa0fb74.jpg) ### [开发可统计单词个数的Android驱动程序(2)](http://blog.csdn.net/nokiaguy/article/details/8584632) 本文节选至[《Android深度探索(卷1):HAL与驱动开发》](http://product.dangdang.com/main/product.aspx?product_id=23043311),接下来几篇文章将详细阐述如何开发ARM架构的Linux驱动,并分别利用android程序、NDK、可执行文件测试Linux驱动。可在ubuntu Linux、Android模拟器和S3C6410开发板(可以选购OK6410-A开发板,需要刷Android)
';

Android深度探索(卷1):安装C/C++交叉编译环境

最后更新于:2022-04-01 07:20:25

X86架构的CPU采用的是复杂指令集(Complex Instruction Set Computer,CICS),而ARM架构的CPU使用的是精简指令集(Reduced Instruction Set Computer,RISC)。由于这两种架构的CPU使用了不同的指令集,因此在X86架构上开发可运行在ARM架构上的程序就必须要使用交叉编译器。通常交叉编译器和相关工具包含的了很多可执行文件以及大量的共享库及头文件等资源。这些资源的集合称为交叉编译环境。         在Internet上可以找到集成好的交叉编译环境,Android NDK和Android源代码中也包含的交叉编译环境。当然,如果我们需要一个完全独立的交叉编译环境,可以下载集成好的交叉编译环境,也可以自己制作(比较复杂,建议读者下载集成好的交叉编译环境)。下面介绍一个CodeSourcery交叉编译环境,这个交叉编译环境直接下载安装就可以安装。       读者可通过如下的网站访问CodeSourcery下载页面。 [http://www.mentor.com/embedded-software/sourcery-tools/sourcery-codebench/editions/lite-edition/](http://www.mentor.com/embedded-software/sourcery-tools/sourcery-codebench/editions/lite-edition/)     上面的页面并未直接提供CodeSourcery的下载链接,读者可以点击“Download the GNU/Linux Release”链接,并输入Email(建议使用Gmail)、地址等信息,最后点击“Get Lite!”按钮提交信息。如图2-29所示。最后系统会将下载地址发送到刚才输入的Email。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8a958e0a.jpg) 进入下载页面,会看到如图2-30所示的不同版本的下载链接,选择最新的版本即可。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8a9701d9.jpg)                              图2-30  CodeSourcery的不同版本        进入CodeSourcery的下载页面后,会看到如图2-31所示的下载链接。目前CodeSourcery有Windows和Linux两个版本。本书使用的是第1个(IA32 GNU/Linux Installer)。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8a98df89.jpg)     Linux版本的安装文件是bin格式,读者可执行下面的命令安装CodeSourcery。其中package表示CodeSourcery的安装文件名。 sh package.bin     执行上面的命令后,会显示可视化的安装界面,如图2-32所示,读者可按提示安装CodeSourcery。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cb8a9a2cdd.jpg)      为了使用方便,建议读者将如下路径加到PATH环境变量中。    /root/compilers/CodeSourcery/Sourcery_CodeBench_Lite_for_ARM_GNU_Linux/bin     下面编写一个简单的C程序来测试一下这个交叉编译环境。 ~~~ // first.c #include <stdio.h> int main() { printf("first arm program\n"); return 0; } ~~~ 输入下面的命令编译first.c文件(需要加-static选项)。 # arm-none-linux-gnueabi-gcc -static -o first first.c 输入下面的命令将first文件上传到任意的Android设备的/data/local目录中(没有root权限也可以)。 # adb push first /data/local 使用adb shell命令进入Android设备的控制台,并进入/data/local目录,执行如下的命令 # ./first      执行上面的命令就会输出“first arm program”信息。first程序在X86架构上运行的Ubuntu Linux中是无法运行的。读者可以试着在Ubuntu Linux中运行first程序,看看会发生什么。 注意:在安装Code Sourcery的过程中会在/lib目录寻找一个libc.so.6库文件,但在Ubuntu Linux11.04及以上版本/lib目录已经没有libc.so.6库文件了。这个库文件被放在了其他的目录。在32位的Ubuntu Linux中该文件的路径是/lib/i386-linux-gnu/libc.so.6,在64位的Ubuntu Linux中该文件的路径是/lib/x86_64-linux-gnu/libc.so.6。在安装Code Sourcery之前可以使用ln命令为libc.so.6文件在/lib目录建立一个链接。 下一篇:[开发可统计单词个数的Android驱动程序(1) ](http://blog.csdn.net/nokiaguy/article/details/8523272) 本文节选至[《Android深度探索(卷1):HAL与驱动开发》](http://product.dangdang.com/main/product.aspx?product_id=23043311),接下来几篇文章将详细阐述如何开发ARM架构的Linux驱动,并分别利用android程序、NDK、可执行文件测试Linux驱动。可在ubuntu Linux、Android模拟器和S3C6410开发板(可以选购OK6410-A开发板,需要刷Android)
';

前言

最后更新于:2022-04-01 07:20:23

> 原文出处:[移动开发专栏文章](http://blog.csdn.net/column/details/android-depth.html) > 作者:[李宁](http://blog.csdn.net/nokiaguy) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** #Android深度探索 > 所有关于Android底层原理和技术分析,主要包括Android和Linux内核源代码分析(C++部分),Android Framework源代码(Java部分)分析、ROM定制、Android系统应用实现原理与源代码解析,Dalivk虚拟机原理分析和性能优化等。
';