lxc1121 2023-06-15T10:21:33+00:00 kejinlu@gmail.com Test 2016-10-11T00:00:00+00:00 lxc1121 http://lxc1121.github.io/2016/10/test vimrc 2016-09-29T00:00:00+00:00 lxc1121 http://lxc1121.github.io/2016/09/vimrc
set nocompatible              "关闭vi兼容
call pathogen#infect()
syntax on

map <F6> :call Setmouse()<CR>
map <F4> :TagbarToggle<CR>
map <F2> :NERDTreeToggle<CR>
map <F5> :MarkdownPreview default<CR>
func Setmouse()
if &mouse=="a"
	set mouse=
else
	set mouse=a
endif
endfunc

syntax enable
set background=dark
colorscheme solarized

set nu cuc cul

if &term =~ "xterm"
	" 256 colors"
	let &t_Co = 256
	" restore screen after quitting
	let &t_ti = "\<Esc>7\<Esc>[r\<Esc>[?47h"
	let &t_te = "\<Esc>[?47l\<Esc>8"
	if has("terminfo")
		let &t_Sf = "\<Esc>[3%p1%dm"
		let &t_Sb = "\<Esc>[4%p1%dm"
	else
		let &t_Sf = "\<Esc>[3%dm"
		let &t_Sb = "\<Esc>[4%dm"
	endif
endif

"打开文件是默认不折叠代码"
set foldlevelstart=99
set foldenable              " 开始折叠
set foldmethod=syntax       " 设置语法折叠
set foldcolumn=0            " 设置折叠区域的宽度
setlocal foldlevel=1        " 设置折叠层数为
set foldlevelstart=99       " 打开文件是默认不折叠代码

set foldclose=all          " 设置为自动关闭折叠
nnoremap <space> @=((foldclosed(line('.')) < 0) ? 'zc' : 'zo')<CR>
                            " 用空格键来开关折叠
set smartindent
"Paste toggle - when pasting something in, don't indent.
set pastetoggle=<F3>
"自动补全括号
inoremap ( ()<ESC>i
inoremap [ []<ESC>i
inoremap { {}<ESC>i
inoremap < <><ESC>i

#一篇文章 http://lifeofzjs.com/blog/2015/05/16/how-to-write-a-server/

]]>
rsyslog+PostgreSQL 2016-08-31T00:00:00+00:00 lxc1121 http://lxc1121.github.io/2016/08/postgresql PostgreSQL 安装
yum install postgresql postgresql-server

正常情况下,安装完成后,PostgreSQL服务器会自动在本机的5432端口开启。 初次安装后,默认生成一个名为postgres的数据库和一个名为postgres的数据库用户。

这里需要注意的是,同时还生成了一个名为postgres的Linux系统用户。

创建用户

通过命令su - postgres进入postgresql管理账户

psql 			/*登录PostgreSQL控制台,相当于命令psql -U postgres -d postgres*/

psql 默认使用当前登录用户名登陆同名数据库,意思是如果当是root用户,那么psql=psql -U root -d root

\password postgres 	/*为用户postgres设置密码,可以不*/
create role rsyslog	/*创建用户rsyslog*/
\password rsyslog 	/*为用户rsyslog设置密码,123456*/

创建数据库

create database "Syslog" owner=rsyslog /**数据库名是大写字母必须要用双引号括起来**/
grant all on database "Syslog" to rsyslog /**为用户rsyslog授权**/

数据库创建完成后\q退出,执行以下命令测试

psql -U rsyslog -d Syslog	/*以用户rsyslog登录并连接到Syslog数据库*/

理论上能正常登陆数据库,\l命令查看数据库Syslog的授权情况

配置rsyslog 在数据库中的表项

  • yum install rsyslog-pgsql 安装rsyslog的postgresql驱动模块
  • 编辑文件/usr/share/doc/rsyslog-7.4.7/pgsql-createDB.sql注释第一行
  • psql -U rsyslog -d Syslog < /usr/share/doc/rsyslog-7.4.7/pgsql-createDB.sql 导入表到数据库Syslog
  • 通过psql -U rsyslog -d Syslog登录数据库,\dt 查看表,owner应该是rsyslog,如不是则需管理员手动修改

配置rsyslog

编辑 /etc/rsyslog.conf文件

$ModLoad ompgsql 	#载入psql驱动模块
#*.*	   :ommysql:localhost,Syslog,rsyslog,123456
*.*        :ompgsql:127.0.0.1,Syslog,rsyslog,123456 

#mail.*     :ommysql:Syslog,rsyslog,123456
# Provides UDP syslog reception
$ModLoad imudp
$UDPServerRun 514	#开启udp端口监听,意思是通过udp端口接收客户端发过来的日志

# Provides TCP syslog reception
$ModLoad imtcp
$InputTCPServerRun 514  #开启tcp端口监听

主意#号后是注释,可以不写

检查效果

  1. 重启 rsyslog服务
  2. psql -U rsyslog -d Syslog 登录postgresql数据库
  3. select * from systemevents;

如能查询到数据库中有日志产生,则配置成功

配置LOGANALYZER

  • LOGANALYZER是用php写的一个web日志分析工具
  • 要使用postgresql,需要安装postgre的php扩展

    yum install php-pgsql php-common

##开始安装LOGANALYZER

下载loganalyzer-‘version’.tar.gz并解压缩** LOGANALYZER的安装实际上是配置,在config.php中配置数据源,用户账户等。 因此config.php需要可写,如不可写,则需要配置selinux和firewall。

rsync -a loganalyzer-'version'/src/ /var/www/html/{"your web app directry"}
cd /var/www/html/{"your web app directry"}
touch config.php
chmod 666 config.php

打开安装的浏览器,地址栏输入localhost/loganalyzer/install.php开始安装 step1、step2点击next跳过即可 LOGANALYZER不支持在postgre上创建登录用户,所以step3中默认配置,点击next直接到step7 Source Type中选择数据源,这里选Database(PDO) Database Storage Engine选择PostgreSQL 其他配置主机=localhost,数据库名=Syslog,数据库用户=rsyslog,密码123456,数据库表SystemEvents

配置完成后点下一步、finish。完成配置后自动跳转到index页面,此时已经可以看到日志信息了

]]>
静态库、动态链接库 2016-08-15T00:00:00+00:00 lxc1121 http://lxc1121.github.io/2016/08/sharelib 使用程序库

静态库

  • 存档文件( archive),也被称为静态库( static library),是一个存储了多个对象文件(object file)的单一文件。(与 Windows 系统的 .LIB 文件基本相当。)

  • ar 命令创建存档文件。传统上,存档文件使用 .a 后缀名,以便与 .o 的对象文件区分开。下面的命令可以将 test1.o 和 test2.o 合并成一个 libtest.a

    % ar cr libtest.a test1.o test2.o

上面的命令cr选项通知ar创建静态库。 传统上静态库使用.a后缀名,区别与.o对象文件

你可以通过为gcc或g++指定 –ltest参数将程序链接到这个库,当连接器在命令行参数中获取到一个静态库文件时,它将在其中搜索被引用但未定义的符号的定义。定义了这些符号的文件将被提取,连接到新程序执行文件中。

通常将静态库当作参数放在命令行参数最后最有意义。

例如test.c

int f ()
{
	return 3;
}
gcc -c test.c 
ar cr libtest.a test.o 其他对象文件(xx.o)\
int main ()
{
	return f ();
} **在程序中使用静态连接库**

gcc –o app app.o –L. –ltest

共享库

又叫动态链接库、共享对象

在某种程度上与由一组对象文件生成的打包文件相当类似。不过,两者之间的区别也是非常明显的。最本质的区别在于,当一个共享库被链接到程序中的时候,程序本身并不会包含共享库中出现的代码。

程序仅包含一个对共享库的引用

第二个重要的区别在于,共享库不仅仅是对象文件的简单组合。当使用的时候,链接器会从中寻找需要的部分进行链接,以匹配未定义的符号引用。而当生成共享库的时候,所有对象文件被合成为一个单独的对象文件,从而使链接到这个库的程序总能包含库中的全部代码,而不仅仅是所需要的部分。 要创建一个共享库,你必须在编译那些用于生成共享库的对象时为编译器指定 –fPIC 选项。

