NYC's Blog - Linux http://niyanchun.com/tag/linux/ systemd学习总结 http://niyanchun.com/systemd-introduction.html 2017-02-27T21:27:00+08:00 现在越来越多的Linux发行版将init系统从upstart(or other)换成了systemd,当然systemd相比于旧的init系统更加灵活和高效,所以本文介绍一下systemd的常用命令。目前已经切换为systemd的常见发行版有:Ubuntu 15.04、Debian 8、CentOS 7、Fedora 15及其这些发行版的高版本。本文使用的是Ubuntu 16.04 LTS,用户为root,非root执行时需要加sudo.当然最全的文档还是man文档,所以细节可以查看man文档。本文主要包括三部分:systemctl常用命令,很多情况下我们会使用systemctl来管理自己的系统就够了。systemd的文件语法介绍,有兴趣的可以了解一下。systemd管理的基本对象称之为unit,这个unit可以是很多类型,,比如.service、.socket、.device、.mount、.swap、.automount等,其实任何系统知道如何操作和管理的资源systemd都可以去管理。不过我们最常见的类型是service(一般后缀为.service),本文第二部分的学习总结也主要是这个,所以本文提到的unit和服务指的是同一个东西,不再细分。本文中所。当然,systemd和upstart的区别本文没有涉及,这里推荐一篇文章,有兴趣的可以看一下:《浅析 Linux 初始化 init 系统,第 3 部分: Systemd》。systemctl常用命令我们主要使用systemctl命令来管理使用systemd的系统,下面以nginx为例进行说明。先安装一个nginx:apt install nginx # Ubuntu 16.04开始不需要使用apt-get了装好以后,我们就可以使用systemctl来管理nginx服务了:服务的启动、停止、重启、重新加载配置文件依次为start、stop、restart、reload:systemctl start/stop/restart/reload nginx.service开机自启/关闭开机自启动分别为enable和disable:systemctl enable/disable nginx.service列出所有已经启动(active)的unit,即已经被加载到内存的unit:systemctl list-units当然可以省略掉list-units,因为systemctl默认就执行的是上述命令。如果想查看所有的已启动和未启动的unit(已加载到内存,但未启动),在后面加上--all即可。列出系统已经安装的所有unit,包括那些没有被加载到内存的unit:systemctl list-unit-filesjournald是systemd中专门收集日志的模块,我们可以用他来查看日志。 查看所有日志(从最旧的开始显示):journalctl如果journald配置了保存上次启动的日志的话, 这个默认显示的自系统上次启动到现在的所有日志。有的发行版可能没有配置(比如我现在正在用的Ubuntu 16.04,其他的不知道),我们可以在/etc/systemd/journald.conf中将Storage设置为persistent,或者创建/var/log/journal目录。当然如果我们只想查看本次启动之后的日志,可以加上-b标志:journalctl -b如果只想查看内核日志的话,可以加上-k(可以和-b联合使用):journalctl -k查看unit的状态:systemctl status nginx.service查看服务的日志:journalctl -u nginx.service # 还可以配合`-b`一起使用,只查看自本次系统启动以来的日志查看unit文件等信息:# 查看unix文件 systemctl cat nginx.service # 查看unit所有依赖 systemctl list-dependencies nginx # 递归的列出所有依赖 systemctl list-dependencies --all nginx.service # 列出unit的详细信息 systemctl show nginx.service修改unit文件:systemctl edit nginx.service这样会创建一个新的配置文件,我们做的修改会覆盖默认的。当然我们想直接修改原来的配置文件的话,可以加上--full参数。必须注意的是,如果修改了unit文件,一定要执行systemctl daemon-reload让修改的配置生效。使用Target。我们都知道Linux有个运行级别(Runlevel),对应不同的模式,比如Ubuntu一般运行在5上面。在systemd中,这个运行级别就是Target:# 查看所有target下的unit systemctl list-unit-files --type=target # 查看默认target,即默认的运行级别。对应于旧的`runlevel`命令 systemctl get-default # 设置默认的target systemctl set-default multi-user.target # 查看某一target下的unit systemctl list-dependencies multi-user.target # 切换target,不属于新target的unit都会被停止 systemctl isolate multi-user.target管理主机:systemctl poweroff # 关机 systemctl reboot # 重启 systemctl rescue # 进入rescue模式systemd的配置文件文件位置这里我们先要说明一下unit的文件位置,一般主要有三个目录:/lib/systemd/system/run/systemd/system/etc/systemd/system这三个目录的配置文件优先级依次从低到高,如果同一选项三个地方都配置了,优先级高的会覆盖优先级低的。系统安装时,默认会将unit文件放在/lib/systemd/system目录。如果我们想要修改系统默认的配置,比如nginx.service,一般有两种方法:在/etc/systemd/system目录下创建nginx.service文件,里面写上我们自己的配置。在/etc/systemd/system下面创建nginx.service.d目录,在这个目录里面新建任何以.conf结尾的文件,然后写入我们自己的配置。推荐这种做法。/run/systemd/system这个目录一般是进程在运行时动态创建unit文件的目录,一般很少修改,除非是修改程序运行时的一些参数时,即Session级别的,才在这里做修改。文件语法上面我们安装了nginx,其实装完以后就会在/lib/systemd/system有一个nginx.service文件,内容如下:# Stop dance for nginx # ======================= # # ExecStop sends SIGSTOP (graceful stop) to the nginx process. # If, after 5s (--retry QUIT/5) nginx is still running, systemd takes control # and sends SIGTERM (fast shutdown) to the main process. # After another 5s (TimeoutStopSec=5), and if nginx is alive, systemd sends # SIGKILL to all the remaining processes in the process group (KillMode=mixed). # # nginx signals reference doc: # http://nginx.org/en/docs/control.html # [Unit] Description=A high performance web server and a reverse proxy server After=network.target [Service] Type=forking PIDFile=/run/nginx.pid ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;' ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;' ExecReload=/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/nginx.pid TimeoutStopSec=5 KillMode=mixed [Install] WantedBy=multi-user.targetunit文件由一些Section组成,Section的名字用中括号括起来,而且是区分大小写的。每个Section的作用域到下一个Section开始或者到文件尾。Section的通用格式如下:[Section] Directive1=value Directive2=value . . .虽然Section之间是没有先后顺序之分的,但unit文件一般都把[Unit]作为第一部分(Section),[Install]作为最后一部分,然后中间是每种类型特性的部分,比如Service就是[Service],当然我们也可以自定义自己的Section,不过需要在前面加上X-前缀。本文就只介绍Service这种类型的,下面依次介绍。[Unit]Unit一般是第一部分,用来描述unit文件元数据以及服务的依赖关系。常用的命令有:Description=:这里一般写服务简短的描述。Documentation=:这里一般是服务文档的链接等。Requires=:这里写本服务依赖的其他服务,启动本服务时,一般会并行的启动该服务和它所依赖的服务,如果它依赖的服务启动失败了,本服务将无法启动成功。Wants=:这个命令和Requires=类似但是相对宽松一些,即使依赖的服务启动失败了,本服务也可以继续正常启动。一般的依赖都推荐使用这个命令。BindsTo=:和Requires=类似,但是如果依赖的服务停止了,本服务也会停止。Before=和After=:这两个需要和上面描述依赖关系的命令一起使用,表示依赖的当前服务与依赖的服务启动的先后顺序:Before=表示当前服务启动成功后才可以启动依赖服务,After=相反。Conflicts=:这个命令后面跟的服务将不能和当前服务同时运行,如果当前服务运行则会导致该命令列举的服务被停止。Condition...=:这个命令往往和许多其他命令一起使用,用来测试一些条件,比如测试当前的操作系统。如果条件不满足,则跳过当前服务的启动。Assert...=:和Condition...=类似,但是如果条件检测不满足会导致失败。[Install]Install一般是最后一部分,用来描述unit的行为或者是否开机自启动等,是可选的。而且只有可以开机自启的(即可以被enable)的才会有这个Section。一般常用的命令有:WantedBy=:这个命令是最通用的用来指定服务如何被enable,即在哪些target/runlevel下被设置为开机自启动。我们可以通过这个命令来指定服务捡的依赖关系,有点像[Unit]部分的Wants=,但是这个只是辅助性的。当一个unit被enable后,就会在/etc/systemd/system目录下创建以.wants为后缀的目录,比如当前unit文件里面写了WantedBy=multi-user.target,那么enable当前unit后,就会在/etc/systemd/system目录下创建multi-user.target.wants目录,并且将当前unit及其依赖的unit的符号链接放在新创建的目录里面。disable该unit之后,它的软连接及其依赖的unit的软连接都将被删除。RequiredBy=:和WantedBy=类似,但是它指定的依赖条件如果不满足,就会导致服务启动失败。如果enable的话,创建的是.requires结尾的目录。Alias=:给服务创建别名。Also=:将多个unit设置为一个组,可以一起操作。[Service]我们之前说了,unit对象有很多种类型,其中device,target,snapshot,scope这几种类型没有对应该类型的Section,其他的都有,比如service这种类型特有的section就是[Service],也就是本节要介绍的。[Service]有一个必须的命令就是Type,它根据进程的行为将服务分为好多类别,不同的类别管理不是不太一样:simple:这种是最普遍的类型,在启动行(使用ExecStart=指定)指定进程,如果Type=和Busname=没有设置,但是ExecStart=却指定了的话,那默认就是这种类型。forking:这种类别指的是那种fork出来子进程后,父进程就马上退出的情况。这种类型下,父进程退出后,systemd仍然认为进程是OK的。而且可以使用PIDFile=命令来指定存放主子进程pid的文件。Nginx就属于这种类型。oneshot:这种一般用在存活时间不长的一次性任务的进程上,它告诉systemd应该等待进程退出后再接着去处理其它的unit。dbus:这种类别的告诉systemd该unit会在D-Bus上面获取一个名字。notify:这种类别的服务会在启动完之后发出一个消息,systemd必须等到接收到这个消息后才可以接着去处理其它unit。idle:这种类别表示在收到所有任务前,服务都不会运行。OK,上面就是Type=可取得值。下面介绍除Type=以外的其他命令:ExecStart=:用来指定进程文件(必须是绝对路径)和启动参数,一般该命令只能指定一次。有一个特殊的用法就是比如其他文件里面已经设置了,我们现在想在优先级更高的地方覆盖它,就可以先写一行ExecStart=(前面的表示清空之前的设置),然后再在另外一行写上完整的命令ExecStart=***。另外,如果在命令之前加上-的话表示进程如果以非0退出,也不算失败。ExecStartPre=和ExecStartPost=:看名字就看出来了,通过这两个指令可以指定在进程运行前和运行后执行的命令,同样也可以加-,表示接受非0的退出状态。ExecReload=:重新加载服务的配置。ExecStop=:指定停止服务的命令,如果未指定,服务停止后将使用kill来杀掉进程。ExecStopPost=:指定服务停止后运行的命令。RestartSec=:如果服务的自动重启设置了的话,这个命令指定多久重启。Restart=:指定systemd在何种状态下重启服务,可用值有:"always", "on-success", "on-failure", "on-abnormal", "on-abort", "on-watchdog".TimeoutSec=:指定systemd在标记服务失败多久前强制杀掉进程。也可以分别指定TimeoutStartSec=和TimeoutStopSec=。更多systemd和systemctl的信息可以查看Linux man文档。参考:Systemd Essentials: Working with Services, Units, and the JournalUnderstanding Systemd Units and Unit Files Ubuntu开启休眠 http://niyanchun.com/open-hibernate-in-ubuntu.html 2016-09-01T13:29:00+08:00 Ubuntu最新的版本中都去掉了休眠功能,如果我们需要可以自行打开,方法如下。1. 先确定系统是否支持休眠?有的电脑是不支持休眠的,执行sudo pm-hibernate,如果可以休眠,那就说明你的系统支持。否则就是不支持,就不用往下看了。PS:休眠必须要有swap分区才可以,如果你的电脑不支持休眠,请确认一下的确是不支持,还是因为你没有设置swap分区。2. 开启休眠新建文件/etc/polkit-1/localauthority/50-local.d/com.ubuntu.enable-hibernate.pkla(sudo vi /etc/polkit-1/localauthority/50-local.d/com.ubuntu.enable-hibernate.pkla),内容如下:[Re-enable hibernate by default in upower] Identity=unix-user:* Action=org.freedesktop.upower.hibernate ResultActive=yes [Re-enable hibernate by default in logind] Identity=unix-user:* Action=org.freedesktop.login1.hibernate;org.freedesktop.login1.hibernate-multiple-sessions ResultActive=yesOK,重启电脑,就有休眠选项了。Ubuntu 16.04上面测试有效。 让Ubuntu默认从字符界面启动 http://niyanchun.com/boot-ubuntu-in-text-mode.html 2016-06-23T23:21:00+08:00 虽然现在Ubuntu已经是Linux发行版里面桌面版使用非常广泛的OS,但是我们可能依旧有时会有需要它默认启动就是字符界面而不是图形界面的需求,特别是如果你是在虚拟机里面安装,只需要远程ssh登陆的时候。网上有很多针对新老版本设置如何从字符界面的方法,大多数都比较麻烦。但其实对于比较新的版本(具体从哪个版本开始的,我也不知道),只需要改动一个地方,就可以了实现默认从字符界面启动了:修改/etc/default/grub文件,给GRUB_CMDLINE_LINUX_DEFAULT选项的值后面加上text,然后执行以下update-grub 命令就可以了。GRUB_DEFAULT=0 GRUB_HIDDEN_TIMEOUT=0 GRUB_HIDDEN_TIMEOUT_QUIET=true GRUB_TIMEOUT=10 GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian` GRUB_CMDLINE_LINUX_DEFAULT="quiet text" GRUB_CMDLINE_LINUX="find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US"   Linux使用PageUp/PgDown查阅历史命令 http://niyanchun.com/use-page-keys-to-search-history-command.html 2016-06-23T23:13:00+08:00 在公司服务器的SUSE上面使用PageUp和PageDown可以查阅补全历史命令,但是在家里的Ubuntu上面却不可以,本来以为是需要装什么工具,后来才发现是配置默认没有打开。打开方式如下:修改/etc/inputrc文件:默认41、42行显示的内容是注释掉的,去掉前面的注释,然后重新打开终端就可以了。BTW,不知道这么好用的功能为何默认是注释掉的。 Lex和Yacc——Yacc学习 http://niyanchun.com/yacc-learning.html 2016-06-04T14:11:00+08:00 一,Yacc文法介绍在《Lex和Yacc——Lex学习》中我们已经了解了Lex,今天再来了解以下Yacc。看度娘是如何解释Yacc的:yacc(Yet Another Compiler Compiler),是一个经典的生成语法分析器的工具。yacc生成的编译器主要是用C语言写成的语法解析器(Parser),需要与词法解析器Lex一起使用,再把两部份产生出来的C程序一并编译。简单来说,Lex是词法分析器,Yacc是语法分析器,二者结合就和以生成一个编译器。当然Lex和Yacc也都是可以单独使用的。相比于Lex,Yacc稍微复杂一些。Yacc的文法采用BNF(Backus-Naur Form)的变量规则描述。BNF文法最初由John Backus和Peter Naur发明,并且用于描述Algol60语言。BNF能够用于表达上下文无关的语言。现代程序语言大多数结构能够用BNF来描述。我们今天用一个加减乘除的计算器的例子来学习Yacc。比如:1+2/3+4*6-3BNF文法:E = num        规约a      0E = E / E       规约b      1E = E * E       规约c       1E = E + E       规约d      2E = E - E        规约e      2上面第一列是E表达式;第二列是规约名字,方便后面描述;第三列是优先级,数字越小优先级越高。这里像E表达式这样出现在左边的结构叫做非终端符(nonterminal),像num这样的标识符叫做终端符,终端符只出现在右边。终端符号:代表一类在语法结构上等效的标记。终端符号有三种类型:命名标记:由%token标识符来定义。按照惯例,他们都是大写。lexer返回命名标记。字符标记:字符常量的写法与C相同。例如,--就是一个字符标记。字符串标记:写法与C的字符串常量相同。例如,“<<”就是一个字符串标记。非终端符号:是一组非终端符号和终端符号组成的符号。按照惯例,它们都是小写。我们将“1+2/3+4*6-3”逐个字符移进堆栈,如下所示:.1+2/3+4*6-31.+2/3+4*6-3 移进E.+2/3+4*6-3 规约aE+.2/3+4*6-3 移进E+2./3+4*6-3 移进E+E./3+4*6-3 规约aE+E/.3+4*6-3 移进E+E/3.+4*6-3 移进E+E/E.+4*6-3 规约aE+E/E+.4*6-3 移进E+E/E+4.*6-3 移进E+E/E+E.*6-3 规约aE+E/E+E*.6-3 移进E+E/E+E*6.-3 移进E+E/E+E*E.-3 规约aE+E/E+E*E-.3 移进E+E/E+E*E-3. 移进E+E/E+E*E-E. 规约aE+E+E*E-E. 规约bE+E+E-E. 规约cE+E-E. 规约dE-E. 规约dE. 规约e我们在实际运算操作中是把一个表达式逐步简化成一个非终端符。称之为“自底向上”或者“移进归约”分析法。点左面的结构在堆栈中,而点右边的是剩余的输入信息。我们以把标记移入堆栈开始。当堆栈顶部和右式要求的记号匹配时,我们就用左式取代所匹配的标记。概念上,匹配右式的标记被弹出堆栈,而左式被压入堆栈。我们把所匹配的标记认为是一个句柄,而我们所做的就是把句柄左式归约。这个过程一直持续到把所有输入都压入堆栈中,而最终堆栈中只剩下最初的非终端符。在第1步中我们把1压入堆栈中。第2步对应规则a,把1转换成E。然后继续压入和归约,直到第5步。此时堆栈中剩下E+E,按照规则E,可以进行E=E+E的合并,然后输入信息并没有结束,这就产生了“移进-归约”冲突(shift-reduce conflict)。在yacc中产生这种冲突时,会继续移进。在第17步,E+E/E,即可以采用规则d,也可以采用E/E规则b,如果使用E=E+E归约,显然从算法角度是错误的,这就有了运算符优先级的概念。这种情况称为“归约-归约”冲突(reduce-reduce conflict)。此时yacc会采用第一条规则,即E=E/E。二,Yacc语法介绍我们用一个使用Lex和Yacc实现的简单的加减乘除计算器的例子来讲解Yacc语法。先列出代码:lex文件:cal.l%{ #include<stdio.h> void yyerror(char *); #include "cal.tab.h" %} %% [0-9]+ { yylval = atoi(yytext); return INTEGER; } [-+*/n] { return *yytext; } [ t] { ; /* 去除空格 */ } . { yyerror("invalid characters"); } %% int yywrap(void) { return 1; }yacc文件:cal.y%{ #include <stdio.h> #include <stdlib.h> int yylex(void); void yyerror(char *); %} /* 上面%{ %}的代码和Lex一样,一般称为定义段。就是一些头文件声明, * 宏定义、变量定义声明、函数声明之类的。 */ %token INTEGER %left '+' '-' %left '*' '/' %% program: program expr 'n' {printf("%dn", $2); } | ; expr: INTEGER { $$ = $1; } | expr '*' expr { $$ = $1 * $3; } | expr '/' expr { $$ = $1 / $3; } | expr '+' expr { $$ = $1 + $3; } | expr '-' expr { $$ = $1 - $3; } ; %% void yyerror(char *s) { printf("Error: %s n", s); } int main(void) { yyparse(); return 0; }编译运行:allan@NYC:~/test/lex_yacc/cal$ bison -d cal.y allan@NYC:~/test/lex_yacc/cal$ lex cal.l allan@NYC:~/test/lex_yacc/cal$ gcc -o parser *.c allan@NYC:~/test/lex_yacc/cal$ ll total 140 drwxrwxr-x 2 allan allan 4096 6月 4 13:19 ./ drwxrwxr-x 3 allan allan 4096 6月 4 10:25 ../ -rw-rw-r-- 1 allan allan 269 6月 4 10:34 cal.l -rw-rw-r-- 1 allan allan 43946 6月 4 13:19 cal.tab.c -rw-rw-r-- 1 allan allan 2048 6月 4 13:19 cal.tab.h -rw-rw-r-- 1 allan allan 479 6月 4 10:32 cal.y -rw-rw-r-- 1 allan allan 44740 6月 4 13:19 lex.yy.c -rwxrwxr-x 1 allan allan 28808 6月 4 13:19 parser* allan@NYC:~/test/lex_yacc/cal$ ./parser 1+2*5+10/5 13 11 11 1 + 4 * 5 21 allan@NYC:~/test/lex_yacc/cal$这里需要说明以下bison(GUN下面的yacc工具)的-d参数(--defines)。当我们将lex和yacc一起使用的时候,我们一般将变量、宏定义等标记名称定义在yacc里面,然后lex里面取引用。我们使用-d的话,yacc工具将编写额外的输出文件,这个文件里面会包含这些宏定义:愈发中定义的标记类型名称,语义的取值类型YYSTYPE,以及一些外部变量声明。比如上面生成的文件叫"cal.tab.h"。然后我们在lex的源文件里面包含这个头文件,就可以在lex里面使用yacc定义的变量,从而将二者配合使用。用Yacc来创建一个编译器包括四个步骤:通过在语法文件上运行Yacc生成一个解析器。说明语法:编写一个.y的语法文件(同时说明C在这里要进行的动作)。编写一个词法分析器来处理输入并将标记传递给解析器。这个一般使用Lex完成。编写一个函数,通过调用yyparse()来开始解析。编写错误处理例程(如yyerror())。编译Yacc生成的代码以及相关的源文件。将目标文件链接到适当的可执行解析器库。下面我们结合上面的例子来看Yacc的语法规则。Yacc和Lex一样,也包含由“%%”分隔的三个段:定义声明、语法规则、C代码段。1. 定义段和预定义标记部分上面%{ %}的代码和Lex一样,一般称为定义段。就是一些头文件声明,宏定义、变量定义声明、函数声明之类的%}和%%之间的那部分可以看做预定义标记部分。%token INTEGER定义声明了一个标记。党我们编译后,会在cal.tab.c中生成一个解析器,同时会在cal.tab.h产生包含信息:enum yytokentype { INTEGER = 258 };其中0-255之间的标记值约定为字符值,是系统保留的后定义的token。cal.tab.h里面其他部分是默认生成的,与token INTEGER无关。lex文件需要包含这两个头文件,并且使用其中对标记值的定义。为了获得标记,yacc会调用yylex。yylex的返回值类型是整型,可以用于返回标记。而在yylval变量中保存这与返回的标记相对应的值。yacc在内部维护这两个堆栈:一个分析栈和一个内容栈。分析站中保存着终端符和非终端符,并且记录了当前解析状态。而内容栈是一个YYSTYPE类型的元素数组,对于分析栈中的每一个元素都保存这一个对应的值。例如,当yylex返回一个INTEGER标记时,把这个标记移入分析栈。同时,相应的yacc值将会被移入内容栈中。分析栈和内容栈的内容总是同步的,因此从栈中找到对应的标记值是很容易的。比如lex文件中下面的这一段:[0-9]+ { yylval = atoi(yytext); return INTEGER; }这是将整数的值保存在yylval中,同时向yacc返回标记INTEGER。即内容栈存在了整数的值,对应的分析栈就为INTEGER标记了。yylval类型由YYSTYPE决定,由于他的默认类型是整型,所以在这个例子中运行正常。再看下面的:%left '+' '-' %left '*' '/'%left表示左结合,%right表示右结合。最后列出的定义拥有最高的优先权。因此乘法和除法拥有比加法和减法更高的优先权。+-*/所有这四个算术符都是左结合的。运用这个简单的技术,我们可以消除文法的歧义。关于结合性,各运算符的结合性分为两种,即左结合性(自左至右)和右结合性(自右至左)。例如算术运算符的结合性是自左至右,即先左后右。如有表达式x-y+z则y应先与“-”号结合, 执行x-y运算,然后再执行+z的运算。这种自左至右的结合方向就称为“左结合性”。而自右至左的结合方向称为“右结合性”。 最典型的右结合性运算符是赋值运算符。如x=y=z,由于“=”的右结合性,应先执行y=z再执行x=(y=z)运算。2. 规则部分%% program: program expr 'n' {printf("%dn", $2); } | ; expr: INTEGER { $$ = $1; } | expr '*' expr { $$ = $1 * $3; } | expr '/' expr { $$ = $1 / $3; } | expr '+' expr { $$ = $1 + $3; } | expr '-' expr { $$ = $1 - $3; } ; %%要理解这个规则,关键一点就是要理解yacc的递归解析方式。program和expr是规则标记,但是作为一个整体描述表达式。先看expr,可以由单个INTEGER值组成,也可以由多个INTEGER和运算符组合组成。以表达式“1+4/23-0”为例:1 4 2 3 0都是expr,就是expr+expr/exprexpr-expr,说到底最后还是个expr。递归思想正好与之相反,逆推下去会发现expr这个规则标记能表示所有的数值运算表达式。再看program,首先program可以为空,也可以用单单的expr加上"n"组成,结合起来看program定义的就是多个表达式组成的文件内容。比如我们创建一个input文件,文件内容及运行结果如下:allan@NYC:~/test/lex_yacc/cal$ cat input 1+5/5+4*5 3+9+2*10-9 2/2 3-9 allan@NYC:~/test/lex_yacc/cal$ ./parser < input 22 23 1 -6 allan@NYC:~/test/lex_yacc/cal$最后,我们来看lex是如何在程序中运作的。以expr: expr '+' expr { $$ = $1 + $3; }为例:在分析栈中我们其实用左式替代了右式。在本例中,我们弹出“expr '+' expr”,然后压入“expr”。我们通过弹出三个成员,压入一个成员来缩小堆栈。在我们的代码中可以看到用相对地址访问内容栈中的值。如$1,$2,这样都是yacc预定义可以直接使用的标。“$1”代表右式中的第一个成员,“$2”代表第二个,后面的以此类推。“$$”表示缩小后的堆栈顶部。在上面的动作中,把对应两个表达式的值相加,弹出内容栈中的三个成员,然后把得到的和压入堆栈中。这样,保持分析栈和内容栈中的内容依然同步。而下面这个定义说明每当一行表达式结束时,打印第二个栈值,即expr的值,完成字符运算。program: program expr 'n' { printf("%dn", $2); }3. C代码部分该部分是函数部分。当Yacc解析出错时,会调用yyerror(),用户可自定义函数的实现。main函数调用了yacc解析的入口函数yyparse()(相当于Lex的yylex())。本文整理自网络。Lex和Yacc是编译器方面的一门比较深的学问,对于不做编译器方面研究的人或许没有太深研究的必要,但是我觉得了解一下这种方式对于我们去理解编译器的行为还是非常有好处的。 网易云音乐推出Linux版了 http://niyanchun.com/netease-cloud-music-in-linux.html 2016-05-28T19:28:00+08:00 今天偶然得知网易云音乐竟然推出Linux版本了,二话不说,赶紧下载安装。 一直以来,Linux下面都没有一个非常完美的在线音乐客户端,但现在有了。说实话之前从来没有用过网易云音乐,但因为它推出了Linux客户端,马上转用网易云音乐。支持。功能就不介绍了,很轻大,很美观。想想五年前使用Linux作为日常OS的时候,真的缺少很多软件。现在,金山快盘、为知笔记、有道词典、搜狗输入法、网易云音乐相继推出,现在Linux真的是集开发和娱乐于一身的OS的。 Linux进程控制之wait类函数 http://niyanchun.com/wait-in-linux.html 2016-05-09T23:10:00+08:00 我们已经知道,fork出来的子进程和父进程谁先运行是随机的,那我们如果控制呢?可以使用wait类函数。#include <sys/wait.h> pid_t wait(int *stat_loc); pid_t waitpid(pid_t pid, int *stat_loc, int options); 若成功则返回进程pid,0,若出错返回-1进程调用上面的两个函数可能会:如果其所有子进程都还在运行,则阻塞。如果有一个子进程已经终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。如果它没有任何子进程,则立即出错返回。两个函数的区别如下:在一个子进程终止前,wait会使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。waitpid并不等待在其调用后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。如果一个子进程已经终止,并且是一个僵尸进程(zombie:一个已经终止,但是其父进程尚未对其进行善后处理的进程),则wait立即返回并获取该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如果调用者阻塞而且它有多个子进程,则在其任一个子进程终止时,wait就立即返回。因为wait返回终止子进程的PID,所以它总能了解到是哪个子进程终止了。这两个函数的参数stat_loc是一个整形指针。如果不关心终止状态,则可以将该参数设置为NULL。如果stat_loc不是NULL,则终止进程的终止状态就存放在它所指向的单元内。那我们如果根据这个整形状态字获取进程的退出状态呢?POSIX.1在<sys/wait.h>中提供了几个互斥的宏来查看:WIFEXITED(stat_loc):若为正常终止子进程返回的状态,则为真。对于这种情况可以执行WEXITSTATUS(stat_loc),取子进程传给exit/_exit/_Exit的低八位。WIFSIGNALED(stat_loc):若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号),对于这种情况,可使用WTERMSIG(stat_loc)获取子进程终止的信号编号。另外,有些实现定义宏WCOREDUMP(stat_loc),若已产生core文件,则返回真。WIFSTOPPED(stat_loc):若为当前暂停子进程返回的状态,则为真。对于这种情况,可使用WSTOPSIG(stat_loc)获取使子进程暂停的信号编号。WIFCONTINUED(stat_loc):若在作业控制暂停后已经继续的子进程返回了状态,则为真。可以看到,wait无法控制等待哪一子进程,但是waitpid却可以等待某一个特定的子进程。对于waitpid里面的pid参数有如下解释:pid == -1:等待任一子进程,此时和wait等效。pid >= 0:等待进程ID与pid相等的子进程。这是我们最常使用的情况。pid == 0:等待其组ID等于调用进程组ID的任一子进程。pid <= -1 :等待其组ID等于pid绝对值的任一子进程。waitpid里面的options参数使我们可以更进一步的控制waitpid的操作。此参数可以为0,或者是下面的三个常量按位或的结果:WCNTINUED:若实现支持作业控制,那么由pid指定的任一子进程在暂停后已经继续,但其状态尚未报告,则返回其状态(POSIX.1的XSI扩展)。WNOHANG:若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0.WUNTRACED:若某实现支持作业控制,而由pid指定的任一子进程已处于暂停状态,并且其状态自暂停以来还未报告过,则返回其状态WIFSTOPPED宏确定返回值是否对应于一个暂停子进程。除了wait和waitpid外,还有一些不太常用的扩展函数:int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options); pid_t wait3(int *status, int options, struct rusage *rusage); pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);这些函数这里就不介绍了,有兴趣的可以查看man文档。最后我们看个例子:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> void pr_exit(int status) { if (WIFEXITED(status)) printf ( "normal termination, exit status = %dn", WEXITSTATUS(status) ); else if (WIFSIGNALED(status)) printf ( "abnormal termination, signal number = %d(%s)n", WTERMSIG(status),strsignal(WTERMSIG(status))); else if (WIFSTOPPED(status)) printf ( "child stopped, signal number = %d(%s)n", WSTOPSIG(status), strsignal(WSTOPSIG(status)) ); } int main(void) { pid_t pid; int status; if ((pid = fork()) < 0) exit(1); else if (pid == 0) exit(7); /* child */ if (wait(&status) != pid) { printf ( "wait errorn" ); exit(2); } pr_exit(status); if ((pid = fork()) < 0) exit(3); else if (pid == 0) abort(); /* generate SIGABRT */ if (wait(&status) != pid) { printf ( "wait errorn" ); exit(2); } pr_exit(status); if ((pid = fork()) < 0) exit(4); else if (pid == 0) status /= 0; /* generate SIGFPE */ if (wait(&status) != pid) { printf ( "wait errorn" ); exit(2); } pr_exit(status); exit(0); }程序执行结果:allan@NYC:~/test$ ./wait_test1 normal termination, exit status = 7 abnormal termination, signal number = 6(Aborted) abnormal termination, signal number = 8(Floating point exception) Linux进程控制之exec族函数 http://niyanchun.com/exec-functions-in-linux.html 2016-05-08T15:07:00+08:00 exec族函数是Linux进程控制原语中非常重要的一部分,往往和fork/vfork配合使用。当一个进程调用一种exec函数时,该进程执行的程序完全替换未新程序,而新程序则从其main函数开始执行(原程序后续部分不会再执行了)。但是调用exec并不创建新进程(不同于fork和vfork),所以调用exec前后进程ID并未改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈。下面是exec族的函数:#include <unistd.h> extern char **environ; int execl(const char *path, const char *arg, ... /* (char *) NULL */); int execlp(const char *file, const char *arg, ... /* (char *) NULL */); int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */); int execv(const char *path, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]); int execvp(const char *file, char *const argv[]); 函数失败返回-1,成功则不返回。这个家族挺大的,挺难记忆的。我们可以稍微总结一下特征:特点1. 以p结尾的两个函数(execlp,execvp)取文件名作为第一个参数,其它几个取路径名作为参数。这里的文件名可以带绝对路径,也可以不带。这样的话,程序将会取PATH环境变量里面找可执行文件。而其它几个不以p结尾只能使用绝对路径。 程序1:使用不以p结尾的,且不使用路径名作为参数:#include <stdio.h> #include <unistd.h> #include <errno.h> int main() { printf ( "begin ...n" ); /* 使用不带p版本的 */ if (execl("ls", "ls", "-rt", NULL) == -1) { perror("execl error"); } printf ( "end ...n" ); return 0; }执行结果:allan@NYC:~/test$ ./exec_test begin ... execl error: No such file or directory end ...程序2:将程序1改为使用以p结尾的#include <stdio.h> #include <unistd.h> #include <errno.h> int main() { printf ( "begin ...n" ); if (execlp("ls", "ls", "-rt", NULL) == -1) { perror("execl error"); } printf ( "end ...n" ); return 0; }执行结果:allan@NYC:~/test$ ./exec_test begin ... apue.c bug.c test1 test1.out test.dat file.c file c-7.c c-7 bug_exp exec_test.c exec_test可以看到,程序2可以正常执行。要注意的是,后面的printf并没有执行到。因为exec替换了原有的进程映像。所以,对于以p结尾的这两个exec函数,我们使用的时候,可以不用指定可执行文件(如果不是ld产生的二进制文件,exec就会尝试用/bin/sh取执行,即把它当做一个shell脚本取执行)的路径,让系统取PATH里面查找。当然也可以指定,但如果指定的话,就一定要指定对,因为系统只会去指定的路径查找,不会再去PATH指定的路径找了。举个例子,ls名字的完整路径是/bin/ls,那么我们可以写成execlp("ls", "ls", "-rt", NULL) ,或者execlp("/bin/ls", "ls", "-rt", NULL),但不能写成execlp("/usr/bin/ls", "ls", "-rt", NULL)。而对于不带p后缀的,第一个参数只能写成"/bin/ls"。即,带不带p决定exec族函数的第一个参数如何写——如果带p,可以不用写正确的绝对路径;如果不带,就必须写正确的绝对路径。特点2. 参数中的l(list)和v(vector)决定其它参数如何传递(l和v是互斥的,只会包含其中之一)简单说就是,包含l的(execl,execlv,execlp),将要执行的可执行文件的参数直接写到exec函数参数里面;包含v的(execv,execve,execvp),必须先将要执行的可执行文件的参数写到一个字符串数组里面,然后把这个数组作为exec的参数。需要注意的是,包含l的,最后一个参数必须是(char *)0,当然,就是C里面的NULL。程序2就是包含l的例子,我们举一个包含v的例子,程序3:包含v的#include <stdio.h> #include <unistd.h> #include <errno.h> char* argv[] = { "ls", "-rt" }; int main() { printf ( "begin ...n" ); if (execv("/bin/ls", argv) == -1) { perror("exec error"); } printf ( "end ...n" ); return 0; }执行结果:allan@NYC:~/test$ ./exec_test begin ... apue.c bug.c test1 test1.out test.dat file.c file c-7.c c-7 bug_exp exec_test.c exec_test特点3:以e结尾的exec函数(execle,execve)可以(选择性的)传递一个指针数组,该指针数组是一些环境变量的定义。其它的不以e结尾的则使用调用进程总的environ变量(一个系统预定义的全局变量)为新程序复制现有的环境。 下面我们举一个例子来看一下以e结尾exec函数和其它的区别:程序4:echoall程序,打印命令行参数以及环境变量,在后面的程序中会使用#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char *argv[]) { char **ptr; extern char ** environ; for (int i = 0; i < argc; i++) { printf ( "argv[%d]: %sn", i, argv[i] ); } for (ptr = environ; *ptr != 0; ptr++) { printf ( "%sn", *ptr ); } exit(0); }程序5:#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/wait.h> char *env_init[] = { "USER=known", "PATH=/tmp", NULL }; int main() { pid_t pid; if ((pid = fork()) < 0) { printf ( "fork errorn" ); exit(1); } else if (pid == 0) { if (execle("/home/allan/test/echoall", "echoall", "myarg1", "MY_ARG2", NULL, env_init) < 0) { perror ( "execle error" ); exit(2); } } if (waitpid(pid, NULL, 0) < 0) { perror("wait error"); exit(3); } if ((pid = fork()) < 0) { printf ( "fork error 2n" ); exit(4); } else if (pid == 0) { if (execlp("/home/allan/test/echoall", "echoall", "only 1 arg", NULL) < 0) { perror("execlp error"); exit(5); } } exit(0); }程序5运行结果:allan@NYC:~/test$ ./exec_test argv[0]: echoall argv[1]: myarg1 argv[2]: MY_ARG2 USER=known PATH=/tmp allan@NYC:~/test$ argv[0]: echoall argv[1]: only 1 arg XDG_VTNR=7 LC_PAPER=zh_CN.UTF-8 /* 中间省略 */ GTK_IM_MODULE=fcitx LESSCLOSE=/usr/bin/lesspipe %s %s LC_TIME=zh_CN.UTF-8 LC_NAME=zh_CN.UTF-8 XAUTHORITY=/home/allan/.Xauthority _=./exec_test可以看到,对于带e的,使用了我们指定的环境变量,不带e的继承了父进程的环境变量(即系统默认的环境变量)。再强调一点:对于带e的exec函数,我们也可以不传递最后一个envp这个参数,这个和传一个空的envp指针数组是一样的,只是编译的时候会报一个WARNING。当然,这样做的后果是我们新的进程映像就没有任何环境变量了。所以,如果不是要为新的进程指定某个特定的环境(变量),就不要使用带e的exec函数。最后,我们再说两个知识点:虽然exec族有很多函数,但具体的实现中往往只有execve是内核的系统调用,其它的都是库函数,它们最终都要调用execve系统调用。exec族函数中都有一个arg0参数,习惯上指定为要执行的文件名,但其实没有什么用。程序并不使用这个参数。 看下面两个例子:程序6:#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/wait.h> int main() { if (execlp("ls", "abc", NULL) < 0) { perror("execle error"); exit(1); } exit(0); }程序7:#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/wait.h> char *argv[] = { "abc" , NULL}; int main() { if (execvp("ls", argv) < 0) { perror("execle error"); exit(1); } exit(0); }上面程序6和程序7中的arg0参数我们都指定为了"abc",但它们都可以正常运行。当然,我们还是遵从习惯,指定为可执行文件名比较好。至此,exec家族就算介绍完了。 Linux进程控制之fork和vfork http://niyanchun.com/fork-and-vfork.html 2016-05-07T11:17:00+08:00 一,fork和vfork#include <sys/types.h> #include <unistd.h> pid_t fork(void); pid_t vfork(void);fork用于创建一个进程,该函数调用一次,返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值是新子进程的进程号。出错时,返回-1。子进程是父进程的一个副本——子进程获得父进程的数据空间、堆和栈的副本(不是共享存储空间),但是父子进程共享正文段,因为这一部分一般都是只读的。fork出来的子进程和父进程谁先执行是不确定的。vfork和fork一样,也用于创建一个新的进程。但是它和fork有两个区别:vfork保证子进程先运行,在子进程调用exec或者exit之后父进程才会被调度运行。vfork一般要和exec族函数配合使用。因为vfork创建的进程并不复制父进程的地址空间,它直接在父进程的进程空间里面运行,直到调用exit或者exec族函数。这也就是为什么vfork会保证子进程会先运行,因为二者共享一个进程空间,同时运行就会有问题。下面我们分析几个有趣的程序:例程1:#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> int glob = 6; char buf[] = "a write to stdoutn"; int main() { int var; pid_t pid; var = 88; if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf) -1) { printf ( "write errorn" ); return 1; } printf ( "before forkn" ); if ((pid = fork()) < 0) { printf ( "fork errorn" ); return 1; } else if (pid == 0) { glob++; var++; } else { sleep(2); // 让子进程先运行,但这样并不能完全保证 } printf ( "pid = %d, glob = %d, var = %dn", getpid(), glob, var); exit(0); }程序执行结果:allan@NYC:~/test$ ./test1 a write to stdout before fork pid = 16752, glob = 7, var = 89 pid = 16751, glob = 6, var = 88 allan@NYC:~/test$ ./test1 > test1.out allan@NYC:~/test$ cat test1.out a write to stdout before fork pid = 16778, glob = 7, var = 89 before fork pid = 16777, glob = 6, var = 88从结果我们可以看出:子进程中堆glob和var的改变并未影响到父进程中glob和var的值,因为二者不在一个进程空间中。有趣的是同一个程序,两种运行方式得到的结果不一样,第二次的执行多输出了一个“before fork”。这里是和I/O函数有关系。write函数本身是不带缓冲区的(见之前《Linux文件I/O》),因为在fork之前调用write,所以其数据写到标准输出一次。但是,标准I/O库是带缓冲的。如果标准输出连到终端设备,则它是行缓冲的,否则它是全缓冲的。当以交互方式运行该程序时,只得到该printf输出的行一次,其原因是标准输出缓冲区由换行符冲洗。但是当标准输出重定向到一个文件时,去得到printf输出行两次。其原因是在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中,然后在将父进程数据空间复制到子进程中时,该缓冲区也被复制到子进程中。于是,那时父子进程各自有了带该行内容的标准I/O缓冲区。在exit之前的第二个printf将其数据添加到现有的缓冲区中。当每个进程终止时,最终会冲洗其缓冲区中的副本。例程2:#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> int glob = 6; int main(void) { int var; pid_t pid; var = 88; printf ( "before vforkn" ); if ((pid = vfork()) < 0) { printf ( "vfork errorn" ); _exit(1); } else if (pid == 0) { glob++; var++; _exit(0); } /* parent continue here */ printf ( "pid = %d, glob = %d, var = %dn", getpid(), glob, var ); exit(0); }程序运行结果:allan@NYC:~/test$ ./c-7 before vfork pid = 7589, glob = 7, var = 89可以看到,使用vfork后,子进程里面对glob和var进行的加1操作,影响到了父进程里面glob和var的值(最后的打印是父进程打印的,子进程已经调用_exit退出了)。这就是之前说的,vfork出来的进程并不会复制父进程的进程空间,而是直接在父进程的进程空间去运行的。例程3:#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> static void f1(void), f2(void); int main(void) { f1(); f2(); _exit(0); } static void f1(void) { pid_t pid; if ((pid = vfork()) < 0) { printf ( "vfork errorn" ); return; } /* child and parent both return */ } static void f2(void) { char buf[1000]; int i; for (i = 0; i < sizeof(buf); i++) buf[i] = 0; }程序运行:allan@NYC:~/test$ ./bug_exp 段错误可见这个程序是无法正常运行的。我们知道每个函数调用的栈帧是存储在栈里面的,当函数f1调用vfork时,父进程的栈指针指向f1函数的栈帧。然后vfork后,子进程先执行然后从f1返回,接着子进程调用f2,并且f2的栈帧覆盖了f1的栈帧,在f2中子进程将自动变量buf的值置为0,即将栈中的1000个字节的值都置为了0.从f2返回后子进程调用_exit,这时栈中main以下的内容已经被f2修改了。然后,父进程从vfork调用后继续,并从f1返回。返回信息虽然保存于栈中,但是多半可能已经被子进程修改了(这依赖于系统对于栈的实现,比如返回信息保存在栈中的具体位置,修改动态变量时覆盖了哪些信息等)。不过这些都是因为父进程和子进程运行在同一个进程空间导致的,虽然有先后顺序,但是还是很危险。所以,vfork一般都是和exec族函数配合使用的。上面是一些进程变量在父子进程中的变化关系,下面我们看一下文件共享方面父子进程的关系。二,父子进程的文件共享关于Linux的文件共享之前已经在《Linux文件I/O》中介绍过。利用fork产生一个子进程的时候,父进程所有打开的文件描述符都会被复制到子进程中。父子进程的每个相同的文件描述符共享一个文件表项。假设几个进程具有三个不同的打开文件,它们是标准输入、标准输出、标准出错。从fork返回时,我们就有下图所示的结构:这种共享文件的方式使父子进程堆同一文件使用了一个文件偏移量。考虑下述情况:一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父子进程都向标准输出进行写操作。如果父进程的标准输出已重定向,那么子进程写到该标准输出时,他讲更新与父进程共享的该文件的偏移量。在我们所考虑的例子中,当父进程等待子进程的时候,子进程写到标准输出;而在子进程终止后,父进程也写到标准输出上,并且知道其输出会添加在子进程所写的数据后。如果父子进程不共享同一文件偏移量,这种形式的交互就很难做到。如果父子进程写到同一文件描述符,但又没有任何形式的同步(例如上面的使父进程等待子进程),那么它们的输出就会相互混合(假定所用的文件描述符是fork之前打开的)。所以,一般fork之后,处理文件描述符有两种常见的方式:父进程等待子进程完成。在这种情况下,父进程无需堆其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已执行了相应更新。父子进程各执行不同的程序段。在这种情况下,在fork之后,父子进程各自关闭它们需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。 Ubuntu 16.04安装有道词典 http://niyanchun.com/install-youdaodict-in-xenial.html 2016-05-04T20:21:00+08:00 注:如果自己不会操作,直接到文章后面下载我编译好的。从Ubuntu 14.04升级到16.04以后,有道词典就安装不上了。因为官方的deb包(Ubuntu版本的)依赖gstreamer0.10-plugins-ugly,但是该软件在16.04里面已经没有了。但其实没有该包,完全不影响有道词典的使用。所以我们可以去掉deb包里面对于该库的依赖。具体操作如下:从官方下载Ubuntu版本的deb包:youdao-dict_1.1.0-0-ubuntu_amd64.deb;创建youdao目录,把该deb包解压到youdao目录:dpkg -X ./youdao-dict_1.1.0-0-ubuntu_amd64.deb youdao;解压deb包中的control信息(包的依赖就写在这个文件里面):dpkg -e ./youdao-dict_1.1.0-0-ubuntu_amd64.deb youdao/DEBIAN;编辑control文件,删除Depends里面的gstreamer0.10-plugins-ugly。创建youdaobuild目录,重新打包:dpkg-deb -b youdao youdaobuild/.这样,在youdaobuild里面就会生成一个新的deb包。我们安装这个包就不会存在依赖的问题了。Ubuntu 16.04之前的版本安装有道词典linux版本可参见:Ubuntu中使用有道词典。编译好的deb包下载:链接:https://pan.baidu.com/s/1uKI64Vw62kVJmMVhXyDWLg 提取码:jik6