Yocto源码分析
server如何运作
在bb/server/process.py
中,定义了当Yocto采用多进程B/S
架构时,server
进程的启动方式:
start_server()
,在bin/bitbake
中,包含了一个start_server()
函数,该函数根据命令行参数,实例化相应的server
对象,并且调用server
的detach
函数,这个函数则调用了server
对象的start()
函数run()
:在bb.server.ProcessServer
类中,存在一个run
函数,该函数设置了一些UI事件,并且调用了bb.cooker.server_main()
,该函数接受两个参数,第一个是一个cooker实例,第二个是一个可执行的函数,Yocto中将self.cooker
和self.main
作为这两个参数,由于ProcessServer
类继承于Process
类,因此在调用该类的start()
方法时,run()
会被自动调用,因此在调用server.start()
时,实际调用的是server_main()
函数server_main()
:该函数执行一些预处理任务(bb.cooker.pre_serve()
),然后调用传进来的函数并且返回其返回值:
1 | #__file__ = 'bitbake/lib/bb/cooker.py' |
而这里的func
,即是上面传进来的bb.server.ProcessServer.main
,因此调用server_main()
实际上是调用了ProcessServer
类的main()
函数
ProcessServer.main()
:该函数会执行一个重要的while
循环:
1 | #__file__ = 'bitbake/lib/bb/server/process.py' |
在其中不断的从两个管道中读取数据,一个管道为命令管道,这个管道两头连接着ui
和server
,这样server就可以接受来自ui
的命令,并把执行结果返回给ui
;另一个管道为异常管道,当其他模块在产生不可恢复的异常后,会向这个管道发送'quit'
消息,接收到该命令后主循环直接退出;在检查完这两个管道后,主循环调用idle_commands()
,并设置0.1秒的延时,用于等待几个管道的数据
idle_commands
:该函数调用register_idle_function
函数注册的idle
函数,这个函数在bb.Command.runCommand()
中,通过
1 | self.cooker.configuration.server_register_idlecallback(self.cooker.runCommands, self.cooker) |
这段代码注册,可以看到,注册的函数为bb.cooker.runCommands
,然后该函数调用这个注册的函数,如果未找到注册函数,则调用select.select()
等待0.1秒后返回。
bb.cooker.runCommands
:该函数就是被注册的idle函数,他会被server
主循环周期的调用,而该函数的实际内容,则是调用bb.command.Command.runAsyncCommand
来执行一个已经就绪的异步命令bb.command.Command.runAsyncCommand
:该函数会判断当前cooker从状态,而分别调用updateCache()
函数或者调用command
对象的currentAsyncCommand
成员函数,这个函数会在多种情况下被赋值为某个函数对象和其参数组成的元组(command, options)
,当该函数被调用时,则会执行在currentAsyncCommand
注册的函数,而updateCache()
则会为启动其他的任务,例如parse
currentAsyncCommand
的赋值:currentAsyncCommand
只会在command.runCommand
函数中被赋值,而command.runCommand
函数,则会在server
对象的runCommand()
中被调用,server.runCommand()
的调用,则出现在ui
端的main()
中唯一一次主动调用server
的代码,这样,即是在ui
端的main
函数中,启动了
依赖关系如何解析
代码位于bb.runqueue.RunQueueData.prepare()
函数中的注释的PART A
部分和内嵌函数generate_recdeps
bb文件如何解析
入口位于bb.cooker.updateCache()
函数中,该函数中有如下代码:
1 | self.parser = CookerParser(self, filelist, masked) |
这段代码初始化了一个CookerParser
对象,这个对象的构造函数中,调用了self.start()
,因此这段代码直接启动了bb文件的解析,具体的start()
函数代码在bb/cooker.py
中的CookerParser
类中
UI端如何运作
由于在Yocto中,服务进程先于UI启动,因此第一次执行命令需要通过ui
传递给server
,而ui
的入口函数,则是位于lib/ui/ui_module_name.py
文件中的main()
函数,根据采用的不同的ui
模块(默认采用knotty.py),main
函数有不同的行为,这里以knotty.py中的main作为例子进行分析
bb.ui.knotty.main
这个函数为ui
端的入口函数,最核心的代码为
1 | #__file__ = 'bitbake/lib/bb/ui/knotty.py' |
这段代码,通过params.parseActions()
从用户调用的bitbake <target>
命令,解析出一个cmdline
字典,其中的action
键是一个列表,其中包含了要运行的命令的字符串格式,要构建的目标<target>
和构建的cmd(默认为build
),例如:cmdline[action]=["buildTarget", "zlib", "build"]
,就意味着即将要运行的命令为buildTarget
,构建目标为zlib
,cmd为build
;而msg
键对应了需要传送给server
端显示的消息,当命令行参数解析到不合适的内容时,则会发送给服务器结束命令,关闭ui
和server
进程。
如果没有出错,通常的第一个action
都是buildTarget
,这个action
随后被作为参数,传给bb.server.ServerCommunicator.runCommand()
函数,该函数调用服务端的函数bb.server.ProcessServer.runCommand
来执行命令
bb.server.ProcessServer.runCommand
:该函数将上面action
中的命令数据通过bb.cooker.command.runCommand()
进行处理,并将返回值通过管道发送给ui
端,这也是唯一一次ui
端显式的调用server
的函数。各种event的处理:在
bb.ui.knotty.main()
中,存在着一个while
循环,该循环读取服务端的管道,并根据服务端返回的命令执行结果和状态执行相应的代码,或者关闭服务端,或者继续发送命令。
buildTarget
在 bitbake/bb/cooker.py
中,有一个buildTarget
函数,该函数为在无任何参数的bitbake
命令时的服务端入口,例如:
1 | bitbake zlib #target 为 zlib |
这是服务端会调用buildTarget
作为如何,该函数如下:
1 | def buildTargets(self, targets, task): |
可以看到,这个函数做了以下几件事:
- 定义了一个内嵌函数
buildTargetsIdle
,看名字可以得知,该内嵌函数会作为idle
函数被注册到server
中,周期的被调用 self.buildSetVars()
用于设置一些和BUILDNAME
,BUILDTIME
等变量buildTaskData
用于生成任务数据,其中包括taskdata
,runlist
,和fulltargetlist
;其中,taskdata
是一个bb.taskdata.TaskData
类的实例,这个对象中包含了和该任务相关的信息,例如依赖,任务名等,runlist
则是该任务的各个目标的名称和对应的task,并以列表的形式进行存储,例如["base-files","do_build"]
就代表了目标base-files
,其task为do_build
,而fulltargetlist
则是所有target的列表- 通过
rq = bb.runqueue.RunQueue(self, self.data, self.recipecache, taskdata, runlist)
来构造一个RunQueue
实例,为随后的build工作做好准备 - 将定义的内嵌函数注册为
idle
回调函数,使其被周期地调用,因此,我们需要分析该函数的实现:
buildTargetsIdle
根据上面的代码,该函数主要执行了
rq.execute_runqueue()
函数,该函数位于bb/runqueue.py
中,而execute_runqueue()
又调用了_execute_runqueue()
,而_execute_runqueue()
的实际工作,是根据runqueue
的实际状态,进行不同的行为:- 在
runQueuePrepare
态,调用bb.runqueue.RunQueueData.prepare()
,这个函数是很相当长的函数,主要行为包括:- STEP A:解析出一个需要执行的任务列表,包括解析依赖
- STEP B:标记所有需要执行的任务
- STEP C:去掉不需要执行的任务
- STEP D:检测并确定最终的需要执行的任务列表
- 进入
runQueueSceneInit
状态
- 在
runQueueSceneInit
状态,调用runqueue.start_worker()
启动,启动工作进程,并构建一个RunQueueExecuteScenequeue
对象,将状态设置为runQueueSceneRun
- 在
runQueueSceneRun
状态,调用RunQueueExecuteScenequeue.execute()
,该函数会将准备好的task依次运行,随后,将状态设置为runQueueRunInit
- 在
runQueueRunInit
状态,会构造一个RunQueueExecuteTasks
对象,然后将状态设置为runQueueRunning
- 在
runQueueRunning
状态,会调用RunQueueExecuteTasks
对象的execute()
函数,该函数会执行在上面的RunQueueData
状态中准备的task,并进入runQueueCleanUp
状态 runQueueCleanUp
状态,调用RunQueueExecute.finish()
函数,并将状态设置为runQueueComplete
runQueueComplete
状态,销毁worker,然后该函数返回
- 在
载入cache的入口
入口函数是bb/cache.py
中的load_cachefile()
函数
run.do_xxx 脚本如何生成
在bb/build.py
中,存在exec_func
函数,该函数运行的某个函数,将会在build/tmp/work
中创建run.do_xxx.pid
名称的脚本,并运行它
如何生成image
yocto在构建完成所有的软件包后,会将所有构建的软件包放在${TMPDIR}/deploy
目录下,称之为软件源,在启动构建rootfs的活动(名为do_rootfs
的task)后,将会执行三个函数:
- create_manifest() 构建软件包的manifest用于test image,并且生成一个
package
列表为create_rootfs()
函数提供需要安装的软件包列表 - create_rootfs() 构建rootfs文件系统,包括执行
pre_cmd
,安装所需软件包,构建/etc ,/dev等目录,构建内核模块,运行ldconfig等,完成rootfs的构建 - create_image 根据image的压缩类型和文件系统类型,制作一个或多个image