gcc -c fPIC test.c

这里的 –fPIC 选项会通知编译器你要将得到的 test1.o 作为共享库的一部分。然后你将得到的对象文件合并成一个共享库:

gcc –shared –fPIC –o libtest.so test1.o test2.o

共享库通常使用.so作为后缀名。文件名以lib开头,这和静态库相同,在程序中使用共享库的方法也和静态库相同,

使用LD_LIBRARY_PAATH

当你将一个程序与共享库进行动态链接时,链接器不会将动态链接库的完整路径加入到执行文件中,而是只记录了动态链接库的名字,当程序实际运行时,系统会搜索并加载这个共享库默认情况下,系统会搜索并加载这个共享库,默认情况下,系统只搜索/lib/和/usr/lib。如果共享库的位置在这两者之外,系统将无法找到这个共享库。 一种解决方法是:在链接的时候指明-Wl,-rpath参数。假设你用下面的命令进行链接:

gcc -o app app.c -L. -ltest -Wl,-rpath, /tmp/path

系统会在/tmp/path/目录下搜索所需库文件。(命令行参数的’-‘切记是英文输入模式下的而非‘-’)

另一种解决方法是在运行程序时设置LD_LIBRARY_PATH环境变量。LD_LIBRARY_PATH环境变量是一组冒号分割的路径。程序运行时,系统会先搜索LD_LIBRARY_PATH所在路径

gcc -o app app.c -L. -ltest
LD_LIBRARY_PATH=/tmp/path
export LD_LIBRARY_PAATH
$ ./app 

ldd 程序名 显示与一个程序建立了动态链接的库的列表

]]>
noVNC的使用之一 2016-07-16T00:00:00+00:00 lxc1121 http://lxc1121.github.io/2016/07/noVNC的使用之一

noVNC是一个HTML5 VNC客户端,采用HTML5 websockets、Canvas和JavaScript实现,noVNC被普遍应用于各大云计算、虚拟机控制面板中,比如OpenStack Dashboard 和 OpenNebula Sunstone 都用的是 noVNC. 前面说了 noVNC 采用 WebSockets 实现,但是目前大多数 VNC 服务器都不支持 WebSockets,所以 noVNC 是不能直接连接 VNC 服务器的,怎么办呢?这就需要一个代理来实现websockets和tcp sockets之间的转换,这个代理就是websockify。

vncserver安装和使用

vncserver是用来实现远程桌面连接的,比如说你由两台机器PC1:192.168.1.101和PC2:192.168.1.102,如果你想在PC1上访问PC2的桌面,就需要在PC2上安装vncserver,然后在PC1上通过vncviewer或noVNC访问PC2。下面以tigervnc-server为例来介绍一下vncserver的安装和使用。

安装

yum -y install tigervnc-server

安装完后,查看vncserver的配置文件:

[root@mycentos liushaolin]# rpm -qc tigervnc-server
/etc/sysconfig/vncservers

在该配置文件中可以修改vncserver的配置,比如远程桌面的sessionnumber,登录时的用户名,屏幕分辨率等等。

启动sncserver

vncserver
或
vncserver :n

这里的n就是sessionnumber,不指定的话默认为1,第一次启动时会提示输入密码,以后也可以使用vncpasswd命令修改密码。VNC的默认端口号是5900,而远程桌面连接端口号则是5900+n。如果使用“vncserver :1”命令启动VNC Server,那么端口应该是5901。

查看连接

vncserver -list[root@mycentos liushaolin]# vncserver -list

TigerVNC server sessions:

X DISPLAY #	PROCESS ID
:1		5918
:3		7726

删除连接

vncserver -kill :n

使用noVNC连接VNC server

noVNC的工作原理

noVNC提供一种在网页上通过html5的Canvas,访问机器上vncserver提供的vnc服务,需要做tcp到websocket的转化,才能在html5中显示出来。网页就是一个客户端,类似win下面的vncviewer,只是此时填的不是裸露的vnc服务的ip+port,而是由noVNC提供的websockets的代理,在noVNC代理服务器上要配置每个vnc服务,noVNC提供一个标识,去反向代理所配置的vnc服务。

noVNC的使用

假设我们在PC2上创建了一个VNC连接,sessionnumber是1,端口号为5901。noVNC可以和vncserver在一台机器上,也可以不在一台机器上。

简单用法

安装noVNC

$git clone https://github.com/kanaka/noVNC
$cd noVNC
$./utils/launch.sh --vnc localhost:5901

启动launch脚本,会输出如下信息:

WebSocket server settings:
  - Listen on :6080
  - Flash security policy server
  - Web server. Web root: /root/noVNC
  - No SSL/TLS support (no cert file)
  - proxying from :6080 to localhost:5901


Navigate to this URL:

    http://localhost:6080/vnc.html?host=localhost&port=6080

这时,访问http://localhost:6080/vnc.html?host=localhost&port=6080http://localhost:6080/vnc.html,然后输入Host地址,端口号,密码,token,其中密码和token有的话需要输入,然后连接即可。当然你可以从PC1的浏览器中输入PC2的IP地址访问。

整个流程大概是这样的:

vnc.html -> 192.168.1.102:6080(PC2) -> websockify.py -> localhost:5901

高级用法

使用websockify可以更改默认6080端口,使用token设置。

用法:./utils/websockify/websockify.py --web ./ 8787 localhost:5901

--web ./指定访问根目录,8787表示访问novnc的端口,localhost可以改成所有安装了vncserver的IP地址,比如:./utils/websockify/websockify.py --web ./ 8787 192.169.1.100:5901

使用token

为什么使用token?

我们上面的场景是基于noVNC代理和vncserver在同一台机器上的,倘若我们想通过noVNC访问局域网中的所有机器,难道要给每一台机器都安装配置noVNC,然后用每台机器的IP地址去访问它吗?显然这种做法是繁琐笨拙的。实际上,我们只需要一台机器作为noVNC代理,其他被访问的机器安装VNC server就可以了。如下图:

在上图中,我们用一台机器作为代理,IP:192.168.1.10,另外两台机器PC1:192.168.1.101和PC2:192.168.1.102上面安装vncserver,我们怎么通过代理去访问PC1和PC2呢?这就需要token大显身手了。

我们需要在代理机器上创建一个token配置文件,假设为/home/token/token.conf,文件内容为:

abc123: 192.168.1.101:5900
123abc: 192.168.1.102:5901

首先,在欲访问的机器上启动vncserver,执行命令vncserver即可。

然后,在代理机器上输入命令:./utils/websockify/websockify.py --web ./ --target-config=./token/token.conf 8787

接下来,访问192.168.1.10:8787/vnc.html,输入对应的token即可访问相应的机器了。

使用vnc_auto.html

在vnc_auto.html中写入noVNC代理的配置

host = "192.168.1.10";
port = 8787;
path = "websockify/?token=xxxxxx";

然后直接访问192.168.1.10:8787/vnc_auto.html即可连接。

问题排查

如果输入host地址,port之后,不能访问,查看密码是否正确,如果显示connection refused,查看被访问主机vncserver是否启动,如果未启动,执行vncserver

]]>
Linux计划任务 2016-07-14T00:00:00+00:00 lxc1121 http://lxc1121.github.io/2016/07/Linux计划任务 计划任务

计划任务就是定时或周期执行的一些工作,比如定期发邮件等等。在Linux中,例行性工作是通过crontab和at进行调度的,所谓调度就是将这些工作安排执行的流程。在Linux中有两种工作调度的方式:一种是例行性的,每隔一定的周期执行一次,用crontab来处理;另一种就是一次性的,执行完之后就没了,用at来处理。

一次性的工作

对于一次性的定时任务使用at命令。要使用at命令需要开启atd服务,systemctl start atd

命令格式:

at [参数] [时间]

命令参数:

  • -m 当指定的任务被完成之后,将给用户发送邮件,即使没有标准输出
  • -l atq的别名
  • -d atrm的别名
  • -v 显示任务将被执行的时间
  • -c 打印任务的内容到标准输出
  • -V 显示版本信息
  • -q<列队> 使用指定的列队
  • -f<文件> 从指定文件读入任务而不是从标准输入读入
  • -t<时间参数> 以时间参数的形式提交要运行的任务

at允许使用一套相当复杂的指定时间的方法。他能够接受在当天的hh:mm(小时:分钟)式的时间指定。假如该时间已过去,那么就放在第二天执行。当然也能够使用midnight(深夜),noon(中午),teatime(饮茶时间,一般是下午4点)等比较模糊的 词语来指定时间。用户还能够采用12小时计时制,即在时间后面加上AM(上午)或PM(下午)来说明是上午还是下午。 也能够指定命令执行的具体日期,指定格式为month day(月 日)或mm/dd/yy(月/日/年)或dd.mm.yy(日.月.年)。指定的日期必须跟在指定时间的后面。 上面介绍的都是绝对计时法,其实还能够使用相对计时法,这对于安排不久就要执行的命令是很有好处的。指定格式为:now + count time-units ,now就是当前时间,time-units是时间单位,这里能够是minutes(分钟)、hours(小时)、days(天)、weeks(星期)。count是时间的数量,究竟是几天,还是几小时,等等。 更有一种计时方法就是直接使用today(今天)、tomorrow(明天)来指定完成命令的时间。

使用举例:

实例1:三天后的下午5点执行 /bin/ls

执行命令:at 5pm + 3 days,之后终端如下,要求你输入需要执行的操作或命令,最后要以Ctrl+d结束。

at> /bin/ls
at> <EOT>
job 7 at 2013-01-08 17:00

实例2:明天下午17:20将时间输入到指定文件中

[root@localhost ~]# at 17:20 tomorrow
at> date >/root/2016.log
at> <EOT>
job 8 at 2013-01-06 17:20
[root@localhost ~]#

计划任务设定后,我们可以使用at -latq命令来查看系统中的at任务。

[liushaolin@mycentos ~]$ atq
2	Sun Jul 17 17:00:00 2016 a liushaolin
3	Fri Jul 15 17:20:00 2016 a liushaolin

显示已经设置的任务内容:at -c 3

删除已经设置的任务:atrm 3at -d 3

at的运行方式:我们使用at这个命令来生成所要运行的工作,并将这个工作以文本文件的方式写入/var/spool/at目录内,该工作便能等待atd这个服务的取用与执行了。并非所有用户都可以进行at调度,/etc/at.allow和/etc/at.deny决定了允许或者禁止那些用户可以进行at调度。

周期性的例行性工作

对于按周期执行的任务使用crontab服务。

命令格式:

crontab [-u username] [-l/e/r]

命令参数:

  • -u:只有root用户才能执行,帮助其他用户新建/删除crontab工作调度
  • -e:编辑crontab的工作内容
  • -l:查询crontab的工作内容
  • -r:删除所有crontab的工作内容,如果想仅删除一项,请使用-e去编辑

和at类似,/etc/cron.allow和/etc/cron.deny允许和禁止那些用户使用crontab进行工作调度。

当用户使用crontab这个命令来新建工作调度之后,该项工作就被记录到/var/spool/cron/里面,而且是以用户名来命名。cron执行的每一项工作都会被记录到/var/log/cron这个日志文件里面。

使用举例:

在终端输入crontab -e后进入编辑界面,然后你就可以定制自己的例行工作了。

实例1:定时发送邮件

0 12  *   *   *  mail buptlsl -s "at 12:00" < /home/buptlsl/.bashrc

我们来看一下这行代码,它的意思是每天的12点给buptlsl发送一封邮件,标题为“at 12:00”,邮件内容读取自文件/home/buptlsl/.bashrc。

每条任务都有6个字段,前5个字段为时间字段,最后一个是命令字段。如下:

代表意义 分钟 小时 月份 命令
数字范围 0-59 0-23 1-31 1-12 0-7 命令

时间字段有几点需要指出:

  • 周的数字0或7都代表周日。
  • /n表示每隔n个时间单位间隔执行一次,例如每5分钟执行一次即为:/5 * * * * command也可以写成0-59/5
  • *表示任何时刻都接受的意思
  • ,逗号表示分隔时间段的意思,如果执行命令的时刻为3时或6时,则为* 3,6 * * * command
  • -表示一段时间范围,例如8点到12点,用8-12表示

系统crontab配置

前面的crontab -e是针对用户设计的,创建的任务会保存在/var/spool/cron/username中,而系统的例行性工作都在/etc/crontab这个文件中,只需要编辑这个文件就可以修改系统的例行性工作了。crond服务会每分钟去读取一次/etc/crontab文件和/var/spool/username文件夹里面的内容来主执行例行任务。所有的系统计划任务最好都放在/etc/crontab

需要区别的是,crontab -e这个crontab其实是/usr/bin/crontab这个执行文件,但是/etc/crontab是一个纯文本文件。

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  *  command to be executed

MAILTO=root表示当例行性命令有错误或者有输出结果时,会将信息传给root,这个地方可以改成你自己的邮箱,例如:haha@gmail.com

更多例子:

1 * * * * root run-parts /etc/cron.hourly //每个小时去执行一遍/etc/cron.hourly内的脚本
2 4 * * * root run-parts /etc/cron.daily //每天去执行一遍/etc/cron.daily内的脚本
22 4 * * 0 root run-parts /etc/cron.weekly //每星期去执行一遍/etc/cron.weekly内的脚本
42 4 1 * * root run-parts /etc/cron.monthly //每个月去执行一遍/etc/cron.monthly内的脚本

时间字段和命令字段之间有一个身份字段,表示执行命令的用户身份,此处为root。

新技能

观察crontab任务的写法你可以看出执行crontab任务的最小时间单位是minute,假如我希望能够每隔10s执行一次任务呢?这种情况可以借助crontab + sleep来实现。

* * * * * php /home/fdipzone/php/crontab/tolog.php
* * * * * * sleep 10; php /home/fdipzone/php/crontab/tolog.php
* * * * * * sleep 20; php /home/fdipzone/php/crontab/tolog.php
* * * * * * sleep 30; php /home/fdipzone/php/crontab/tolog.php
* * * * * * sleep 40; php /home/fdipzone/php/crontab/tolog.php
* * * * * * sleep 50; php /home/fdipzone/php/crontab/tolog.php

当然这种方法比较粗暴并且丑陋,所以可以借助脚本来实现。

首先,编写shell脚本crontab.sh

#!/bin/bash
step=10 #间隔的秒数,不能大于60
for (( i = 0; i < 60; i=(i+step) )); do
    $(php '/home/fdipzone/php/crontab/tolog.php')
    sleep $step
done
exit 0

然后创建计划任务:

* * * * * /home/liushaolin/php/crontab/crontab.sh
]]>
用celery和rabbitmq做异步任务调度 2016-05-09T00:00:00+00:00 lxc1121 http://lxc1121.github.io/2016/05/celery-rabbitmq 任务队列的应用场景

考虑如下场景:

1.社交网站某用户发布了一组照片,这条新鲜事需要适时地推送给该用户的所有好友。如果好友数量很多的话,在同一时刻有很多的推送任务要处理,如果同步操作的话会造成阻塞。而且一个社交网站有大量用户,同时进行推送会给服务器造成很大压力。所以,处于用户体验考虑,用户发布照片这个操作需要在较短时间得到反馈。

2.在文献搜索系统的主页,用户可以查到当前一小时最热门的十篇文献,并能够直接访问该文献。文献系统所管理的文献非常多,出于用户体验考虑,用户获得十大热门文献这个操作也要在较短时间内得到反馈。

考虑到大并发的web系统,对于上面两种场景的需求,如果在请求处理周期内完成这些任务,再将结果返回,这种传统的做法会导致用户等待的时间过长,同时web后台对任务的处理能力也缺乏扩展性。

对于这种场景,任务队列是最有效的解决方案。任务队列一般采用生产-消费模型,包括生产者、任务处理中间方、任务消费者。任务生产者负责生产任务,中间方负责接收任务生产者的任务处理请求,对任务进行调度,并将任务发送给消费者来处理。

Celery: 基于 Python 的开源分布式任务调度模块

Celery是一个用python编写的分布式的任务调度模块,它有着简明的 API,并且有丰富的扩展性,适合用于构建分布式的 Web 服务。

celery模块架构

Celery 的模块架构较为简洁,但是提供了较为完整的功能:

任务生产者 (task producer)

任务生产者 (task producer) 负责产生计算任务,交给任务队列去处理。在 Celery 里,一段独立的 Python 代码、一段嵌入在 Django Web 服务里的一段请求处理逻辑,只要是调用了 Celery 提供的 API,产生任务并交给任务队列处理的,我们都可以称之为任务生产者。

任务调度器 (celery beat)

Celery beat 是一个任务调度器,它以独立进程的形式存在。Celery beat 进程会读取配置文件的内容,周期性地将执行任务的请求发送给任务队列。Celery beat 是 Celery 系统自带的任务生产者。系统管理员可以选择关闭或者开启 Celery beat。同时在一个 Celery 系统中,只能存在一个 Celery beat 调度器。

任务代理 (broker)

任务代理方负责接受任务生产者发送过来的任务处理消息,存进队列之后再进行调度,分发给任务消费方 (celery worker)。因为任务处理是基于 message(消息) 的,所以我们一般选择 RabbitMQ、Redis 等消息队列或者数据库作为 Celery 的 message broker。

任务消费方 (celery worker)

Celery worker 就是执行任务的一方,它负责接收任务处理中间方发来的任务处理请求,完成这些任务,并且返回任务处理的结果。Celery worker 对应的就是操作系统中的一个进程。Celery 支持分布式部署和横向扩展,我们可以在多个节点增加 Celery worker 的数量来增加系统的高可用性。在分布式系统中,我们也可以在不同节点上分配执行不同任务的 Celery worker 来达到模块化的目的。

结果保存

Celery 支持任务处理完后将状态信息和结果的保存,以供查询。Celery 内置支持 rpc, Django ORM,Redis,RabbitMQ 等方式来保存任务处理后的状态信息。

安装Celery和RabbitMQ

极为简单:

pip install celery
yum -y install rabbitmq-server

Celery需要一个消息代理人(broker)来处理请求,分发任务。我们使用rabbitmq来做这个broker。一般安装完rabbitmq就会自动启动,没有启动的话,可以用systemctl start rabbitmq-server来启动。

使用Celery

创建一个celery实例

创建一个tasks.py文件,引入celery并实例化:

from celery import Celery
from time import sleep
app = Celery('tasks', backend='amqp', broker='amqp://')

第一个参数会添加到任务上用来区分它们。backend参数是可选的,如果想要查询任务状态或者任务执行结果时必填。broker参数表示用来连接broker的URL,rabbitmq采用的是一种称为’amqp’的协议,如果rabbitmq运行在默认设置下,celery不需要其他信息,只要amqp://即可。

创建Celery任务

在tasks.py中继续添加如下代码:

@app.task(ignore_result=True)    #这个hello函数不需要返回有用信息,设置ignore_rsult可以忽略任务结果
def hello():
    print 'Hello,Celery!'

@app.task
def add(x,y):
    sleep(5)    #模拟耗时操作
    return x+y

启动celery worker进程

在终端中执行

celery -A tasks worker --loglevel=info
或
celery -A tasks worker &    #后台执行

使用队列处理任务

打开python命令行,导入任务函数:

from tasks import hello, add
hello()    #正常执行,没有被worker处理
add()    #正常执行,没有被worker处理
hello.delay()    #交给worker处理
res = add.delay(3,4)    #交给worker处理

查看任务是否执行完,可以用ready()方法res.ready(),获取返回值可以使用get()方法,res.get()

使用配置文件

对于简单的应用来说,可以直接在tasks文件中通过app = Celery(...)的方式来实例化。

当然,创建了Celery实例之后,也可以修改配置:

单个:

app.conf.CELERY_TASK_SERIALIZER = 'json'

或批量(支持dict语法):

app.conf.update(
    BROKER_URL = 'amqp://',
    CELERY_RESULT_BACKEND = 'amqp://',
    CELERY_TASK_SERIALIZER = 'json',
    CELERY_ACCEPT_CONTENT = ['json'],
    CELERY_RESULT_SERIALIZER = 'json',
    CELERY_TIMEZONE = 'Asia/Shanghai',
    CELERY_ENABLE_UTC = True
)

但是对于大的项目不推荐这种硬编码的方式,最好将Celery的配置保存在一个配置文件中,便于集中管理和使用。

新建一个配置文件celeryconfig.py,写入配置内容:

BROKER_URL = 'amqp://'
CELERY_RESULT_BACKEND = 'amqp://'
CELERY_TASK_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Asia/Shanghai'
CELERY_ENABLE_UTC = True
CELERY_IMPORTS = ("tasks",)

然后在tasks文件中app.config_from_object('celeryconfig')

参考资料:

]]>
栈与函数调用 2016-04-14T00:00:00+00:00 lxc1121 http://lxc1121.github.io/2016/04/Stack-and-function-call 这篇文章主要来探究一下函数调用过程中栈的变化。

]]>
Lab 1:Booting a PC 2016-03-22T00:00:00+00:00 lxc1121 http://lxc1121.github.io/2016/03/JOS-lab1 介绍

本实验分成三部分:第一部分聚焦熟悉x86汇编语言、QEMU x86仿真器以及PC上电启动过程;第二部分检验我们6.828内核的boot loader,它存在于boot目录下;最后第三部分深入研究6.828内核名为JOS的初始化模板,它存在于kernel目录下。

软件安装

安装开发环境、工具链和QEMU虚拟机,不再赘述,参考Tools

使用git获取实验代码:git clone https://pdos.csail.mit.edu/6.828/2014/jos.git lab。需要使用x86或x64架构的机器。

第一部分:PC启动

了解x86汇编

练习1:阅读Brennan’s Guide to Inline Assembly的语法部分。

模拟x86

我们使用QEMU虚拟机来模拟x86。通过QEMU和GDB配合可以对PC启动进行跟踪调试。

进入实验代码目录,编译源码:

liushaolin@centos$ cd lab
liushaolin@centos$ make
+ as kern/entry.S
+ cc kern/init.c
+ cc kern/console.c
+ cc kern/monitor.c
+ cc kern/printf.c
+ cc lib/printfmt.c
+ cc lib/readline.c
+ cc lib/string.c
+ ld obj/kern/kernel
+ as boot/boot.S
+ cc -Os boot/main.c
+ ld boot/boot
boot block is 414 bytes (max 510)
+ mk obj/kern/kernel.img

如果出现“undefined reference to __udivdi3”,你可能缺少32-bit gcc multilib。

运行虚拟机:进入lab目录,在终端中输入make qemu命令。

启动虚拟机

当前的kernel监视器中仅有两个命令:helpkerninfo

K> help
help - display this list of commands
kerninfo - display information about the kernel
K> kerninfo
Special kernel symbols:
  entry  f010000c (virt)  0010000c (phys)
  etext  f0101a75 (virt)  00101a75 (phys)
  edata  f0112300 (virt)  00112300 (phys)
  end    f0112960 (virt)  00112960 (phys)
Kernel executable memory footprint: 75KB
K>

PC的物理地址空间

接下来我们深入了解一点PC是如何启动的。一台PC的物理地址空间的基本布局大致如下:

物理地址空间布局

第一代的PC是基于16位intel 8088处理器,仅能寻址1MB的物理内存。因此早期PC的物理地址空间起于0x00000000,但是止于0x000FFFFF,而不是0xFFFFFFFF。640KB的空间标记为“低地址”,是早期PC仅能使用的唯一RAM。事实上,非常早期的PC仅能分配16KB,32KB,64KB的RAM。

0x000A0000到0x000FFFFF的384KB空间被硬件保留用于诸如视频播放缓冲等特殊用途。这片保留区域最重要的部分就是Basic Input/Output System(BIOS),BIOS占据了从0x000F0000到0x000FFFFF的64KB空间。在早期PC中,BIOS被保存在真正的ROM中,但是现在计算机把BIOS保存在可更新的flash存储器中。BIOS负责执行诸如激活显卡以及检查已装入的内存总量这些基本的系统初始化。执行完初始化之后。BIOS从某个合适的地方,比如软盘/硬盘/光盘以及网络中载入操作系统并将机器的控制权转交给操作系统。

当intel打破了“1MB障碍”之后,为了后向兼容已有的软件,PC设计师保留了最低的1MB地址空间布局。因此,现代PC在物理地址0x000A0000到0x00100000有一个“洞”,把RAM分成了“低的”或“传统的内存”(前面的640KB)和“扩展内存”(所有剩下的)。另外,在32位物理地址空间最顶部,在所有物理RAM之上的空间被BIOS保留用于32位PCI设备。

现在x86处理器可以支持高达4GB的物理RAM,因此RAM可以扩展到0xFFFFFFFF。在这种情况下,BIOS必须留出第二个“洞”,也就是在32位可寻址空间的顶部,留出需要映射的32位设备的地址空间。由于设计上的限制,JOS仅使用开始的256MB物理内存。

只读存储(ROM)BIOS

在这一部分,你将使用QEMU的调试功能研究一台兼容IA-32架构的PC是如何启动的。

使用make qemu-gdbmake gdb可以联调kernel。具体方法为:打开两个终端,都进入lab目录,其中一个输入make qemu-gdb,另一个输入make gdb。这时,QEMU启动,但是暂停在处理器执行第一条指令之前等待GDB的debug指令,效果如下:

联调kernel

可以看到第一条指令是:[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b。我们可以推断:

  • IBM PC从物理地址0x000ffff0开始执行,位于64KB保留的BIOS区域的顶端。
  • CS和IP寄存器的内容分别是:CS=0xf000,IP=0xfff0
  • 第一条指令是jmp指令,调转到CS=0xf000,IP=0xe05b指向的地址

为什么QEMU如此启动呢?这是因为intel的8088处理器就是这样设计的。因为一台PC的BIOS有固定的物理地址范围:0x000f0000-0x000fffff,这样设计可以保证机器开机或重启时BIOS能够立即获取机器的控制权。一旦处理器复位,处理器就进入实模式把CS和IP寄存器的值置为0xf000:0xfff0,根据公式physical address=16*segment+offset计算出物理地址0xffff0。从该地址到BIOS的结束位置(0x100000)还剩16字节,所以我们不必惊讶BIOS要干的第一件事就是跳转到BIOS更前面的位置,毕竟仅仅16字节能干什么呢?

练习2:使用GDB的si指令单步执行几条BIOS的指令,猜测一下BIOS可能做了什么。

当BIOS运行时,它建立一张中断描述符表并初始化VGA显示器之类的各种设备。完成初始化PCI总线和所有BIOS已知的重要设备之后,它便开始搜寻可启动设备,软盘,硬盘,光盘等等,从磁盘中载入引导加载器(boot loader)并将控制权转交给它。

进入gdb后,可以使用i r指令查看各寄存器的值,通过x指令查看内存。还可以用i r ax查看指定的寄存器。gdb调试的时候,step和next指令都是不能使用的,单行执行汇编指令stepi可用。

gdb查看命令

第二部分:Boot Loader

PC的磁盘被分成一个个大小位512字节的区域,称为扇区(sector)。一个扇区是磁盘的最小转移粒度:每一次读或写操作大小必须是一个或多个扇区并且要和扇区边界对齐。如果磁盘是可引导的,那么第一个扇区成为引导扇区(boot sector),这个扇区是存放boot loader代码的地方。当BIOS找到可引导磁盘之后,它把512字节的引导扇区加载到物理地址从0x7c00到0x7dff的内存中,并使用jmp指令将CS:IP设置为0000:7c00,将控制权转交给boot loader。和BIOS的载入地址类似,这段地址是随意的——但对于PC来说是固定的和标准化的。

在计算机发展过程中,从CD-ROM引导的能力要出现的晚得多。因此,设计师有机会重新思考引导过程。现在从CD-ROM引导的BIOS有一些复杂,CD-ROM的扇区大小是2048字节,而不是512字节,BIOS可以把更大的引导镜像载入内存。

6.828中,我们使用传统的硬盘引导方式,这意味着我们的boot loader必须满足可怜的512字节。xv6的boot loader由一个汇编源文件boot/boot.S和一个C源文件boot/main.c组成。仔细地通读这些源文件,确保你明白发生了什么。boot loader必须完成两个重要功能:

  • 首先,引导加载器把处理器从实模式切换到32位保护模式,因为只有在该模式下,软件才能访问物理地址空间中所有高于1MB的内存。你只要知道在保护模式下,分段地址(段地址:偏移地址对)到物理地址的转换有所不同,偏移地址是32位,而不再是16位。
  • 第二,引导加载器直接通过特殊的x86 I/O指令访问IDE磁盘设备寄存器来读取内核。

在理解了boot loader的源代码之后,查看一下obj/boot/boot.asm文件,这个文件是GNUMakefile在编译完boot loader之后创建的可执行文件的反汇编。通过这个文件可以清楚的知道boot loader的代码在物理内存的具体位置,也更易于知道在GDB中单步执行boot loader时发生了什么。同样,obj/kern/kernel.asm 是JOS kernel的反汇编,这个文件在调试内核的时候很有帮助。

你可以在GDB中使用b命令设置地址断点。举个例子,b *0x7c00就在地址0x7c00处设置了一个断点。在断点处,你可以使用csi命令继续执行:c命令使QEMU继续运行直到遇到下一个断点;si N可以一次跳过N条指令。

使用x/i命令可以查看内存中的指令。语法格式为:*x/N ADDR*,表示查看从地址ADDR开始的N条指令。

练习3:阅读实验工具指导,尤其是GDB部分。

在0x7c00处设置断点。继续执行,然后单步调试,和obj/boot/boot.asm反汇编进行对照。

跳到bootmain()中的readsect()函数,跟踪这个函数,跳回bootmain()函数,确定从磁盘读取剩余内核扇区的for循环的头和尾。找出for循环结束之后将要执行的代码,在此处设置断点,然后单步执行完剩余的boot loader代码。

思考下列问题:

1.在什么地方,处理器开始执行32位代码?是什么导致了从16位模式到32位模式的转变?

我们知道,当CPU一开始上电复位之后,是在实模式运行。打开lab1/boot/boot.S,其中有一行ljmp $PROT_MODE_CSEG, $protcseg,从实模式跳到保护模式的时候开始执行32位代码。

2.Bootloader执行的最后一条指令是什么,kernel加载的第一条指令是什么?

打开lab1/boot/main.c,找到bootmain()函数中的最后一条语句为((void (*)(void)) (ELFHDR->e_entry))();,ELFHDR是指向0x10000(被强制转换为struct Elf类型)的指针,通过readseg函数初始化ELFHDR,这个初始化的数据来源就是硬盘上的内核镜像。怎么看ELFHDR->e_entry指向的位置呢?反汇编kernel镜像!

反汇编kernel的命令:objdump -x ./obj/kern/kernel

[liushaolin@localhost kern]$ objdump -x kernel

kernel:     文件格式 elf32-i386
kernel
体系结构:i386,标志 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
起始地址 0x0010000c

可以看出kernel的起始地址是0x0010000c,GDB设置断点调试,可以看到kernel的第一条指令就是:movw $0x1234, 0x472。在kern/entry.S中可以找到这条语句:

.globl entry
entry:
        movw $0x1234,0x472    # warm boot

3.Bootloader是怎么确定要读取多少扇区以保证可以从磁盘获取整个内核的?它是在哪里找到该信息的呢?

Bootloader是通过ELF文件存储的信息确定并读取的。具体见后面的加载内核部分。

下面是obj/kern/kernel的ELF头:

[liushaolin@localhost kern]$ objdump -h kernel

kernel:     文件格式 elf32-i386

节:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00001907  f0100000  00100000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       00000730  f0101920  00101920  00002920  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         00003871  f0102050  00102050  00003050  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .stabstr      000018ba  f01058c1  001058c1  000068c1  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data         0000a300  f0108000  00108000  00009000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  5 .bss          00000644  f0112300  00112300  00013300  2**5
                  ALLOC
  6 .comment      0000002c  00000000  00000000  00013300  2**0
                  CONTENTS, READONLY

下面是obj/boot/boot.out的ELF头:

[liushaolin@localhost boot]$ objdump -h boot.out

boot.out:     文件格式 elf32-i386

节:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000017e  00007c00  00007c00  00000074  2**2
                  CONTENTS, ALLOC, LOAD, CODE
  1 .eh_frame     000000b0  00007d80  00007d80  000001f4  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         000007b0  00000000  00000000  000002a4  2**2
                  CONTENTS, READONLY, DEBUGGING
  3 .stabstr      00000846  00000000  00000000  00000a54  2**0
                  CONTENTS, READONLY, DEBUGGING
  4 .comment      0000002c  00000000  00000000  0000129a  2**0
                  CONTENTS, READONLY

加载内核

现在我们进一步研究一下bootloader C语言部分的细节,对应的文件为boot/main.c。在此之前,来回顾一下C的基本知识。

练习4:阅读指针代码pointers.c

想要弄明白boot/main.c,你首先要知道什么是ELF二进制文件。当你编译、连接一个类似于JOS kernel的C程序时,编译器把每个C源文件转换为包含以二进制格式编码的汇编语言指令的目标文件(‘.o’文件),然后连接器把所有的目标文件组合成一个单一的二进制镜像,比如obj/kern/kernel,这个镜像文件就是以ELF格式存储的二进制文件,它表示“可执行和可连接的格式”。

你可以认为一个ELF可执行文件带有一个包含加载信息的头,后面跟着几个*程序段(program section)*,其中的每一个段都是一个将要加载到内存中指定地址的连续代码块或数据块。Bootloader不会修改代码或数据,它把代码或数据加载到内存中然后执行。

一个ELF二进制文件有一个固定长度的ELF头,后面跟着一个可变长度的*程序头(program header)*,列出每一个将要载入的程序段。C语言对这些ELF头的定义在inc/elf.h中。我们感兴趣的程序段包括:

  • .text:程序的可执行指令
  • .rodata:只读数据。比如C编译器生成的ASCII字符串常量
  • .data:数据段,包含程序的初始化数据。比如初始化声明的全局变量

当连接器计算出一个程序的内存布局,它会为那些未初始化的全局变量比如int x在被称为.bss的段上保留空间,.bss段在内存中紧跟.data段。C语言要求“未初始化”的全局变量初始值为0,因此没有必要在ELF二进制文件中储存.bss段的内容,连接器只是记录.bss段的地址和大小。加载器或程序自己需要将.bss段的内容置零。

需要特别注意的是:.text段的VMA(或者link address)和LMA(或者load address)。load address是该段要加载到内存中的地址。程序段的link address是该段期望开始执行的内存地址(也就是虚拟地址)。关于VMA和LMA可以参考LMA和VMALinker Script,LMA,VMA为什么要有VMA和LMA两个地址?

一般来说,段的VMA和LMA是相同的,可以查看bootloader的.text段。凡是讲到代码段、数据段等等,指的就是虚拟内存。 对于普通的应用程序来说,load address和link address也都是指虚拟内存,LMA和VMA是相同的,这也就是所说的大多数情况下二者等价。在载入内核开启分页机制之前,虚拟地址是直接映射为物理地址的。

Bootloader通过ELF程序头(prrogram headers)来决定如何加载各个段。程序头指明了哪些段将要被加载到内存以及各个段占据的目的地址。可以通过下面的命令查看程序头:

[liushaolin@localhost lab]$ objdump -x obj/kern/kernel

obj/kern/kernel:     文件格式 elf32-i386
obj/kern/kernel
体系结构:i386,标志 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
起始地址 0x0010000c

程序头:
    LOAD off    0x00001000 vaddr 0xf0100000 paddr 0x00100000 align 2**12
         filesz 0x0000717b memsz 0x0000717b flags r-x
    LOAD off    0x00009000 vaddr 0xf0108000 paddr 0x00108000 align 2**12
         filesz 0x0000a300 memsz 0x0000a944 flags rw-
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
         filesz 0x00000000 memsz 0x00000000 flags rwx

节:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00001907  f0100000  00100000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       00000730  f0101920  00101920  00002920  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         00003871  f0102050  00102050  00003050  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .stabstr      000018ba  f01058c1  001058c1  000068c1  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data         0000a300  f0108000  00108000  00009000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  5 .bss          00000644  f0112300  00112300  00013300  2**5
                  ALLOC
  6 .comment      0000002c  00000000  00000000  00013300  2**0
                  CONTENTS, READONLY

      .
      .
      .

ELF文件中需要载入内存的是那些被标记为“LOAD”的部分。同时给出了其他一些信息,比如虚拟地址(vaddr),物理地址(paddr)以及区域大小(memsz和filesz)。

回到boot/main.c中,每一个程序头的ph->p_pa字段包含了该段的目的物理地址(在这种情况下也是真正的物理地址)。

BIOS将引导扇区加载到从0x7c00开始的内存中,因此也是引导扇区的load address,也是引导扇区开始执行的地方,也就是引导扇区的link address。我们通过在boot/Makefrag中向连接器传递-Ttext 0x7c00来设置引导扇区的link address,因此连接器可以在生成的代码中给出正确的内存地址。

练习5: Trace through the first few instructions of the boot loader again and identify the first instruction that would “break” or otherwise do the wrong thing if you were to get the boot loader’s link address wrong. Then change the link address in boot/Makefrag to something wrong, run make clean, recompile the lab with make, and trace into the boot loader again to see what happens. Don’t forget to change the link address back and make clean again afterward!

回头看一下kernel的load address和link address。和bootloader不同的是,这两个地址是不同的:kernel告诉bootloader把它加载到内存的低地址(1M字节),但是希望从一个高地址开始执行。我们会在下一章节中研究如何实现。

除了段信息,还有一个对我们来说很重要的字段,就是e_entry。该字段是程序的入口地址:在程序的text段中程序开始执行的内存地址。可以用下面的命令查看程序入口:

[liushaolin@localhost lab]$ objdump -f obj/kern/kernel

obj/kern/kernel:     文件格式 elf32-i386
体系结构:i386,标志 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
起始地址 0x0010000c

第三部分:内核(kernel)

当你观察bootloader的link address和load address时,它们是完全一致的,但是kernel的这两个地址却大不一样。和bootloader类似,内核也以一些汇编代码开始,做好必要准备之后C代码才能正常运行。

使用虚拟内存解决位置依赖(position dependence)

为了脱离供用户程序使用的处理器虚拟地址空间较低的部分,操作系统的内核通常连接并运行在很高的虚拟地址上,比如0xf0100000。这样安排的原因在下一个实验中会详细说明。

许多机器在地址0xf0100000处并没有物理内存,因此我们不能指望能够在这种地方存储内核。相反,我们使用处理器的内存管理硬件将虚拟地址0xf0100000(内核代码运行的link address)映射到物理地址0x00100000(bootloader加载内核到内存的地址)。这样,尽管内核的虚拟地址的高度足够给用户进程留出充足的地址空间,内核还是会加载到物理内存RAM的1MB处,就在BIOS ROM之上。

实际上,我们在下一个实验中将把PC物理内存最低的256MB地址空间,从0x00000000到0x0fffffff映射到虚拟地址从0xf0000000到0xffffffff。现在你应该明白为什么JOS只能使用前256MB物理内存了。

到目前为止,我们只映射前4MB物理内存,这足够我们启动和运行了。我们使用kern/entrypgdir.c中手写的,固定初始化的页目录和页表来完成这一映射。到目前为止,你不必知道具体的细节,只需要明白达到的效果。直到kern/entry.S设置好CR0_PG标志之前,内存的引用都是作为物理地址对待(严格的讲,应该是线性地址,但是boot/boot.S建立了从线性地址到物理地址的等价映射,我们永远不会改变这一点)。一旦CR0_PG标志设置好,内存的引用就是通过把虚拟地址映射为物理地址来实现的。entry_pgdir将0xf0000000到0xf0400000范围的虚拟地址和0x00000000到0x00400000范围的虚拟地址映射为从0x00000000到0x00400000的物理地址。任何不在这两个范围内的虚拟地址都将引起硬件异常,因为我们还没建立异常处理,这会导致QEMU宕机或者无限重启。

练习7:Exercise 7. Use QEMU and GDB to trace into the JOS kernel and stop at the movl %eax, %cr0. Examine memory at 0x00100000 and at 0xf0100000. Now, single step over that instruction using the stepi GDB command. Again, examine memory at 0x00100000 and at 0xf0100000. Make sure you understand what just happened.

What is the first instruction after the new mapping is established that would fail to work properly if the mapping weren’t in place? Comment out the movl %eax, %cr0 in kern/entry.S, trace into it, and see if you were right.

exercise 7

movl %eax, %cr0处设置断点,执行到此处。在执行这条命令之前,发现0x00100000和0xf0100000两个地址的内容是不同的,执行完之后二者就变成相同的了。原因就是在执行这条指令之前,还没有建立分页机制,高地址的内核区域还没有映射到内核的物理地址,只有低地址是有效的;执行完这条指令之后,开启了分页,由于有静态映射表(kern/entrypgdir)的存在,两块虚拟地址区域都映射到同一块物理地址区域。

注释掉kern/entry.S中的movl %eax, %cr0,这样就无法开启分页,虚拟地址无法映射到物理地址。所以,此时第一条执行失败的命令是 0x100031: mov $0xf0110000,%esp,因为0xf0110000是高的虚拟地址,由于没有分页,CPU不知道访问哪一个物理地址。

终端格式化输出

大多数人都把类似于printf()的功能看成理所当然的,有时甚至认为他们是C语言的“基本”。但是,在一个OS内核中,我们不得不亲自实现所有的I/O。

通读kern/printf.clib/printfmt.ckern/console.c这三个文件,确保你理解它们之间的关系。在后续实验中你会明白为什么printfmt.c放在一个单独的lib目录下。

练习8:We have omitted a small fragment of code - the code necessary to print octal numbers using patterns of the form “%o”. Find and fill in this code fragment.

要求补全/lib/printfmt.c中“%o”部分的代码,这是八进制输出,仿照十进制“%d”代码如下:

case 'o':
    num = getuint(&ap, lflag);
    base = 8;
    goto number;

回答下列问题:

  1. 解释printf.c和console.c之间的接口。尤其是,console.c输出什么函数?printf.c是如何使用该函数的?

printf.c中的cprintf函数调用vcprintf,vcprintf调用lib/printfmt.c中的vprintfmt,vprintfmt调用printf.c中的cputchar函数,最终cputchar函数实现字符打印。

  1. 解释下面console.c的代码片段:
if (crt_pos >= CRT_SIZE) {
              int i;
              memcpy(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
              for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
                      crt_buf[i] = 0x0700 | ' ';
              crt_pos -= CRT_COLS;
      }

这段代码的作用是检测当前屏幕的输出buffer是否满了,注意这里的memmove其实就是把第二个参数指向的地址内容移动n字节到第一个参数指向的地址。n由第三个参数指定。如果buffer满了,把屏幕第一行覆盖掉逐行上移,空出最后一行,并由for循环填充以空格,最后把crt_pos置于最后一行的行首。

下面的问题可以参考Lecture 2

  1. 单步调试下面的代码:
int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);

调用cprintf()时,fmt指向什么?ap指向什么?

cprintf的代码为:

int cprintf(const char *fmt, ...)
{
	va_list ap;
	int cnt;

	va_start(ap, fmt);
	cnt = vcprintf(fmt, ap);
	va_end(ap);

	return cnt;
}

fmt指向cprintf的第一个参数,也就是格式化字符串”x %d, y %x, z %d\n”的首地址。ap是一个va_list变量,在执行完va_start之后,ap指向第一个可变参数也就是x,在进入vcprintf之后会按顺序指向y,z。在执行完va_end(ap)之后,ap变成NULL。

  1. 执行下面的代码,查看输出是什么?为什么是这样?
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s",57616,&i);

输出结果为:He110 world。原因就是57616的16进制形式为0xe110,第二个参数不是变量本身,而是变量地址,共四个字节。x86是little-endian,输出顺序是从低到高:72对应’r’,6c对应’l’,64对应’d’,00对应’\0’。

  1. 执行下面的语句,打印结果是什么?
cprintf("x=%d y=%d", 3);

打印结果为“x=3 y=xxx”,由于只有一个可变参数3,在打印完第一个参数之后ap会加4,但是内容是不可预知的。

  1. 假设GCC编译器改变了调用约定,按照声明的顺序将变量压栈,这样最后一个变量最后入栈。那么该如何修改cprintf或者它的接口以保证还可以正常打印呢?

这里涉及到变长参数,头文件定义如下:

说明调用了GCC里的函数,没有找到__builtin_函数的实现,参考inc/stdarg.h

va_arg 每次是以地址往后增长取出下一参数变量的地址的,而这个实现方式是默认编译器是以从右往左的顺序将参数入栈的,且栈是以从高往低的方向增长的。后压栈的参数放在了内存地址的低位置,所以如果要以从左到右的顺序依次取出每个变量,那么编译器必须以相反的顺序即从右往左将参数压栈。如果编译器更改了压栈的顺序,那么为了仍然能正确取出所有的参数,那么需要修改上面代码中的 va_start 和 va_arg 两个宏,即

#define va_start(ap, last) ((ap) = (va_list)&(last) - __va_size(last))
#define va_arg(ap, type)   (*(type *)((ap) -= __va_size(type), (ap) + __va_size(type)))

JOS提供了颜色打印功能,具体实现可以参考http://blog.csdn.net/woxiaohahaa/article/details/49702219

在本实验的最后一个练习中,我们将探索一些更多关于C语言在X86下使用栈的细节,并在这一过程中写一个新的可用的内核监视器函数来打印栈的回溯信息:一个保存的来自嵌套调用的指令指针值的列表。

以下资料帮助理解栈的原理:

练习9:确定内核是在何处初始化它的栈的,它的栈位于内核中何处?内核是怎么为自己预留栈空间的?初始化栈指针指向这块预留区域的哪个“端”?

打开kern/entry.S,可以发现内核栈的初始化:

我们发现栈有两部分,第一部分是实际栈空间,一共KSTKSIZE,其大小定义在inc/memlayout.h中,KSTKSIZE = 8 × PGSIZE = 8 × 4096B = 32KB,另一部分是栈底指针bootstacktop,因为它指向栈空间定义完以后的高地址位置。前面我们说过栈是向低地址增长的,所以最高位置就是栈顶,这个位置会作为初值传递给%esp。

X86栈指针(esp寄存器)指向当前正在使用的栈的最低位置。在这片栈区域上低于该位置的一切空间都是可用的。栈是向下生长的,出栈入栈具体就不多说了。在32位模式下,栈只能保留32位数据(并不是栈的大小只有32位,而是数据长度为4字节),esp总是能被4整除。

练习10: To become familiar with the C calling conventions on the x86, find the address of the test_backtrace function in obj/kern/kernel.asm, set a breakpoint there, and examine what happens each time it gets called after the kernel starts. How many 32-bit words does each recursive nesting level of test_backtrace push on the stack, and what are those words?

这个练习要求我们熟悉一下栈。打开obj/kern/kernel.asm,主要分析一下test_backtrace(),找到函数的调用入口:

我们打开qemu,使用gdb调试,在0xf01000de处设置断点。单步执行,查看栈帧和各寄存器的值,如下:

可以发现此时eip寄存器的值为0xf01000e5,也就是下一条指令的地址。找到kernel.asm中对应的指令为call f0100040 <test_backtrace>,也就是下一条指令就是调用test_backtrace。

继续单步执行call函数后,eip会指向0xf0100040,ebp不变,esp由于call函数压栈将下一条指令地址也就是0xf01000ea入栈。如下图中,eip的值被保存在0xf010ffdc,也就是当前esp指向的栈顶。在图中也可以看到,下一条要执行的指令就是push %ebp。进入test_backtrace函数发现第一条指令就是push %ebp

此时esp为0xf01000dc,ebp为0xf01000f8,函数i386_init()的栈帧从ebp~esp共32字节,继续执行到test_backtrace(x-1)时:

此时esp为0xf01000bc,ebp为0xf01000d8,也就是函数test_backtrace(x=5)的栈帧从0xf01000d8~0xf01000bc共32字节。我们可以类推,test_backtrace(x=4)的栈帧从0xf01000b8~0xf010009c。

我们可以发现每个 test_backtrace() 函数一共有4类栈空间使用:

1)入口处%ebp压栈。

2)将%ebx压栈,保存函数参数。

3)保留0x14个即20个byte的空间作为临时变量储存。

4)在call时,将%eip压栈。

一共4+4+20+4=32(byte),也即每次调用 test_backtrace() 函数,会压栈32字节的空间。

代码的结尾处,与压栈的过程刚好相反:

add    $0x14,%esp
pop    %ebx         #将栈里的临时变量空间出栈
pop    %ebp         #恢复上一个函数的栈底,栈大小减少4字节
ret                 #恢复eip,栈大小减少4字节

在函数的调用和返回过程中,压栈和出栈是对应的。关于函数调用过程中栈的变化可以参考:栈与函数调用

练习11: Implement the backtrace function as specified above. Use the same format as in the example, since otherwise the grading script will be confused. When you think you have it working right, run make grade to see if its output conforms to what our grading script expects, and fix it if it doesn’t. After you have handed in your Lab 1 code, you are welcome to change the output format of the backtrace function any way you like.

If you use read_ebp(), note that GCC may generate “optimized” code that calls read_ebp() before mon_backtrace()’s function prologue, which results in an incomplete stack trace (the stack frame of the most recent function call is missing). While we have tried to disable optimizations that cause this reordering, you may want to examine the assembly of mon_backtrace() and make sure the call to read_ebp() is happening after the function prologue.

这个练习让我们实现一个栈回溯函数mon_backtrace(),在kern/monitor.c中给出了函数原型,inc/x86.h中的read_ebp函数会有所帮助。该函数的打印结果应当如下所示:

Stack backtrace:
  ebp f0109e58  eip f0100a62  args 00000001 f0109e80 f0109e98 f0100ed2 00000031
  ebp f0109ed8  eip f01000d6  args 00000000 00000000 f0100058 f0109f28 00000061
  ...

第一行打印内容是当前正在执行的函数,也就是mon_backtrace本身的栈,第二行打印的是调用mon_backtrace的函数的栈,第三行以此类推。通过上面对栈的跟踪调试,我们知道栈的结构如下所示:

stack

图中每个框表示四个字节,arg1是最左边的函数参数,ebp指向栈底,在未调用函数时,esp指向arg1,发生函数调用时,esp-1,然后eip寄存器的值压栈。

补全kern/monitor.c的代码如下:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
        // Your code here.
    uint32_t *ebp = (uint32_t *)read_ebp();
    uint32_t eip = ebp[1];
    uint32_t arg1= ebp[2];
    uint32_t arg2 = ebp[3];
    uint32_t arg3 = ebp[4];
    uint32_t arg4 = ebp[5];
    uint32_t arg5 = ebp[6];
    cprintf("Stack backtrace:\n");
    while(ebp)
    {
        cprintf("ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",ebp,eip,arg1,arg2,arg3,arg4,arg5);
        ebp = (uint32_t *)ebp[0];
        eip = ebp[1];
        arg1= ebp[2];
        arg2 = ebp[3];
        arg3 = ebp[4];
        arg4 = ebp[5];
        arg5 = ebp[6];
    }

        return 0;
}

通过调用函数read_ebp获得当前函数栈的ebp值,我们要将其以地址的方式存储起来,这时0x0[%ebp]可以读出上个函数的栈指针,0x4[%ebp]可以读出返回地址%eip,0x8[%ebp]可以读出第一个参数. . .

]]>
逻辑地址、线性地址和物理地址 2016-01-22T00:00:00+00:00 lxc1121 http://lxc1121.github.io/2016/01/VA2PA 基本概念

数据总线

数据总线是计算机中各组成部件之间进行数据传输时的公共通道。“内数据总线宽度”是指CPU芯片内部数据传送的宽度;“外数据总线宽度”是指CPU与外部数据交换时的数据宽度。

地址总线

地址总线是載对存储器或I/O端口进行访问时,传送由CPU提供的要访问的存储单元或I/O端口的地址信息的总线,其宽度决定了处理器能直接访问的主存容量的大小。

现在的微型计算机系统采用下图的三级存储器组织结构,即缓冲存储器Cache、主存、和外存。高速缓冲存储器Cache的使用,大大减少了CPU读取指令和操作数所需的时间,使CPU的执行速度显著提高。

在硬件工程师和普通用户看来,内存就是固化在主板上的内存条,但在应用程序员眼中,并不关心主板上内存条的容量,而是他们可以使用的内存空间,而对于OS开发者来说,则是介于二者之间,既需要知道物理内存的细节,也要为应用程序员提供一个内存空间。逻辑地址、线性地址和物理地址是计算机原理中很重要的三个概念。这三者密切联系又有本质不同。从逻辑地址到物理地址经过了这样的变换:逻辑地址->分段->线性地址->分页->物理地址。

逻辑地址

逻辑地址(Logic Address)是指由程序产生的与段相关的偏移地址部分。比如,在C程序中,可以使用&操作读取指针变量本身的值,实际上这个值就是逻辑地址。它是相对于你当前进程数据段的地址,和绝对物理地址不相干。只有在intel实模式下,逻辑地址才等同于物理地址。应用程序员仅需要和逻辑地址打交道,分段和分页机制对他们来说是透明的,仅由系统编程人员涉及。编译好的程序的入口地址可以看作是首地址,逻辑地址通常可以认为是编译器为我们分配好的相对于这个首地址的偏移。一个逻辑地址由段标识符和段内偏移量组成。总之,逻辑地址是相对于应用程序而言的。 逻辑地址有时也称虚拟地址。

线性地址

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。 需要注意的是,线性地址是抽象的,对于同一平台是大小固定的,比如32位地址总线,线性地址空间为4GB,对于每个进程都是这样。也就是说线性地址空间不是进程共享的,而是进程隔离的,每个进程都有相同大小的线性空间,一个进程对某一线性地址的访问与其他进程对同一线性地址的访问不冲突,得到的值也不尽相同。对于CPU来说,某一时刻只有一个进程在运行,这个进程就认为自己独占4GB的线性空间。当进程切换的时候,线性空间也随之切换。尽管线性空间的大小和内存大小之间没有关系,但是任何线性地址最后还是要转换为物理地址才能被CPU使用去访问物理内存。基于分页机制,4GB的线性地址一部分被映射到物理内存,一部分被映射到磁盘上的交换文件,一部分什么也没有映射。 逻辑地址到线性地址的转换如下图:

物理地址

物理地址(Physical Address)是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

分段机制

分段提供了隔绝各个代码段/数据段和堆栈段的机制,因此多个任务可以运行在同一处理器上而不互相干扰。从逻辑地址(虚拟地址)到线性地址经过了分段处理。分断机制来源于早期的intel 8086处理器,早期的处理器工艺无法在一个处理器上面封装40引脚,8086处理器地址总线宽度为20位,而寄存器的宽度只有16位。20位可寻址空间为1MB,而16位的寄存器无法一次读取20位的地址,这就产生了矛盾。为了解决这一矛盾,使CPU能够寻址1MB空间,这就引入了分段机制。将段寄存器的地址左移4位,加上16位的偏移就形成了一个20位的地址。 在Linux中没有分段管理,逻辑地址就是线性地址。

分页机制

准确的说分页是CPU提供的一种机制,Linux只是根据这种机制的规则,利用它实现了内存管理。分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间(为简化分析,我们不考虑扩展分页的情况)。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)。注意,为了实现每个任务的平坦的虚拟内存,每个任务都有自己的页目录表和页表

为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。

32位的线性地址被分成3个部分:

最高10位 Directory 页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量

页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位)叫做页目录项,页目录项里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。

页表的大小也是4k,同样包含1024项,每个项4字节叫做页表项,页表项的内容为最终物理页的物理内存起始地址。

最低的12位表示物理页内的偏移。

对于一个要转换成物理地址的虚拟地址,CPU首先根据CR3中的值,找到页目录所在的物理页。然后根据虚拟地址的第22位到第31位这10位(最高的10bit)的值作为索引,找到相应的页目录项(PDE,page directory entry),页目录项中有这个虚拟地址所对应页表的物理地址。有了页表的物理地址,根据虚拟地址的第12位到第21位这10位的值作为索引,找到该页表中相应的页表项(PTE,page table entry),页表项中就有这个虚拟地址所对应物理页的物理地址。最后用虚拟地址的最低12位,也就是页内偏移,加上这个物理页的物理地址,就得到了该虚拟地址所对应的物理地址。

下面是一个例子:

参考资料

]]>