
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/78172009 博客地址
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/64135653 上一周花了大部分时间重新拾起了之前落下的MIT6.824 2016的分布式课程,实现和调试了下Raft协议,虽然Raft协议相对其他容错分布式一致性协议如Paxos/Multi-Paxos/VR/Zab等来说更容易理解,但是在实现和调试过程中也遇到不少细节问题。虽然论文中有伪代码似的协议描述,但是要把每一小部分逻辑组合起来放到正确的位置还是需要不少思考和踩坑的,这篇文章对此做一个小结。 Raft实现 这里实现的主要是Raft基本的Leader Election和Log Replication部分,没有考虑Snapshot和Membership Reconfiguration的部分,因为前两者是后两者的实现基础,也是Raft协议的核心。MIT6.824 2016使用的是Go语言实现,一大好处是并发和异步处理非常直观简洁,不用自己去管理异步线程。 宏观 合理规划同步和异步执行的代码块,比如Heartbeat routine/向多个节点异步发送请求的routine 注意加锁解锁,每个节点的heartbeat routine/请求返回/接收请求都可能改变Raft结构的状态数据,尤其注意不要带锁发请求,很容易和另一个同时带锁发请求的节点死锁 理清以下几块的大体逻辑 公共部分的逻辑 发现小的term丢弃 发现大的term,跟新自身term,转换为Follower,重置votedFor 修改term/votedFor/log之后需要持久化 Leader/Follower/Candidate的Heartbeat routine逻辑 Leader Election 发送RequestVote并处理返回,成为leader后的逻辑(nop log replication) 接收到RequestVote的逻辑,如何投票(Leader Election Restriction) Log Replication 发送AppendEntries并处理返回(consistency check and repair),达成一致后的逻辑(更新commitIndex/nextIndex/matchIndex, apply log) 接收到AppendEntries的逻辑(consistency check and repair, 更新commitIndex,apply log) 细节 Leader Election timeout的随机性 timeout的范围,必须远大于rpc请求的平均时间,不然可能很久都选不出主,通常rpc请求在ms级别,所以可设置150~300ms 选主请求发送结束后,由于有可能在选主请求(RequestVote)的返回或者别的节点的选主请求中发现较大的term,而被重置为Follower,这时即使投票数超过半数也应该放弃成为Leader,因为当前选主请求的term已经过时,成为Leader可能导致在新的term中出现两个Leader.(注意这点是由于发送请求是异步的,同步请求发现较大的term后可直接修改状态返回) 每次发现较大的term时,自身重置为Follower,更新term的同时,需要重置votedFor,以便在新的term中可以参与投票 每次选主成功后,发送一条nop的日志复制请求,让Leader提交所有之前应该提交的日志,从而让Leader的状态机为最新,这样为读请求提供linearializability,不会返回stale data Log Replication Leader更新commitIndex时,需要严格按照论文上的限制条件(使用matchIndex),不能提交以前term的日志 对于同一term同一log index的日志复制,如果失败,应该无限重试,直到成功或者自身不再是Leader,因为我们需要保证在同一term同一log index下有唯一的一条日志cmd,如果不无限重试,有可能会导致以下的问题 五个节点(0, 1, 2, 3, 4), node 0为leader,复制一条Term n, LogIndex m, Cmd cmd1的日志 node 1收到cmd1的日志请求,node 2, 3, 4未收到 如果node 0不无限重试而返回,此时另一个cmd2的日志复制请求到达,leader 0使用同一个Term和LogIndex发送请求 node 2, 3, 4收到cmd2的请求,node 1未收到 node 1通过election成为新的leader(RequestVote的检查会通过,因为具有相同的Term和LogIndex) node 1发送nop提交之前的日志,cmd1被applied(consistency check会通过,因为PrevLogTerm和PrevLogIndex相同) cmd2则被node 2, 3, 4 applied cmd1和cmd2发生了不一致 测试和其他一些问题 测试过程中发现MIT6.824测试有两处小问题 一个是TestReElection中隔离leader1,重连leader1后需要睡眠至少一个心跳周期,让leader1接收到leader1的心跳而转换为follower 另一个是cfg.one中提交一个日志后需要检查所有参与节点applied日志后的结果,所以需要leader和所有follower尽早applied日志,但是follower总是滞后于leader至少一个心跳周期或者一次AppendEntries请求的,所以这个检查会失败 Start异步执行的问题? 由于测试代码直接阻塞调用Start,需要获取Start返回的Term/Index等,当日志复制请求失败时,Start会无限重试,从而阻塞测试代码,而无法重新加入节点,到时整个测试阻塞。 如果在单独的goroutine中执行Start的逻辑,log index的获取是序列化的(Raft需要保证前面所有的日志提交后才能提交本条日志),所以本质上后面的请求需要等待前面的请求完成并持久化日志然后再拿下一个log index,所以还是序列化的。而且,并发之后给commitIndex和apply log的逻辑引入了更多的复杂度。 一些优化点在保证基本协议正确性的前提下如何实现? 锁的优化 pipeline batch 客户端交互,保证exactly once语义 总结 一个工程级别的分布式一致性协议实现并不容易,要注意的细节很多,不仅要保证正确地实现协议,还要考虑优化点,在优化整个系统的性能时保证系统的正确性。 分布式系统尤其是像分布式一致性协议这样的复杂系统需要大量的测试来保证系统的正确性,算法本身简洁的描述忽略了非常多实际工程中会遇到的各种fault,在工程实现之后很难保证其正确性,有些case需要经历多次状态转换才能发现失败原因。 大致实现了Raft之后,再回过头去看Paxos/Multi-Paxos,会更明白Raft为了简单做的trade-off 保证协议safety性质的前提下,通过增加以下三个条件来简化Leader恢复或者说View Change过程中的状态恢复,保证日志从Leader上单向流动到Follower(而这个过程又可以合并到AppendEntries日志复制的逻辑中,即consistency check),这个过程往往是关键和最复杂的步骤。 选主的时候满足发请求的节点和被请求的节点日志至少一样新,保证选主成功后Leader上的日志最新 日志必须顺序提交(对数据库事务日志来说可能并不友好) 新选出的Leader不能直接提交以前Term的日志,需要写入一条当前Term的日志后才能提交之前Term的日志 最后放上简单的代码供参考:-), Raft
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/60475459 数据交互协议和RPC框架对于分布式系统来说是必不可少的组件,这个系列主要用来分析Protobuf和GRPC的实现原理,本文主要介绍Protobuf生成代码的流程以及Protobuf与GRPC之间的交互方式。 简要描述 Protobuf Protobuf主要由三大部分构成: Core: 包括核心的数据结构比如Message和Service等等 Compiler: proto文件的Tokenizer和Parser; 代码生成器接口以及不同语言的具体实现, 并提供插件机制; protoc的主程序 Runtime: 支撑不同语言的基础数据结构,通常和Core的主要数据结构对应,Ruby和PHP等直接以扩展的形式封装使用Core中的数据结构,而Go和Java则重新实现了一套对应的数据结构 GRPC GRPC也可以看做三大部分构成: Core: C语言实现的channel, http, transport等核心组件 Compiler: 各个语言的Protobuf插件,主要作用是解析proto文件中的service并生成对应的server和client代码接口 Runtime: 支撑不同语言的通信框架,通常是封装Core中的C实现,但是Go和Java是完全重新实现的整个框架(grpc-go和grpc-java) 基本流程 proto files -> tokenizer and parser -> FileDescriptor -> CodeGenerator(内部注册的生成器实现或者外部插件比如grpc插件) -> code 代码生成主要流程的源码分析 入口 // protobuf/src/google/protobuf/compiler/main.cc int main(int argc, char* argv[]) { google::protobuf::compiler::CommandLineInterface cli; // 注册插件的前缀,当使用protoc --name_out=xx生成代码时,如果name对应的插件 // 没有在内部注册那么默认当做插件,会查找protoc-gen-name的程序是否存在,如 // 果指定了--plugin=protoc-gen-name=/path/to/bin参数,则优先使用此参数设置 // 的路径这是grpc的protobuf插件以及go的protobuf实现与protoc命令交互的机制。 cli.AllowPlugins("protoc-"); // 注册内部代码生成器插件 google::protobuf::compiler::cpp::CppGenerator cpp_generator; cli.RegisterGenerator("--cpp_out", "--cpp_opt", &cpp_generator, "Generate C++ header and source."); /* ... */ return cli.Run(argc, argv); } 参数和proto文件解析 // protobuf/src/google/protobuf/compiler/command_line_interface.cc int CommandLineInterface::Run(int argc, const char* const argv[]) { /* ... */ // 1. 解析参数,核心参数是--plugin, --name_out, -I, --import_path等 // --plugin被解析成<name, path>的KV形式,--name_out可以通过--name_out=k=v:out_dir // 的形式指定k=v的参数,这个参数会被传递给代码生成器(插件),这个参数有时很有用, // 比如go的protobuf实现中,使用protoc --go_out=plugins=grpc:. file.proto来传递 // plugins=grpc的参数给protoc-gen-go,从而在生成的时候会一并生成service的代码 switch (ParseArguments(argc, argv)) { /* ... */ } // 2. Tokenizer和Parser解析proto文件,生成FileDescriptor Importer importer(&source_tree, &error_collector); for (int i = 0; i < input_files_.size(); i++) { /* ... */ // 词法和语法分析 const FileDescriptor* parsed_file = importer.Import(input_files_[i]) /* ... */ } // 3. 调用CodeGenerator生成代码 for (int i = 0; i < output_directives_.size(); i++) { /* ... */ // 按照命令行的--name1_out=xx, --name2_out=xx先后顺序多次调用,生成代码 if (!GenerateOutput(parsed_files, output_directives_[i], *map_slot)) { STLDeleteValues(&output_directories); return 1; } } } 代码生成 bool CommandLineInterface::GenerateOutput( const std::vector<const FileDescriptor*>& parsed_files, const OutputDirective& output_directive, GeneratorContext* generator_context) { // 不是内部注册的CodeGenerator,而是插件 if (output_directive.generator == NULL) { /* ... */ // 插件的可执行文件全名protoc-gen-name string plugin_name = PluginName(plugin_prefix_ , output_directive.name); // 传递给插件的参数 string parameters = output_directive.parameter; if (!plugin_parameters_[plugin_name].empty()) { if (!parameters.empty()) { parameters.append(","); } parameters.append(plugin_parameters_[plugin_name]); } // 开子进程执行插件返回生成的代码数据 if (!GeneratePluginOutput(parsed_files, plugin_name, parameters, generator_context, &error)) { std::cerr << output_directive.name << ": " << error << std::endl; return false; } } else { // 内部已经注册过的CodeGenerator,直接调用 // 传递的参数 string parameters = output_directive.parameter; if (!generator_parameters_[output_directive.name].empty()) { if (!parameters.empty()) { parameters.append(","); } parameters.append(generator_parameters_[output_directive.name]); } // 生成 if (!output_directive.generator->GenerateAll( parsed_files, parameters, generator_context, &error)) { /* ... */ } } } GRPC的protobuf插件实现 // GRPC的service相关的生成器位于grpc/src/compiler目录下, // 主要实现grpc::protobuf::compiler::CodeGenerator接口, // 这里以C++为例 // grpc/src/compiler/cpp_plugin.cc class CppGrpcGenerator : public grpc::protobuf::compiler::CodeGenerator { /* ... */ virtual bool Generate(const grpc::protobuf::FileDescriptor *file, const grpc::string &parameter, grpc::protobuf::compiler::GeneratorContext *context, grpc::string *error) const { // 生成头文件相关代码(.grpc.pb.h) grpc::string header_code = // 版权声明,宏,include grpc_cpp_generator::GetHeaderPrologue(&pbfile, generator_parameters) + // 导入grpc内部头文件,核心类的前向声明 grpc_cpp_generator::GetHeaderIncludes(&pbfile, generator_parameters) + // Service, StubInterface接口相关 grpc_cpp_generator::GetHeaderServices(&pbfile, generator_parameters) + // namespace和宏的结束标识 grpc_cpp_generator::GetHeaderEpilogue(&pbfile, generator_parameters); std::unique_ptr<grpc::protobuf::io::ZeroCopyOutputStream> header_output( context->Open(file_name + ".grpc.pb.h")); grpc::protobuf::io::CodedOutputStream header_coded_out(header_output.get()); header_coded_out.WriteRaw(header_code.data(), header_code.size()); // 生成源码(.grpc.pg.cc) grpc::string source_code = grpc_cpp_generator::GetSourcePrologue(&pbfile, generator_parameters) + grpc_cpp_generator::GetSourceIncludes(&pbfile, generator_parameters) + grpc_cpp_generator::GetSourceServices(&pbfile, generator_parameters) + grpc_cpp_generator::GetSourceEpilogue(&pbfile, generator_parameters); std::unique_ptr<grpc::protobuf::io::ZeroCopyOutputStream> source_output( context->Open(file_name + ".grpc.pb.cc")); grpc::protobuf::io::CodedOutputStream source_coded_out(source_output.get()); source_coded_out.WriteRaw(source_code.data(), source_code.size()); /* ... */ } }
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/53410708 容器的生命周期涉及到内部的程序实现和面向用户的命令行界面,runc内部容器状态转换操作、runc命令的参数定义的操作、docker client定义的容器操作是不同的,比如对于docker client的create来说, 语义和runc就完全不同,这一篇文章分析runc的容器生命周期的抽象、内部实现以及状态转换图。理解了runc的容器状态转换再对比理解docker client提供的容器操作命令的语义会更容易些。 容器生命周期相关接口 最基本的required的接口 Start: 初始化容器环境并启动一个init进程,或者加入已有容器的namespace并启动一个setns进程;执行postStart hook; 阻塞在init管道的写端,用户发信号替换执行真正的命令 Exec: 读init管道,通知init进程或者setns进程继续往下执行 Run: Start + Exec的组合 Signal: 向容器内init进程发信号 Destroy: 杀掉cgroups中的进程,删除cgroups对应的path,运行postStop的hook 其他 Set: 更新容器的配置信息,比如修改cgroups resize等 Config: 获取容器的配置信息 State: 获取容器的状态信息 Status: 获取容器的当前运行状态: created、running、pausing、paused、stopped Processes: 返回容器内所有进程的列表 Stats: 容器内的cgroups统计信息 对于linux容器定义并实现了特有的功能接口 Pause: free容器中的所有进程 Resume: thaw容器内的所有进程 Checkpoint: criu checkpoint Restore: criu restore 接口在内部的实现 对于Start/Run/Exec的接口是作为不同os环境下的标准接口对开发者暴露,接口在内部的实现有很多重复的部分可以统一,因此内部的接口实际上更简洁,这里以linux容器为例说明 对于Start/Run/Exec在内部实现实际上只用到下面两个函数,通过传入flag(容器是否处于stopped状态)区分是创建容器的init进程还是创建进程的init进程 start: 创建init进程,如果status == stopped,则创建并执行newInitProcess,否则创建并执行newSetnsProcess,等待用户发送执行信号(等在管道写端上),用用户的命令替换掉 exec: 读管道,发送执行信号 Start直接使用start Run实际先使用start(doInit = true),然后exec Exec实际先使用start(doInit = false), 然后exec 对用户暴露的命令行参数与容器接口的对应关系,以linux容器为例 create -> Start(doInit = true) start -> Exec run -> Run(doInit = true) exec -> Run(doInit = false) kill -> Signal delete -> Signal and Destroy update -> Set state -> State events -> Stats ps -> Processes list linux specific pause -> Pause resume -> Resume checkpoint -> Checkpoint restore -> Restore runc命令行的动作序列对容器状态机的影响 对于一个容器的生命周期来说,稳定状态有4个: stopped、created、running、paused 注意下面状态转换图中的动作是runc命令行参数动作,不是容器的接口动作,这里没考虑checkpoint相关的restore状态 delete |------| /-------------------------------------------------------------| | | / |----- start ---| | | V / | | | |---------| ----------- create ----------> |---------|<---------/ | | stopped | | created |------------| | |---------| <-------- delete(with kill)--- |---------| | | ^ ^ | | | | | | | run | |--------------- delete(-f with kill) ---| exec | | delete(-f with kill) | | | | | | | | | | resume | V | | |---------| -----------------------------> |----------| | | | paused | | running |<----------|-------| |---------| <---------------------------- |----------| | ^ pause ^ | | | | | | | |--exec--| | | | |--------------------------- pause ---------------------------|
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/52831354 最近使用Python遇到两个非常不好定位的问题,表现都是Python主进程hang住。最终定位出一个是subprocess模块的问题,一个是threading.Timer线程的问题。 subprocess模块不当使用的问题 Python的subprocess比较强大,基本上能替换os.system、os.popen、commands.getstatusoutput的功能,但是在使用的过程中需要注意参数stdin/stdout/stderr使用subprocess.PIPE的情况,因为管道通常会有默认的buffer size(Linux x86_64下实测是64K,这里有个疑问io.DEFAULT_BUFFER_SIZE是8K,而ulimit -a的pipe size为512 * 8 = 4K?),父进程如果不使用communicate消耗掉子进程write pipe(stdout/stderr)中的数据,直接进入wait,此时子进程可能阻塞在了pipe的写上,从而导致父子进程都hang住。下面是测试代码。 # main.py #!/usr/bin/env python # encoding: utf-8 import subprocess import os import tempfile import sys import traceback import commands # both parent and child process will hang # if run.py stdout/stderr exceed 64K, since # parent process is waiting child process exit # but child process is blocked by writing pipe def testSubprocessCallPipe(): # call: just Popen().wait() p = subprocess.Popen(["python", "run.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ret = p.wait() print ret # will not hang since the parent process which # call communicate will poll or thread to comsume # the pipe buffer, so the child process can write # all it's data to stdout or stderr pipe and it will # not be blocked. def testSubprocessCommunicate(): p = subprocess.Popen(["python", "run.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) print p.communicate()[0] # will not hang since sys.stdout and sys.stderr # don't have 64K default buffer limitation, child # process can write all it's data to stdout or # stderr fd and exit def testSubprocessCallStdout(): # call: just Popen().wait() p = subprocess.Popen(["python", "run.py"], stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr) ret = p.wait() print ret # will not hang since file has no limitation of 64K def testSubprocessCallFile(): stdout = tempfile.mktemp() stderr = tempfile.mktemp() print "stdout file %s" % (stdout,), "stderr file %s" % (stderr,) stdout = open(stdout, "w") stderr = open(stderr, "w") p = subprocess.Popen(["python", "run.py"], stdin=None, stdout=stdout, stderr=stderr) ret = p.wait() print ret print os.getpid() # not hang print "use file" testSubprocessCallFile() # not hang print "use sys.stdout and sys.stderr" testSubprocessCallStdout() # not hang print "use pipe and communicate" testSubprocessCommunicate() # hang print "use pipe and call directly" testSubprocessCallPipe() # run.py import os print os.getpid() string = "" # > 64k will hang for i in range(1024 * 64 - 4): string = string + "c" # flush to my stdout which might # be sys.stdout/pipe/fd... print string 另外,在subprocess模块源码中还注释说明了另外一种由于fork -> 子进程gc -> exec导致的进程hang住,详细信息可以阅读subprocess模块源码。 threading.Timer的使用不当的问题 定位步骤: pstack 主进程,查看python语言源码的c调用栈,追踪主线程(图中线程1)的各个函数调用栈的python源码,猜测是阻塞在threading._shutdown方法上,修改threading模块源码,并添加日志,定位确实阻塞在_exitFunc的循环join thread上。 线程2的表现是不断创建不断退出,为threading.start入口添加打印traceback,最终定位在一个模块的心跳计时器。调大心跳周期,观察步骤1中的线程id,确定是心跳计时器线程。注: approach 2中可用ctrl-c构造异常,构造hang住的情况。 重现poc import threading import time import sys # approach 1 class TestClassA(object): timer = None count = 0 def __del__(self): print "called del" if self.timer is not None: self.timer.cancel() def new_timer(self): # current reference 3 + getrefcount 1 = 4 print "in new_timer: %d" % (sys.getrefcount(self)) print "ffff" self.count += 1 # my father timer thread exit, ref count -1, but start # a new thread will make it still 3 self.timer = threading.Timer(1, self.new_timer) self.timer.start() def start_timer(self): self.timer = threading.Timer(1, self.new_timer) self.timer.start() def test(): t = TestClassA() print "enter test: %d" % (sys.getrefcount(t),) # 2 t.start_timer() # pass ref to a new timer thread through self.new_timer: 3 print "before out test: %d" % (sys.getrefcount(t),) # 3 # approach 2 class TestClassB(object): timer = None count = 0 def __del__(self): print "called del" def func(*ins): print "fffff" ins[0].count += 1 ins[0].timer = threading.Timer(1, func, ins) # will increase reference count of ins ins[0].timer.start() def test_in_scope(): t = TestClassB() print "enter test_in_scope: %d" % (sys.getrefcount(t)) t.timer = threading.Timer(1, func, (t,)) t.timer.start() while t.count < 4: time.sleep(1) #try: # while t.count < 4: # time.sleep(1) #except: # pass # if we interrupt or raise some other exceptions and not catch that, # will hang t.timer.cancel() print "before exit test_in_scope: %d" % (sys.getrefcount(t)) # approachh 3 def test_closure(): t = TestClassA() print "enter test_closure: %d" % (sys.getrefcount(t),) def func_inner(): print "ffffffff" t.timer = threading.Timer(1, func_inner) # will increase reference count t.count += 1 t.timer.start() print "in func: %d" % (sys.getrefcount(t)) t.timer = threading.Timer(1, func_inner) t.timer.start() print "before out test_closure: %d" % (sys.getrefcount(t),) #print "================= test approach 1 ===============" #print "before test" #test() #print "after test" print "================= test approach 2 ===============" print "before test_in_scope" test_in_scope() print "after test_in_scope" #print "================= test approach 3 ================" #print "before test_closure" #test_closure() #print "after test_closure" print "before exit main thread, it will wait and join all other threads" sys.exit()
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/52758877 本文主要介绍Docker的一些基本概念、Docker的源码分析、Docker的一些issue、Docker周边生态等等。 基本概念 Basics docker大体包括三大部分,runtime(container)、image(graphdriver)、registry,runtime提供环境的隔离与资源的隔离和限制,image提供layer、image、rootfs的管理、registry负责镜像存储与分发。当然,还有其他一些比如data volume, network等等,总体来说还是分为计算、存储与网络。 computing 接口规范 命名空间隔离、资源隔离与限制的实现 造坑与入坑 network 接口规范与实现 bridge veth pair for two namespace communication bridge and veth pair for multi-namespace communication do not support multi-host overlay docker overlay netowrk: with swarm mode or with kv etcd/zookeeper/consul -> vxlan coreos flannel -> 多种backend,udp/vxlan… ovs weave -> udp and vxlan,与flannel udp不同的是会将多container的packet一块打包 一篇对比 calico pure layer 3 null 与世隔绝 host 共享主机net namespace storage graphdriver(layers,image and rootfs) graph:独立于各个driver,记录image的各层依赖关系(DAG),注意是image不包括运行中的container的layer,当container commit生成image后,会将新layer的依赖关系写入 device mapper snapshot基于block,allocation-on-demand 默认基于空洞文件(data and metadata)挂载到回环设备 aufs diff:实际存储各个layer的变更数据 layers:每个layer依赖的layers,包括正在运行中的container mnt:container的实际挂载根目录 overlayfs vfs btrfs … volume driver接口 local driver flocker: container和volume管理与迁移 rancher的convoy:多重volume存储后端的支持device mapper, NFS, EBS…,提供快照、备份、恢复等功能 数据卷容器 registry:与docker registry交互 支持basic/token等认证方式 token可以基于basic/oauth等方式从第三方auth server获取bearer token tls通信的支持 libkv 支持consul/etcd/zookeeper 分布式存储的支持 security docker libseccomp限制系统调用(内部使用bpf) linux capabilities限制root用户权限范围scope user namespace用户和组的映射 selinux apparmor … image and registry Other Stuffs 迁移 CRIU: Checkpoint/Restoreuser In User namespace CRAK: Checkpoint/Restart as A Kernel module 开放容器标准 runtime runc runv rkt(appc) libcontainer and runc containerd docker client and docker daemon OCI标准和runC原理解读 Containerd:一个控制runC的守护进程 runC:轻量级容器运行环境 源码分析 for docker 1.12.* 主要模块 docker client DockerCli => 封装客户端的一些配置 command => 注册docker client支持的接口 docker/engine-api/client/[Types|Client|Request|Transport|Cancellable] => 规范访问dockerd apiserver的接口 docker engine daemon DaemonCli apiserver => 接受docker client请求,转发到daemon rpc daemon => 其他功能比如设置docker根目录、inti process、dockerd运行的user namespace等其他信息 包含一个很重要的部分: remote => 通过libcontainerd与containerd的grpc server后端打交道 cluster => swarm mode相关 containerd containerd => grpc server,提供给dockerd操作容器、进程等的接口,提供containerd、containerd-shim、containerd-ctr工具 libcontainer(runc) libcontainer(runc) 提供容器的生命周期相关的接口标准,提供runc工具 基本流程:docker client ==http==> dockerd apiserver ====> remote grpc client(libcontainerd) ==grpc==> containerd ==cmd==> containerd-shim ==cmd==> runc exec/create等 ==cmd==> runc init初始化坑内init进程环境,然后execve替换成容器指定的应用程序 详细分析 客户端部分省略,这里主要介绍docker engine daemon(DaemonCli)、containerd以及libcontainer(runc)三大部分。 DaemonCli: 启动docker daemon与containerd daemon的核心对象,包含三大部分,apiserver、Daemon对象和cluster apiserver middleware routers 通用模式 提供backend具体操作的后端接口(实际全在daemon.Daemon实现,而daemon.Daemon会作为所有router的backend) 提供解析请求的routers函数(实际调用backend接口) 注册routers build => docker build container => container创建启停等 image => 镜像 network => 网络 plugin => 插件机制 swarm => swarm模式相关 volumn => 数据卷 system => 系统信息等 我们可以用nc手动测试apiserver,具体实现的接口可以参考标准文档或者api/server下的源码 执行命令即可看到json输出(还有个python的客户端lib docker-py) echo -e “GET /info HTTP/1.0\r\n” | nc -U /var/run/docker.sock echo -e “GET /images/json HTTP/1.0\r\n” | nc -U /var/run/docker.sock daemon.Daemon对象 daemon除了处理engine daemon需要的通用环境(比如storage driver等)外,还包括registry部分和与containerd交互的grpc接口client(libcontainerd.Client/libcontainerd.Remote相关)。在DaemonCli的初始化过程中会由libcontainerd.New创建libcontainerd.remote,启动containerd daemon(grpc server)并且为docker engine daemon注入containerd/types中规范的与containerd daemon通信的grpc接口client 以docker pause為例,整個調用鏈條為: docker client -> apiserver container router postContainerPause -> daemon.Daemon.ContainerPause(backend) -> backend.containerd.Pause -> libcontainerd.Client.Pause -> remote.apiClient.UpdateContainer -> containerd.APIClient.UpdateContainer -> grpc.UpdateContainer -> containerd daemon UpdateContainer -> 调用containerd-shim containerid container_path runc -> 调用runc命令 说明: containerd是一个从docker daemon中抽出来的项目,提供操作runc的界面(包括一个daemon grpc server、一个ctr客户端工具用grpc.APIClient与grpc server通信、以及containerd-shim负责调用runc命令处理容器生命周期),runc提供的只是一个容器生命周期lib标准和cli工具,而没有daemon。 可以看出,runc(libcontainerd)提供了runtime的lib接口标准,不同os可以实现此接口屏蔽容器的具体实现技术细节;而containerd提供了一个基于libcontainerd接口的server以及cli工具(主要是grpc规范了);而docker daemon(engine)的apiserver提供的是docker client的restful http接口,会通过containerd的grpc Client标准接口与containerd的server通信。我们可以看到”/var/run/docker/libcontainerd/docker-containerd.sock”和”/var/run/docker.sock”,如上面通过nc与docker daemon直接通信,我们也可以使用grpc client与libcontainerd的daemon直接通信 综上,不难看出docker提供的几个主要二进制文件是干嘛的了…(docker/dockerd/docker-containerd/docker-containerd-shim/docker-containerd-ctr/docker-runc) 用runc直接操作容器: docker-runc list 用docker-containerd-ctr 通过docker-containerd grpc Server操作容器: docker-containerd-ctr –address “unix:///var/run/docker/libcontainerd/docker-containerd.sock” containers list 用docker通过dockerd、docker-containerd操作容器: docker ps 拆分的好处显而易见:标准化、解耦、新特性的实验、换daemon无需停止容器等等 cluster 這一部分與swarm相关,实际上是把swarmkit集成到了docker engine daemon中 每次启动docker engine daemon时会检查/var/lib/docker/swarm目录下是否有状态文件,如果有则需要恢复集群,重新启动节点;否则,直接返回,不开启swarm mode swarm中的节点有ManagerNode和WorkerNode之分,worker可以被promote成manager,manager也可以被demote回worker。在节点加入集群时可以指定加入的角色是worker还是manager。默认启动一个manager节点 containerd 容器元数据、提供管理容器生命周期的grpc server以及ctr 客户端工具,具体的容器的操作是通过containerd-shim调用runc命令,每个容器的init进程在容器外部会有对应的containerd-shim进程。 提供了一套任务执行机制,把对容器的生命周期的操作用Task/Worker模型抽象,提供更高的性能 从docker engine daemon拆分,使得engine daemon升级时容器不用stop 简单流程 核心的对象: grpc server、supervisor、worker、task、runtime(處理container和process相關元數據等)等 主routine的grpc apiserver等待grpc请求 -> supervisor server handleTask -> 放入supervisor的tasks chan -> worker从tasks chan中取出执行 -> shim -> runc libcontainer(or runc) 未完待续 从containerd到runc到实际的坑内进程起来经过的进程模型(以下起进程都是通过go的cmd) containerd的worker启动containerd-shim进程,传递参数shim containerdid containerpath runtime(其中runtime默认为runc),并且给runc传递exec/create的行为参数,起好坑。 containerd-shim启动runc exec/create进程,等待runc进程的结束,负责容器内的init进程的退出时的清理工作。containerd-shim与containerd daemon进程通信是通过control和exit两个具名管道文件。 runc exec/create作为父进程负责创建容器内的init进程,并用管道与init进程通信,这个init进程实际上是执行runc init命令,初始化容器环境,然后等待containerd执行runc start的信号,让用户的进程替换容器中的init,在容器中执行起来。 runc init进程负责初始化整个环境,包括清除所有runc exec/create父进程的环境变量,加载docker engine daemon传下来的docker client和docker image中指定的环境变量,设置namespace等等,然后等在管道的child上,等待runc exec/create父进程发送process的json配置文件,runc init坑内进程拿到这个配置文件,初始化所有的坑内环境,然后等待在exec.fifo具名管道文件上,等待runc start发送信号,然后开始execve用用户的程序替换掉runc init。 相关系统 Docker和Mesos Container建坑流程和进程模型对比 注: P代表进程, L代表线程 Docker containerd的worker启动containerd-shim进程,传递参数shim containerdid containerpath runtime(其中runtime默认为runc),并且给runc传递exec/create的行为参数,起好坑。 containerd-shim启动runc exec/create进程,等待runc进程的结束,负责容器内的init进程的退出时的清理工作。containerd-shim与containerd daemon进程通信是通过control和exit两个具名管道文件。 runc exec/create作为父进程负责创建容器内的init进程,并用管道与init进程通信,这个init进程实际上是执行runc init命令,初始化容器环境,然后等待containerd执行runc start的信号,让用户的进程替换容器中的init,在容器中执行起来。 runc init进程负责初始化整个环境,包括清除所有runc exec/create父进程的环境变量,加载docker engine daemon传下来的docker client和docker image中指定的环境变量,设置namespace等等,然后等在管道的child上,等待runc exec/create父进程发送process的json配置文件,runc init坑内进程拿到这个配置文件,初始化所有的坑内环境,然后等待在exec.fifo具名管道文件上,等待runc start发送信号,然后开始execve用用户的程序替换掉runc init。 Mesos Native Linux Container 基本模型 与docker containerd的主进程和matrix-agent的ContainerManager主线程类似,executor(mesos默认提供Command、Container两种executor)起一进程负责维护containers list的内存状态,并且fork&exec执行容器的启动 建坑流程 Creates a “freezer” cgroup for the container. Creates posix “pipe” to enable communication between host (parent process) and container process. Spawn child process(container process) using clone system call. Moves the new container process to the freezer hierarchy. Signals the child process to continue (exec’ing) by writing a character to the write end of the pipe in the parent process. Mesos todo Kubernetes todo issues 记录一些Docker相关项目遇到的一些bug或者issue Docker overlayfs在centos6上的bug,某些容器启动时会触发(比如ubuntu) 目前解决办法: 升级内核到3.18.*(另外一个trick的办法是先在ubuntu的机器上export然后import,再push到仓库) https://github.com/docker/docker/issues/10294#issuecomment-212620653 https://github.com/docker/docker/issues/10294 127.16.0.0网段的路由默认被设置导致创建网络失败 -> route del … -> 但是导致公司内网无法访问机器 解决办法 手动创建bridge,启动docker时,避开机器路由的默认网段(比如10.,172.,192.*),尤其注意公司机器路由了大网段,docker可能找到一个子网段,还是能成功启动,但是后续创建网络会失败。。。 先route del机器的路由网段,docker network create,再设置route(选这种方式绕过…) docker default local pool 参考https://github.com/docker/libnetwork/issues/779 参考https://github.com/docker/docker/issues/21847 devicemapper storage driver依赖udev,而udev不支持static linked的docker binary,需要dynamic的docker binary Harbor harbor的tags和manifests api连接registry泄露fd导致进程超过最大可用fd在新的请求解析dns时失败 定位bug应该是每次请求接口时新建client的同时新建了tarnsport对象,从而导致上一次请求的transport的连接无法复用,从而泄露fd。解决办法是使用对每个host的请求使用一个全局的transport和client 或者关闭transport的keepalive或者在发请求的时候加req.Close=true,关闭连接重用 Distribution(Registry v2) Distribution的http.Server没有设置ReadTimeout和WriteTimeout,如果客户端与registry通信没有注意连接重用的问题,则可能导致泄露fd harbor与distribution通信建立的连接会泄露fd,导致registry和harbor都会由于操作进程限制的fd数量而请求失败 通常不可能直接设置registry的timeout,因为对于较大的镜像可能导致下载超时 Mesos todo Kubernetes todo
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/51705667 主要介绍内核抢占的相关概念和具体实现,以及抢占对内核调度和内核竞态和同步的一些影响。 (所用内核版本3.19.3) 1. 基本概念 用户抢占和内核抢占 用户抢占发生点 当从系统调用或者中断上下文返回用户态的时候,会检查need_resched标志,如果被设置则会重新选择用户态task执行 内核抢占发生点 当从中断上下文返回内核态的时候,检查need_resched标识以及__preemp_count计数,如果标识被设置,并且可抢占,则会触发调度程序preempt_schedule_irq() 内核代码由于阻塞等原因直接或间接显示调用schedule,比如preemp_disable时可能会触发preempt_schedule() 本质上内核态中的task是共享一个内核地址空间,在同一个core上,从中断返回的task很可能执行和被抢占的task相同的代码,并且两者同时等待各自的资源释放,也可能两者修改同一共享变量,所以会造成死锁或者竞态等;而对于用户态抢占来说,由于每个用户态进程都有独立的地址空间,所以在从内核代码(系统调用或者中断)返回用户态时,由于是不同地址空间的锁或者共享变量,所以不会出现不同地址空间之间的死锁或者竞态,也就没必要检查__preempt_count,是安全的。__preempt_count主要负责内核抢占计数。 2. 内核抢占的实现 percpu变量__preempt_count 抢占计数8位, PREEMPT_MASK => 0x000000ff 软中断计数8位, SOFTIRQ_MASK => 0x0000ff00 硬中断计数4位, HARDIRQ_MASK => 0x000f0000 不可屏蔽中断1位, NMI_MASK => 0x00100000 PREEMPTIVE_ACTIVE(标识内核抢占触发的schedule) => 0x00200000 调度标识1位, PREEMPT_NEED_RESCHED => 0x80000000 __preempt_count的作用 抢占计数 判断当前所在上下文 重新调度标识 thread_info的flags thread_info的flags中有一个是TIF_NEED_RESCHED,在系统调用返回,中断返回,以及preempt_disable的时候会检查是否设置,如果设置并且抢占计数为0(可抢占),则会触发重新调度schedule()或者preempt_schedule()或者preempt_schedule_irq()。通常在scheduler_tick中会检查是否设置此标识(每个HZ触发一次),然后在下一次中断返回时检查,如果设置将触发重新调度,而在schedule()中会清除此标识。 // kernel/sched/core.c // 设置thread_info flags和__preempt_count的need_resched标识 void resched_curr(struct rq *rq) { /*省略*/ if (cpu == smp_processor_id()) { // 设置thread_info的need_resched标识 set_tsk_need_resched(curr); // 设置抢占计数__preempt_count里的need_resched标识 set_preempt_need_resched(); return; } /*省略*/ } //在schedule()中清除thread_info和__preempt_count中的need_resched标识 static void __sched __schedule(void) { /*省略*/ need_resched: // 关抢占读取percpu变量中当前cpu id,运行队列 preempt_disable(); cpu = smp_processor_id(); rq = cpu_rq(cpu); rcu_note_context_switch(); prev = rq->curr; /*省略*/ //关闭本地中断,关闭抢占,获取rq自旋锁 raw_spin_lock_irq(&rq->lock); switch_count = &prev->nivcsw; // PREEMPT_ACTIVE 0x00200000 // preempt_count = __preempt_count & (~(0x80000000)) // 如果进程没有处于running的状态或者设置了PREEMPT_ACTIVE标识 //(即本次schedule是由于内核抢占导致),则不会将当前进程移出队列 // 此处PREEMPT_ACTIVE的标识是由中断返回内核空间时调用 // preempt_schdule_irq或者内核空间调用preempt_schedule // 而设置的,表明是由于内核抢占导致的schedule,此时不会将当前 // 进程从运行队列取出,因为有可能其再也无法重新运行。 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { // 如果有信号不移出run_queue if (unlikely(signal_pending_state(prev->state, prev))) { prev->state = TASK_RUNNING; } else { // 否则移除队列让其睡眠 deactivate_task(rq, prev, DEQUEUE_SLEEP); prev->on_rq = 0; // 是否唤醒一个工作队列内核线程 if (prev->flags & PF_WQ_WORKER) { struct task_struct *to_wakeup; to_wakeup = wq_worker_sleeping(prev, cpu); if (to_wakeup) try_to_wake_up_local(to_wakeup); } } switch_count = &prev->nvcsw; } /*省略*/ next = pick_next_task(rq, prev); // 清除之前task的need_resched标识 clear_tsk_need_resched(prev); // 清除抢占计数的need_resched标识 clear_preempt_need_resched(); rq->skip_clock_update = 0; // 不是当前进程,切换上下文 if (likely(prev != next)) { rq->nr_switches++; rq->curr = next; ++*switch_count; rq = context_switch(rq, prev, next); cpu = cpu_of(rq); } else raw_spin_unlock_irq(&rq->lock); post_schedule(rq); // 重新开抢占 sched_preempt_enable_no_resched(); // 再次检查need_resched if (need_resched()) goto need_resched; } __preempt_count的相关操作 /////// need_resched标识相关 /////// // PREEMPT_NEED_RESCHED位如果是0表示需要调度 #define PREEMPT_NEED_RESCHED 0x80000000 static __always_inline void set_preempt_need_resched(void) { // __preempt_count最高位清零表示need_resched raw_cpu_and_4(__preempt_count, ~PREEMPT_NEED_RESCHED); } static __always_inline void clear_preempt_need_resched(void) { // __preempt_count最高位置位 raw_cpu_or_4(__preempt_count, PREEMPT_NEED_RESCHED); } static __always_inline bool test_preempt_need_resched(void) { return !(raw_cpu_read_4(__preempt_count) & PREEMPT_NEED_RESCHED); } // 是否需要重新调度,两个条件:1. 抢占计数为0;2. 最高位清零 static __always_inline bool should_resched(void) { return unlikely(!raw_cpu_read_4(__preempt_count)); } ////////// 抢占计数相关 //////// #define PREEMPT_ENABLED (0 + PREEMPT_NEED_RESCHED) #define PREEMPT_DISABLE (1 + PREEMPT_ENABLED) // 读取__preempt_count,忽略need_resched标识位 static __always_inline int preempt_count(void) { return raw_cpu_read_4(__preempt_count) & ~PREEMPT_NEED_RESCHED; } static __always_inline void __preempt_count_add(int val) { raw_cpu_add_4(__preempt_count, val); } static __always_inline void __preempt_count_sub(int val) { raw_cpu_add_4(__preempt_count, -val); } // 抢占计数加1关闭抢占 #define preempt_disable() \ do { \ preempt_count_inc(); \ barrier(); \ } while (0) // 重新开启抢占,并测试是否需要重新调度 #define preempt_enable() \ do { \ barrier(); \ if (unlikely(preempt_count_dec_and_test())) \ __preempt_schedule(); \ } while (0) // 抢占并重新调度 // 这里设置PREEMPT_ACTIVE会对schdule()中的行为有影响 asmlinkage __visible void __sched notrace preempt_schedule(void) { // 如果抢占计数不为0或者没有开中断,则不调度 if (likely(!preemptible())) return; do { __preempt_count_add(PREEMPT_ACTIVE); __schedule(); __preempt_count_sub(PREEMPT_ACTIVE); barrier(); } while (need_resched()); } // 检查thread_info flags static __always_inline bool need_resched(void) { return unlikely(tif_need_resched()); } ////// 中断相关 //////// // 硬件中断计数 #define hardirq_count() (preempt_count() & HARDIRQ_MASK) // 软中断计数 #define softirq_count() (preempt_count() & SOFTIRQ_MASK) // 中断计数 #define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \ | NMI_MASK)) // 是否处于外部中断上下文 #define in_irq() (hardirq_count()) // 是否处于软中断上下文 #define in_softirq() (softirq_count()) // 是否处于中断上下文 #define in_interrupt() (irq_count()) #define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET) // 是否处于不可屏蔽中断环境 #define in_nmi() (preempt_count() & NMI_MASK) // 是否可抢占 : 抢占计数为0并且没有处在关闭抢占的环境中 # define preemptible() (preempt_count() == 0 && !irqs_disabled()) 3. 系统调用和中断处理流程的实现以及抢占的影响 (arch/x86/kernel/entry_64.S) 系统调用入口基本流程 保存当前rsp, 并指向内核栈,保存寄存器状态 用中断号调用系统调用函数表中对应的处理函数 返回时检查thread_info的flags,处理信号以及need_resched 如果没信号和need_resched,直接恢复寄存器返回用户空间 如果有信号处理信号,并再次检查 如果有need_resched,重新调度,返回再次检查 中断入口基本流程 保存寄存器状态 call do_IRQ 中断返回,恢复栈,检查是中断了内核上下文还是用户上下文 如果是用户上下文,检查thread_info flags是否需要处理信号和need_resched,如果需要,则处理信号和need_resched,再次检查; 否则,直接中断返回用户空间 如果是内核上下文,检查是否需要need_resched,如果需要,检查__preempt_count是否为0(能否抢占),如果为0,则call preempt_schedule_irq重新调度 // 系统调用的处理逻辑 ENTRY(system_call) /* ... 省略 ... */ // 保存当前栈顶指针到percpu变量 movq %rsp,PER_CPU_VAR(old_rsp) // 将内核栈底指针赋于rsp,即移到内核栈 movq PER_CPU_VAR(kernel_stack),%rsp /* ... 省略 ... */ system_call_fastpath: #if __SYSCALL_MASK == ~0 cmpq $__NR_syscall_max,%rax #else andl $__SYSCALL_MASK,%eax cmpl $__NR_syscall_max,%eax #endif ja ret_from_sys_call /* and return regs->ax */ movq %r10,%rcx // 系统调用 call *sys_call_table(,%rax,8) # XXX: rip relative movq %rax,RAX-ARGOFFSET(%rsp) ret_from_sys_call: movl $_TIF_ALLWORK_MASK,%edi /* edi: flagmask */ // 返回时需要检查thread_info的flags sysret_check: LOCKDEP_SYS_EXIT DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF movl TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET),%edx andl %edi,%edx jnz sysret_careful // 如果有thread_info flags需要处理,比如need_resched //// 直接返回 CFI_REMEMBER_STATE /* * sysretq will re-enable interrupts: */ TRACE_IRQS_ON movq RIP-ARGOFFSET(%rsp),%rcx CFI_REGISTER rip,rcx RESTORE_ARGS 1,-ARG_SKIP,0 /*CFI_REGISTER rflags,r11*/ // 恢复之前保存percpu变量中的栈顶地址(rsp) movq PER_CPU_VAR(old_rsp), %rsp // 返回用户空间 USERGS_SYSRET64 CFI_RESTORE_STATE //// 如果thread_info的标识被设置了,则需要处理后返回 /* Handle reschedules */ sysret_careful: bt $TIF_NEED_RESCHED,%edx // 检查是否需要重新调度 jnc sysret_signal // 有信号 // 没有信号则处理need_resched TRACE_IRQS_ON ENABLE_INTERRUPTS(CLBR_NONE) pushq_cfi %rdi SCHEDULE_USER // 调用schedule(),返回用户态不需要检查__preempt_count popq_cfi %rdi jmp sysret_check // 再一次检查 // 如果有信号发生,则需要处理信号 sysret_signal: TRACE_IRQS_ON ENABLE_INTERRUPTS(CLBR_NONE) FIXUP_TOP_OF_STACK %r11, -ARGOFFSET // 如果有信号,无条件跳转 jmp int_check_syscall_exit_work /* ... 省略 ... */ GLOBAL(int_ret_from_sys_call) DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF movl $_TIF_ALLWORK_MASK,%edi /* edi: mask to check */ GLOBAL(int_with_check) LOCKDEP_SYS_EXIT_IRQ GET_THREAD_INFO(%rcx) movl TI_flags(%rcx),%edx andl %edi,%edx jnz int_careful andl $~TS_COMPAT,TI_status(%rcx) jmp retint_swapgs /* Either reschedule or signal or syscall exit tracking needed. */ /* First do a reschedule test. */ /* edx: work, edi: workmask */ int_careful: bt $TIF_NEED_RESCHED,%edx jnc int_very_careful // 如果不只need_resched,跳转 TRACE_IRQS_ON ENABLE_INTERRUPTS(CLBR_NONE) pushq_cfi %rdi SCHEDULE_USER // 调度schedule popq_cfi %rdi DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF jmp int_with_check // 再次去检查 /* handle signals and tracing -- both require a full stack frame */ int_very_careful: TRACE_IRQS_ON ENABLE_INTERRUPTS(CLBR_NONE) int_check_syscall_exit_work: SAVE_REST /* Check for syscall exit trace */ testl $_TIF_WORK_SYSCALL_EXIT,%edx jz int_signal pushq_cfi %rdi leaq 8(%rsp),%rdi # &ptregs -> arg1 call syscall_trace_leave popq_cfi %rdi andl $~(_TIF_WORK_SYSCALL_EXIT|_TIF_SYSCALL_EMU),%edi jmp int_restore_rest int_signal: testl $_TIF_DO_NOTIFY_MASK,%edx jz 1f movq %rsp,%rdi # &ptregs -> arg1 xorl %esi,%esi # oldset -> arg2 call do_notify_resume 1: movl $_TIF_WORK_MASK,%edi int_restore_rest: RESTORE_REST DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF jmp int_with_check // 再次检查thread_info flags CFI_ENDPROC END(system_call) // 中断入口基本流程 // 调用do_IRQ的函数wrapper .macro interrupt func subq $ORIG_RAX-RBP, %rsp CFI_ADJUST_CFA_OFFSET ORIG_RAX-RBP SAVE_ARGS_IRQ // 进入中断处理上下文时保存寄存器 call \func /*... 省略 ...*/ common_interrupt: /*... 省略 ...*/ interrupt do_IRQ // 调用c函数do_IRQ实际处理中断 ret_from_intr: // 中断返回 DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF decl PER_CPU_VAR(irq_count) // 减少irq计数 /* Restore saved previous stack */ // 恢复之前的栈 popq %rsi CFI_DEF_CFA rsi,SS+8-RBP /* reg/off reset after def_cfa_expr */ leaq ARGOFFSET-RBP(%rsi), %rsp CFI_DEF_CFA_REGISTER rsp CFI_ADJUST_CFA_OFFSET RBP-ARGOFFSET exit_intr: GET_THREAD_INFO(%rcx) testl $3,CS-ARGOFFSET(%rsp) // 检查是否中断了内核 je retint_kernel // 从中断返回内核空间 /* Interrupt came from user space */ /* * Has a correct top of stack, but a partial stack frame * %rcx: thread info. Interrupts off. */ // 用户空间被中断,返回用户空间 retint_with_reschedule: movl $_TIF_WORK_MASK,%edi retint_check: LOCKDEP_SYS_EXIT_IRQ movl TI_flags(%rcx),%edx andl %edi,%edx CFI_REMEMBER_STATE jnz retint_careful // 需要处理need_resched retint_swapgs: /* return to user-space */ /* * The iretq could re-enable interrupts: */ DISABLE_INTERRUPTS(CLBR_ANY) TRACE_IRQS_IRETQ SWAPGS jmp restore_args retint_restore_args: /* return to kernel space */ DISABLE_INTERRUPTS(CLBR_ANY) /* * The iretq could re-enable interrupts: */ TRACE_IRQS_IRETQ restore_args: RESTORE_ARGS 1,8,1 irq_return: INTERRUPT_RETURN // native_irq进入 ENTRY(native_iret) /*... 省略 ...*/ /* edi: workmask, edx: work */ retint_careful: CFI_RESTORE_STATE bt $TIF_NEED_RESCHED,%edx jnc retint_signal // 需要处理信号 TRACE_IRQS_ON ENABLE_INTERRUPTS(CLBR_NONE) pushq_cfi %rdi SCHEDULE_USER // 返回用户空间之前调度schedule popq_cfi %rdi GET_THREAD_INFO(%rcx) DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF jmp retint_check // 再次检查thread_info flags retint_signal: testl $_TIF_DO_NOTIFY_MASK,%edx jz retint_swapgs TRACE_IRQS_ON ENABLE_INTERRUPTS(CLBR_NONE) SAVE_REST movq $-1,ORIG_RAX(%rsp) xorl %esi,%esi # oldset movq %rsp,%rdi # &pt_regs call do_notify_resume RESTORE_REST DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF GET_THREAD_INFO(%rcx) jmp retint_with_reschedule // 处理完信号,再次跳转处理need_resched //// 注意,如果内核配置支持抢占,则返回内核时使用这个retint_kernel #ifdef CONFIG_PREEMPT /* Returning to kernel space. Check if we need preemption */ /* rcx: threadinfo. interrupts off. */ ENTRY(retint_kernel) // 检查__preempt_count是否为0 cmpl $0,PER_CPU_VAR(__preempt_count) jnz retint_restore_args // 不为0,则禁止抢占 bt $9,EFLAGS-ARGOFFSET(%rsp) /* interrupts off? */ jnc retint_restore_args call preempt_schedule_irq // 可以抢占内核 jmp exit_intr // 再次检查 #endif CFI_ENDPROC END(common_interrupt) 4. 抢占与SMP并发安全 中断嵌套可能导致死锁和竞态,一般中断上下文会关闭本地中断 软中断 一个核上的task访问percpu变量时可能由于内核抢占导致重新调度到另一个核上继续访问另一个核上同名percpu变量,从而可能发生死锁和竞态,所以访问percpu或者共享变量时需要禁止抢占 自旋锁需要同时关闭本地中断和内核抢占 … 5. 几个问题作为回顾 什么时候可抢占? 什么时候需要抢占重新调度? 自旋锁为什么需要同时关闭中断和抢占? 为什么中断上下文不能睡眠?关闭抢占后能否睡眠? 为什么percpu变量的访问需要禁止抢占? …
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/51628366 1. 介绍 简单玩了下Linux kernel为容器技术提供的基础设施之一namespace(另一个是cgroups),包括uts/user/pid/mnt/ipc/net六个(3.13.0的内核). 这东西主要用来做资源的隔离,我感觉本质上是全局资源的映射,映射之间独立了自然隔离了。主要涉及到的东西是: clone setns unshare /proc/pid/ns, /proc/pid/uid_map, /proc/pid/gid_map等 后面简单分析了一下内核源码里面是怎么实现这几个namespace以及以几个简单系统调用为例,看看namespace怎么产生影响的,然后简单分析下setns和unshare的实现 2. 测试流程及代码 下面是一些简单的例子,主要测试uts/pid/user/mnt四个namespace的效果,测试代码主要用到三个进程,一个是clone系统调用执行/bin/bash后的进程,也是生成新的子namespace的初始进程,然后是打开/proc/pid/ns下的namespace链接文件,用setns将第二个可执行文件的进程加入/bin/bash的进程的namespace(容器),并让其fork出一个子进程,测试pid namespace的差异。值得注意的几个点: 不同版本的内核setns和unshare对namespace的支持不一样,较老的内核可能只支持ipc/net/uts三个namespace 某个进程创建后其pid namespace就固定了,使用setns和unshare改变后,其本身的pid namespace不会改变,只有fork出的子进程的pid namespace改变(改变的是每个进程的nsproxy->pid_namespace_for_children) 用setns添加mnt namespace应该放在其他namespace之后,否则可能出现无法打开/proc/pid/ns/…的错误 // 代码1: 开一些新的namespace(启动新容器) #define _GNU_SOURCE #include <sys/wait.h> #include <sched.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \ } while (0) /* Start function for cloned child */ static int childFunc(void *arg) { const char *binary = "/bin/bash"; char *const argv[] = { "/bin/bash", NULL }; char *const envp[] = { NULL }; /* wrappers for execve */ // has const char * as argument list // execl // execle => has envp // execlp => need search PATH // has char *const arr[] as argument list // execv // execvpe => need search PATH and has envp // execvp => need search PATH //int ret = execve(binary, argv, envp); int ret = execv(binary, argv); if (ret < 0) { errExit("execve error"); } return ret; } #define STACK_SIZE (1024 * 1024) /* Stack size for cloned child */ int main(int argc, char *argv[]) { char *stack; char *stackTop; pid_t pid; stack = malloc(STACK_SIZE); if (stack == NULL) errExit("malloc"); stackTop = stack + STACK_SIZE; /* Assume stack grows downward */ //pid = clone(childFunc, stackTop, CLONE_NEWUTS | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | SIGCHLD, NULL); pid = clone(childFunc, stackTop, CLONE_NEWUTS | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_NEWIPC | SIGCHLD, NULL); //pid = clone(childFunc, stackTop, CLONE_NEWUTS | //CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_NEWIPC //| CLONE_NEWNET | SIGCHLD, NULL); if (pid == -1) errExit("clone"); printf("clone() returned %ld\n", (long) pid); if (waitpid(pid, NULL, 0) == -1) errExit("waitpid"); printf("child has terminated\n"); exit(EXIT_SUCCESS); } // 代码2: 使用setns加入新进程 #define _GNU_SOURCE // ? #include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <sys/utsname.h> #include <unistd.h> #include <sys/types.h> #include <sched.h> #include <fcntl.h> #include <wait.h> // mainly setns and unshare system calls /* int setns(int fd, int nstype); */ // 不同版本内核/proc/pid/ns下namespace文件情况 /* CLONE_NEWCGROUP (since Linux 4.6) fd must refer to a cgroup namespace. CLONE_NEWIPC (since Linux 3.0) fd must refer to an IPC namespace. CLONE_NEWNET (since Linux 3.0) fd must refer to a network namespace. CLONE_NEWNS (since Linux 3.8) fd must refer to a mount namespace. CLONE_NEWPID (since Linux 3.8) fd must refer to a descendant PID namespace. CLONE_NEWUSER (since Linux 3.8) fd must refer to a user namespace. CLONE_NEWUTS (since Linux 3.0) fd must refer to a UTS namespace. */ /* // 特殊的pid namespace CLONE_NEWPID behaves somewhat differently from the other nstype values: reassociating the calling thread with a PID namespace changes only the PID namespace that child processes of the caller will be created in; it does not change the PID namespace of the caller itself. Reassociating with a PID namespace is allowed only if the PID namespace specified by fd is a descendant (child, grandchild, etc.) of the PID namespace of the caller. For further details on PID namespaces, see pid_namespaces(7). */ /* int unshare(int flags); CLONE_FILES | CLONE_FS | CLONE_NEWCGROUP | CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_NEWUTS | CLONE_SYSVSEM */ #define MAX_PROCPATH_LEN 1024 #define errorExit(msg) \ do { fprintf(stderr, "%s in file %s in line %d\n", msg, __FILE__, __LINE__);\ exit(EXIT_FAILURE); } while (0) void printInfo(); int openAndSetns(const char *path); int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stdout, "usage : execname pid(find namespaces of this process)\n"); return 0; } printInfo(); fprintf(stdout, "---- setns for uts ----\n"); char uts[MAX_PROCPATH_LEN]; snprintf(uts, MAX_PROCPATH_LEN, "/proc/%s/ns/uts", argv[1]); openAndSetns(uts); printInfo(); fprintf(stdout, "---- setns for user ----\n"); char user[MAX_PROCPATH_LEN]; snprintf(user, MAX_PROCPATH_LEN, "/proc/%s/ns/user", argv[1]); openAndSetns(user); printInfo(); // 注意pid namespace的不同行为,只有后续创建的子进程进入setns设置 // 的新的pid namespace,本进程不会改变 fprintf(stdout, "---- setns for pid ----\n"); char pidpath[MAX_PROCPATH_LEN]; snprintf(pidpath, MAX_PROCPATH_LEN, "/proc/%s/ns/pid", argv[1]); openAndSetns(pidpath); printInfo(); fprintf(stdout, "---- setns for ipc ----\n"); char ipc[MAX_PROCPATH_LEN]; snprintf(ipc, MAX_PROCPATH_LEN, "/proc/%s/ns/ipc", argv[1]); openAndSetns(ipc); printInfo(); fprintf(stdout, "---- setns for net ----\n"); char net[MAX_PROCPATH_LEN]; snprintf(net, MAX_PROCPATH_LEN, "/proc/%s/ns/net", argv[1]); openAndSetns(net); printInfo(); // 注意mnt namespace需要放在其他后面,避免mnt namespace改变后 // 找不到/proc/pid/ns下的文件 fprintf(stdout, "---- setns for mount ----\n"); char mount[MAX_PROCPATH_LEN]; snprintf(mount, MAX_PROCPATH_LEN, "/proc/%s/ns/mnt", argv[1]); openAndSetns(mount); printInfo(); // 测试子进程的pid namespace int ret = fork(); if (-1 == ret) { errorExit("failed to fork"); } else if (ret == 0) { fprintf(stdout, "********\n"); fprintf(stdout, "in child process\n"); printInfo(); fprintf(stdout, "********\n"); for (;;) { sleep(5); } } else { fprintf(stdout, "child pid : %d\n", ret); } for (;;) { sleep(5); } waitpid(ret, NULL, 0); return 0; } void printInfo() { pid_t pid; struct utsname uts; uid_t uid; gid_t gid; // pid namespace pid = getpid(); // user namespace uid = getuid(); gid = getgid(); // uts namespace uname(&uts); fprintf(stdout, "pid : %d\n", pid); fprintf(stdout, "uid : %d\n", uid); fprintf(stdout, "gid : %d\n", gid); fprintf(stdout, "hostname : %s\n", uts.nodename); } int openAndSetns(const char *path) { int ret = open(path, O_RDONLY, 0); if (-1 == ret) { fprintf(stderr, "%s\n", strerror(errno)); errorExit("failed to open fd"); } if (-1 == (ret = setns(ret, 0))) { fprintf(stderr, "%s\n", strerror(errno)); errorExit("failed to setns"); } return ret; } 3. 测试效果 user的效果 : 通过/proc/pid/uid_map和/proc/pid/gid_map设置container外用户id和容器内用户id的映射关系(把这放前面是因为后面hostname和mount需要权限…) uts的效果 : 改变container中的hostname不会影响container外面的hostname pid和mnt的效果 : container中进程id被重新映射,在container中重新挂载/proc filesystem不会影响容器外的/proc setns的测试 依次为init进程,container init进程(6个namespace的flag都指定了),新加入container的进程以及其fork出的子进程的namespace情况,可以看到container init进程与init进程的namespace完全不同了,新加入container的进程除了pid与init相同外,其他namespace与container init进程相同,而新加入container的进程fork出的子进程的namespace则与container init进程完全相同 新加入container init进程pid namespace的子进程 程序2输出 4. 内核里namespace的实现 (1) 主要数据结构 源码主要位置: // net_namespace为啥不链接个头文件到include/linux... include/net/net_namespace.h include/linux/mnt_namespace.h与fs/mount.h include/linux/ipc_namespace.h include/linux/pid_namespace.h include/linux/user_namespace.h // 这个命名估计是历史原因... include/linux/utsname.h 几个namespace结构 注意其他namespace都内嵌了user_namespace struct user_namespace { // uid_map struct uid_gid_map uid_map; // gid_map struct uid_gid_map gid_map; struct uid_gid_map projid_map; atomic_t count; // 父user_namespace struct user_namespace *parent; int level; kuid_t owner; kgid_t group; struct ns_common ns; unsigned long flags; /* Register of per-UID persistent keyrings for this namespace */ #ifdef CONFIG_PERSISTENT_KEYRINGS struct key *persistent_keyring_register; struct rw_semaphore persistent_keyring_register_sem; #endif }; // uts_namespace struct uts_namespace { struct kref kref; struct new_utsname name; struct user_namespace *user_ns; // 封装ns的一些通用操作钩子函数 struct ns_common ns; }; // pid_namespace struct pid_namespace { struct kref kref; // pid映射 struct pidmap pidmap[PIDMAP_ENTRIES]; struct rcu_head rcu; int last_pid; unsigned int nr_hashed; // pid_namespace里面,子进程挂掉会由此进程rape struct task_struct *child_reaper; struct kmem_cache *pid_cachep; unsigned int level; // 父pid_namespace struct pid_namespace *parent; // 当前namespace在proc fs中的位置 #ifdef CONFIG_PROC_FS struct vfsmount *proc_mnt; struct dentry *proc_self; struct dentry *proc_thread_self; #endif #ifdef CONFIG_BSD_PROCESS_ACCT struct bsd_acct_struct *bacct; #endif // pid_namespace依赖user_namespace struct user_namespace *user_ns; // 工作队列workqueue相关 struct work_struct proc_work; kgid_t pid_gid; int hide_pid; int reboot; /* group exit code if this pidns was rebooted */ // 封装ns的一些通用操作钩子函数 struct ns_common ns; }; // mount namespace struct mnt_namespace { atomic_t count; struct ns_common ns; // 新的mount namespace的根挂载点 struct mount * root; struct list_head list; // 内嵌的user_namespace struct user_namespace *user_ns; u64 seq; /* Sequence number to prevent loops */ wait_queue_head_t poll; u64 event; }; struct ipc_namespace { atomic_t count; struct ipc_ids ids[3]; int sem_ctls[4]; int used_sems; unsigned int msg_ctlmax; unsigned int msg_ctlmnb; unsigned int msg_ctlmni; atomic_t msg_bytes; atomic_t msg_hdrs; size_t shm_ctlmax; size_t shm_ctlall; unsigned long shm_tot; int shm_ctlmni; /* * Defines whether IPC_RMID is forced for _all_ shm segments regardless * of shmctl() */ int shm_rmid_forced; struct notifier_block ipcns_nb; /* The kern_mount of the mqueuefs sb. We take a ref on it */ struct vfsmount *mq_mnt; /* # queues in this ns, protected by mq_lock */ unsigned int mq_queues_count; /* next fields are set through sysctl */ unsigned int mq_queues_max; /* initialized to DFLT_QUEUESMAX */ unsigned int mq_msg_max; /* initialized to DFLT_MSGMAX */ unsigned int mq_msgsize_max; /* initialized to DFLT_MSGSIZEMAX */ unsigned int mq_msg_default; unsigned int mq_msgsize_default; /* user_ns which owns the ipc ns */ struct user_namespace *user_ns; struct ns_common ns; }; struct net { atomic_t passive; /* To decided when the network * namespace should be freed. */ atomic_t count; /* To decided when the network * namespace should be shut down. */ #ifdef NETNS_REFCNT_DEBUG atomic_t use_count; /* To track references we * destroy on demand */ #endif spinlock_t rules_mod_lock; // net_namespace链表 struct list_head list; /* list of network namespaces */ struct list_head cleanup_list; /* namespaces on death row */ struct list_head exit_list; /* Use only net_mutex */ // 内嵌的user_namespace struct user_namespace *user_ns; /* Owning user namespace */ struct ns_common ns; struct proc_dir_entry *proc_net; struct proc_dir_entry *proc_net_stat; /*... 省略 ...*/ (2) namespace如何产生影响(以uts和pid namespace为例) uts_namespace, 以uname系统调用为例 // syscall uname SYSCALL_DEFINE1(uname, struct old_utsname __user *, name) { int error = 0; if (!name) return -EFAULT; down_read(&uts_sem); // utsname() if (copy_to_user(name, utsname(), sizeof(*name))) error = -EFAULT; up_read(&uts_sem); if (!error && override_release(name->release, sizeof(name->release))) error = -EFAULT; if (!error && override_architecture(name)) error = -EFAULT; return error; } static inline struct new_utsname *utsname(void) { // 到当前进程uts namespace中查找utsname return &current->nsproxy->uts_ns->name; } pid namespace,以getpid系统调用为例 /** * sys_getpid - return the thread group id of the current process * * Note, despite the name, this returns the tgid not the pid. The tgid and * the pid are identical unless CLONE_THREAD was specified on clone() in * which case the tgid is the same in all threads of the same group. * * This is SMP safe as current->tgid does not change. */ SYSCALL_DEFINE0(getpid) { return task_tgid_vnr(current); } static inline pid_t task_tgid_vnr(struct task_struct *tsk) { return pid_vnr(task_tgid(tsk)); } pid_t pid_vnr(struct pid *pid) { return pid_nr_ns(pid, task_active_pid_ns(current)); } // 从pid namespace中获取真正的pid number nr pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns) { struct upid *upid; pid_t nr = 0; if (pid && ns->level <= pid->level) { upid = &pid->numbers[ns->level]; if (upid->ns == ns) nr = upid->nr; } return nr; } EXPORT_SYMBOL_GPL(pid_nr_ns); struct upid { /* Try to keep pid_chain in the same cacheline as nr for find_vpid */ // 真正的pid int nr; // pid_namespace struct pid_namespace *ns; struct hlist_node pid_chain; }; // 带有namespace和pid struct pid { atomic_t count; unsigned int level; /* lists of tasks that use this pid */ // 多个线程共享一个pid struct hlist_head tasks[PIDTYPE_MAX]; struct rcu_head rcu; struct upid numbers[1]; }; setns系统调用的实现 SYSCALL_DEFINE2(setns, int, fd, int, nstype) { struct task_struct *tsk = current; struct nsproxy *new_nsproxy; struct file *file; struct ns_common *ns; int err; file = proc_ns_fget(fd); if (IS_ERR(file)) return PTR_ERR(file); err = -EINVAL; ns = get_proc_ns(file_inode(file)); if (nstype && (ns->ops->type != nstype)) goto out; // 直接为当前进程创建新的nsproxy,然后copy当前进程的namespace到 // 新创建的nsproxy,最后视引用技术情况将原来的nsproxy放回 // kmem_cache,是否不太高效?不能直接在原来的nsproxy上 // install新的ns,没变的namespace不需要更改?不过貌似namespace // 不会经常变化,所以对性能要求也不需要很高? new_nsproxy = create_new_namespaces(0, tsk, current_user_ns(), tsk->fs); if (IS_ERR(new_nsproxy)) { err = PTR_ERR(new_nsproxy); goto out; } err = ns->ops->install(new_nsproxy, ns); if (err) { free_nsproxy(new_nsproxy); goto out; } // 切换当前进程的nsproxy,并可能释放nsproxy switch_task_namespaces(tsk, new_nsproxy); out: fput(file); return err; } static struct nsproxy *create_new_namespaces(unsigned long flags, struct task_struct *tsk, struct user_namespace *user_ns, struct fs_struct *new_fs) { struct nsproxy *new_nsp; int err; // 创建新的nsproxy new_nsp = create_nsproxy(); if (!new_nsp) return ERR_PTR(-ENOMEM); // 分配新的mnt_namespace new_nsp->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs); if (IS_ERR(new_nsp->mnt_ns)) { err = PTR_ERR(new_nsp->mnt_ns); goto out_ns; } // 分配新的uts namespace new_nsp->uts_ns = copy_utsname(flags, user_ns, tsk->nsproxy->uts_ns); if (IS_ERR(new_nsp->uts_ns)) { err = PTR_ERR(new_nsp->uts_ns); goto out_uts; } // 分配新的ipc namespace new_nsp->ipc_ns = copy_ipcs(flags, user_ns, tsk->nsproxy->ipc_ns); if (IS_ERR(new_nsp->ipc_ns)) { err = PTR_ERR(new_nsp->ipc_ns); goto out_ipc; } // 注意不同于其他namespace 这里改变的是此进程的子进程的pid namespace new_nsp->pid_ns_for_children = copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns_for_children); if (IS_ERR(new_nsp->pid_ns_for_children)) { err = PTR_ERR(new_nsp->pid_ns_for_children); goto out_pid; } // 分配新的net new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->net_ns); if (IS_ERR(new_nsp->net_ns)) { err = PTR_ERR(new_nsp->net_ns); goto out_net; } /*... 省略 ...*/ unshare系统调用的实现 // unshare主要也是使用create_new_nsproxy和switch_tasks_namespace SYSCALL_DEFINE1(unshare, unsigned long, unshare_flags) { struct fs_struct *fs, *new_fs = NULL; struct files_struct *fd, *new_fd = NULL; struct cred *new_cred = NULL; struct nsproxy *new_nsproxy = NULL; /*... 省略 ...*/ // 内部调用了create_new_nsproxy err = unshare_nsproxy_namespaces(unshare_flags, &new_nsproxy, new_cred, new_fs); /*... 省略 ...*/ if (new_nsproxy) // 切换当前进程的nsproxy到新的nsproxy, // 并可能释放nsproxy,nsproxy本身结构放回kmem_cache, // 而nsproxy中的uts/ipc/net/user/mnt以及嵌入其他 // namespace中的user namespace也会根据引用计数释放回slab switch_task_namespaces(current, new_nsproxy);
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/51582198 基本性质 后面两个贼有用 a≡b(modm)⟺a=k∗m+b a≡b(modm)∧c≡d(modm)⟹a+c≡b+d(modm)∧a∗c≡b∗d(modm) (a+b)modm=((amodm)+(bmodm))modm a∗bmodm=((amodm)∗(bmodm))modm 一些题目的分析与证明 大整数的求余与二进制字符串模3余数 证明(直接证明通用x进制的字符串对整数m求余) 假设字符串为A1A2A3...An则S=A1∗xn−1+A2∗xn−2+...+An为字符串代表的十进制值Smodm=(A1∗xn−1+A2∗xn−2+...+An)modm=((A1∗xn−1+A2∗xn−2)modm+(A3∗xn−3...+An)modm)modm=((A1∗x+A2)∗xn−2modm+(...)modm)modm=(((A1∗x+A2)modm)∗(xn−2modm)modm+(...)modm)modm令temp=(A1∗x+A2)modm=((temp∗(xn−2modm))modm+(...)modm)modm=(((tempmodm)∗(xn−2modm))modm+(...)modm)modm=((temp∗xn−2)modm+(...)modm)modm=(temp∗xn−2+A3∗nn−3+...+An)modm 由此我们可以看到递推公式temp=(temp∗x+Anext)modm 二进制字符串模3余数,同上 同余幂,求bnmodm,b和m都较大 将n表示成二进制串A1A2…An,则bn=b2A1∗(n−1)+...用mod的乘法性质和大整数除法类似的证明方法可以得出递推公式t=(t∗power)modmpower=b2Ai∗(n−i)modm即第i项对m的余数,可以通过幂次递增的递推求得 同余的其他一些应用 哈希 生成伪随机数(比如线性同余xn=(xn−1∗k+c)modm) 加密
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/51534406 这一篇或者说一个系列用来记录Redis相关的一些源码分析,不定时更新。 目前已添加的内容: Redis之eventloop Redis数据结构之dict Redis之eventloop 简介 Redis的eventloop实现也是比较平常的,主要关注文件描述符和timer相关事件,而且timer只是简单用一个单链表(O(n)遍历寻找最近触发的时间)实现。 流程 主要在initServer(server.c)中初始化整个eventloop相关的数据结构与回调 // 注册系统timer事件 if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) { serverPanic("Can't create event loop timers."); exit(1); } // 注册poll fd的接收客户端连接的读事件 for (j = 0; j < server.ipfd_count; j++) { if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler,NULL) == AE_ERR) { serverPanic( "Unrecoverable error creating server.ipfd file event."); } } // 同上 if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE, acceptUnixHandler,NULL) == AE_ERR) serverPanic("Unrecoverable error creating server.sofd file event."); acceptTcpHandler处理客户端请求,分配client结构,注册事件 cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport); acceptCommonHandler(cfd,0,cip); createClient,创建客户端 // receieved a client, alloc client structure // and register it into eventpoll client *createClient(int fd) { client *c = zmalloc(sizeof(client)); if (fd != -1) { anetNonBlock(NULL,fd); anetEnableTcpNoDelay(NULL,fd); if (server.tcpkeepalive) anetKeepAlive(NULL,fd,server.tcpkeepalive); // register read event for client connection // the callback handler is readQueryFromClient // read into client data buffer if (aeCreateFileEvent(server.el,fd,AE_READABLE, readQueryFromClient, c) == AE_ERR) { close(fd); zfree(c); return NULL; } } client读事件触发,读到buffer,解析client命令 dQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) --> processInputBuffer // handle query buffer // in processInputBuffer(c); if (c->reqtype == PROTO_REQ_INLINE) { if (processInlineBuffer(c) != C_OK) break; } else if (c->reqtype == PROTO_REQ_MULTIBULK) { if (processMultibulkBuffer(c) != C_OK) break; } else { serverPanic("Unknown request type"); } /* Multibulk processing could see a <= 0 length. */ if (c->argc == 0) { resetClient(c); } else { /* Only reset the client when the command was executed. */ // handle the client command if (processCommand(c) == C_OK) resetClient(c); /* freeMemoryIfNeeded may flush slave output buffers. This may result * into a slave, that may be the active client, to be freed. */ if (server.current_client == NULL) break; } 处理客户端命令 // in processCommand /* Exec the command */ if (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand && c->cmd->proc != discardCommand && c->cmd->proc != multiCommand && c->cmd->proc != watchCommand) { queueMultiCommand(c); addReply(c,shared.queued); } else { // call the cmd // 进入具体数据结构的命令处理 call(c,CMD_CALL_FULL); c->woff = server.master_repl_offset; if (listLength(server.ready_keys)) handleClientsBlockedOnLists(); } 其他注意点 关于timer的实现没有采用优先级队列(O(logn))等其他数据结构,而是直接采用O(n)遍历的单链表,是因为一般来说timer会较少? Redis数据结构之dict 主要特点 Redis的hashtable实现叫dict,其实现和平常没有太大的区别,唯一比较特殊的地方是每个dict结构内部有两个实际的hashtable结构dictht,是为了实现增量哈希,故名思义,即当第一个dictht到一定负载因子后会触发rehash,分配新的dictht结构的动作和真正的rehash的动作是分离的,并且rehash被均摊到各个具体的操作中去了,这样就不会长时间阻塞线程,因为Redis是单线程。另外,增量hash可以按多步或者持续一定时间做。 主要数据结构 dictEntry => hashtable的bucket dictType => 规定操作hashtable的接口 dictht => hashtable dict => 对外呈现的”hashtable” dictIterator => 迭代器,方便遍历 // dict.h // hash table entry typedef struct dictEntry { void *key; // key union { void *val; uint64_t u64; int64_t s64; double d; } v; // value struct dictEntry *next; // linked list } dictEntry; // operations(APIS) of some type of hashtable typedef struct dictType { // hash function unsigned int (*hashFunction)(const void *key); // copy key void *(*keyDup)(void *privdata, const void *key); // copy value void *(*valDup)(void *privdata, const void *obj); // key comparison int (*keyCompare)(void *privdata, const void *key1, const void *key2); // dtor for key void (*keyDestructor)(void *privdata, void *key); // dtor for value void (*valDestructor)(void *privdata, void *obj); } dictType; /* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table. */ // a hashtable typedef struct dictht { dictEntry **table; // entries unsigned long size; // max size unsigned long sizemask; // mask unsigned long used; // current used } dictht; typedef struct dict { dictType *type; // type operations void *privdata; // for extension dictht ht[2]; // two hashtables // rehashing flag long rehashidx; /* rehashing not in progress if rehashidx == -1 */ // users number unsigned long iterators; /* number of iterators currently running */ } dict; /* If safe is set to 1 this is a safe iterator, that means, you can call * dictAdd, dictFind, and other functions against the dictionary even while * iterating. Otherwise it is a non safe iterator, and only dictNext() * should be called while iterating. */ typedef struct dictIterator { dict *d; long index; int table, safe; dictEntry *entry, *nextEntry; /* unsafe iterator fingerprint for misuse detection. */ long long fingerprint; } dictIterator; 主要接口 // dict.h // create dict *dictCreate(dictType *type, void *privDataPtr); // expand or initilize the just created dict, alloc second hashtable of dict for incremental rehashing int dictExpand(dict *d, unsigned long size); // add, if in rehashing, do 1 step of incremental rehashing int dictAdd(dict *d, void *key, void *val); dictEntry *dictAddRaw(dict *d, void *key); // update, if in rehashing, do 1 step of incremental rehashing // can we first find and return the entry no matter it is update or add, so // we can speed up the update process because no need to do twice find process? int dictReplace(dict *d, void *key, void *val); dictEntry *dictReplaceRaw(dict *d, void *key); // delete if in rehashing, do 1 step of incremental rehashing int dictDelete(dict *d, const void *key); // free the memory int dictDeleteNoFree(dict *d, const void *key); // not free the memory // can we use a double linked list to free the hash table, so speed up? void dictRelease(dict *d); // find an entry dictEntry * dictFind(dict *d, const void *key); void *dictFetchValue(dict *d, const void *key); // resize to eh pow of 2 number just >= the used number of slots int dictResize(dict *d); // alloc a new iterator dictIterator *dictGetIterator(dict *d); // alloc a safe iterator dictIterator *dictGetSafeIterator(dict *d); // next entry dictEntry *dictNext(dictIterator *iter); void dictReleaseIterator(dictIterator *iter); // random sampling dictEntry *dictGetRandomKey(dict *d); unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count); // get stats info void dictGetStats(char *buf, size_t bufsize, dict *d); // murmurhash unsigned int dictGenHashFunction(const void *key, int len); unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len); // empty a dict void dictEmpty(dict *d, void(callback)(void*)); void dictEnableResize(void); void dictDisableResize(void); // do n steps rehashing int dictRehash(dict *d, int n); // do rehashing for a ms milliseconds int dictRehashMilliseconds(dict *d, int ms); // hash function seed void dictSetHashFunctionSeed(unsigned int initval); unsigned int dictGetHashFunctionSeed(void); // scan a dict unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, void *privdata); 一些可能优化的地方 在dictReplace中能否统一add和update的查找,无论是add还是update都返回一个entry,用标识表明是add还是update,而不用在update时做两次查找,从而提升update的性能 在release整个dict时,是循环遍历所有头bucket,最坏情况接近O(n),能否用双向的空闲链表优化(当然这样会浪费指针所占空间)
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/51427031 时钟 硬件时钟 RTC(real time clock),记录wall clock time,硬件对应到/dev/rtc设备文件,读取设备文件可得到硬件时间 读取方式 通过ioctl #include <linux/rtc.h> int ioctl(fd, RTC_request, param); hwclock命令 通常内核在boot以及从低电量中恢复时,会读取RTC更新system time 软件时钟 HZ and jiffies, 由内核维护,对于PC通常HZ配置为 1s / 10ms = 100 精度影响select等依赖timeout的系统调用 HRT(high-resolution timers). Linux 2.6.21开始,内核支持高精度定时器,不受内核jiffy限制,可以达到硬件时钟的精度。 外部时钟 从网络ntp,原子钟等同步 时间 时间类别 wall clock time => 硬件时间 real time => 从某个时间点(比如Epoch)开始的系统时间 sys and user time => 通常指程序在内核态和用户态花的时间 时间的表示 time_t 从Epoch开始的秒数 calendar time 字符串 拆分时间 struct tm struct tm { int tm_sec; /* seconds */ int tm_min; /* minutes */ int tm_hour; /* hours */ int tm_mday; /* day of the month */ int tm_mon; /* month */ int tm_year; /* year */ int tm_wday; /* day of the week */ int tm_yday; /* day in the year */ int tm_isdst; /* daylight saving time */ }; struct timeval/struct timespec struct timeval { time_t seconds; suseconds_t useconds; } struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; 系统时间的操作 #include <time.h> #include <sys/time.h> // number of seconds since epoch time_t time(time_t *t) //参数time_t* char *ctime(const time_t *timep); char *ctime_r(const time_t *timep, char *buf); struct tm *gmtime(const time_t *timep); struct tm *gmtime_r(const time_t *timep, struct tm *result); struct tm *localtime(const time_t *timep); struct tm *localtime_r(const time_t *timep, struct tm *result); //参数struct tm* char *asctime(const struct tm *tm); char *asctime_r(const struct tm *tm, char *buf); time_t mktime(struct tm *tm); int gettimeofday(struct timeval *tv, struct timezone *tz);//如果系统时间调整了会影响 int clock_gettime(clockid_t clk_id, struct timespec *tp); //将tm按照format处理后放到s size_t strftime(char *s, size_t max, const char *format, const struct tm *tm); //将字符串时间s按照format格式化后放入tm char *strptime(const char *s, const char *format, struct tm *tm); 定时器 sleep unsigned int sleep(unsigned int seconds); usleep int usleep(useconds_t usec); nanosleep int nanosleep(const struct timespec *req, struct timespec *rem); alarm // SIGALARM after seconds unsigned int alarm(unsigned int seconds); timer_create int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid); setitimer timerfd_create + select/poll/epoll int timerfd_create(int clockid, int flags); select // struct timeval可以精确到微秒(如果硬件有高精度时钟支持) int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); // struct timespec可以精确到纳秒,但是pselect下次无法修改timeout int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask); // 一般能提供周期,延时,时间点触发,但核心还是时间点触发的timer // 1.call_period => 触发一次重新注册call_at // 2.call_later => 转换为call_at // 3.call_at => 时间点触发的timer可以用一个优先级队列保存 poll // timeout最小单位ms,并且rounded up to系统时钟的精度 int poll(struct pollfd *fds, nfds_t nfds, int timeout); // 注意timespec会被转换成ms int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout_ts, const sigset_t *sigmask); epoll // timeout最小单位ms,并且rounded up to系统时钟的精度 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask); eventfd + select/poll/epoll 一个fd可同时负责读接受事件通知和写触发事件通知 signaled + select/poll/epoll 借助alarm/setitimer/timer_create等触发的SIGALARM,通过signalfd传递到多路复用中 pipe + select/poll/epoll 一端另起线程定时触发,另一端放到多路复用中 分布式系统的时间 扯点其他的东西:-)。时间是个复杂而有意思的东西,在单机上不同处理器不同进程不同线程可以读到同一个系统时钟CLOCK_REALTIME,而且在一定时间范围内t1~t2发生的事件,即使在t1之前,t2之后系统时间与真实时间发生了一定偏移,只要时间戳的相对顺序没乱,那么我们就可以完全确定t1~t2时间戳之间不同线程发生事件的顺序。但是不同机器之间的系统时间总会互相漂移(ntp局域网0.1ms左右,互联网1-50ms左右),导致我们没法直接使用系统时间(google的原子钟也是将一个时间段作为时间点的,只要这个时间段比较小,那么性能应该可以接收),所以需要logic clock以及衍生出来的vector clock或者version number等。 没有全局时钟是分布式系统需要一致性算法的一个重要原因,因为我们没办法根据单机的系统时间戳来判断多台机器之间事件的先后顺序,那么对于一个新的节点,我们要把之前所有的时间atomic broadcast到这个新节点就会出现问题,所以这也是分布式一致性算法(Paxos/Raft/Viewstamp Replication/Zab)解决的一个问题,当然再加上网络的异步,以及无法获知各个节点的全局状态,以及机器crash等各种问题,这些算法往往加入了容错性。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/51306894 (此文主要用来记录一些调试,性能测试与分析等工具的用法,备忘) Linux下的追踪和性能统计 Linux内核提供的基础设施 tarcepoints => 静态探测点 kprobe => 内核态动态探测点(kernel/kprobe.c, example:sample/kprobe) uprobe => 用户态动态探测点(kernel/events/uprobe.c) 其最基本的用法我们可以写内核模块注入某个探测点的探针,做一些追踪与统计分析, 但通常会有更方便的框架以及其前端工具,比如下面将提到的ftrace与trace-cmd, perf_events与perf, systemtap, 还有基于这些前端工具的工具perf-tools… ftrace framework 1. 介绍 ftrace框架主要以debugfs中/sys/kernel/debug/trace文件系统的形式提供了静态和动态 追踪的接口,ftrace框架有命令行和图像化的前端工具trace-cmd 和 kernelshark。而且 提供了不同种类的tracer, 可以使用下面命令查看: cat /sys/kernel/debug/trace/available_tracers ftrace的核心代码位于kernel/trace目录下,ftrace.c注册了debugfs下的trace目录, trace_kprobe.c和trace_uprobe.c提供了kprobe和uprobe的接口。除了kprobe和uprobe, ftrace还提供了events支持,主要位于/kernel/sys/debug/trace/events, 主要包括 硬件事件,内核软件事件,以及静态tracepoints的事件。可以通过下面命令查看支持的事件: cat /sys/kernel/debug/trace/available_events 2. 例子 (例子来源于内核源码Documentation/trace) 基于ftrace使用kprobe动态trace: // 添加探针 echo 'p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)' > /sys/kernel/debug/tracing/kprobe_events echo 'r:myretprobe do_sys_open $retval' >> /sys/kernel/debug/tracing/kprobe_events // 激活 echo 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable echo 1 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable // 查看输出 cat /sys/kernel/debug/tracing/trace // 关闭 echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable echo 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable // 移除探针 echo -:myprobe >> kprobe_events echo > /sys/kernel/debug/tracing/kprobe_events 基于ftrace使用uprobe动态trace(kernel/trace/trace_uprobe.c) // 添加探针 echo 'p: /bin/bash:0x4245c0' > /sys/kernel/debug/tracing/uprobe_events echo 'r: /bin/bash:0x4245c0' > /sys/kernel/debug/tracing/uprobe_events // 激活 echo 1 > events/uprobes/enable // 查看输出 cat /sys/kernel/debug/tracing/trace // 关闭 echo 0 > events/uprobes/enable // 移除 echo '-:bash_0x4245c0' >> /sys/kernel/debug/tracing/uprobe_events echo > /sys/kernel/debug/tracing/uprobe_events 基于ftrace使用tracepoints静态events(kernel/trace/trace_events.c) 通常我们可以写内核模块给某个静态tracepoint添加探针 基于ftrace events // 添加event echo sched_wakeup >> /sys/kernel/debug/tracing/set_event echo *:* > /sys/kernel/debug/tracing/set_event echo 'irq:*' > /sys/kernel/debug/tracing/set_event // 激活event echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable // 查看输出 cat /sys/kernel/debug/tracing/trace // 移除event echo '!sched_wakeup' >> /sys/kernel/debug/tracing/set_event echo > /sys/kernel/debug/tracing/set_event perf_events 1. 介绍 perf_events和对应的前端工具perf提供了硬件和软件层面的计数等性能分析。其源码位于 内核源码树tools/perf目录下。 2. 例子 systemtap 1. 介绍 2. 例子 perf-tools and flamegraph 1. 介绍 2. 例子 GDB常用调试命令和调试技巧 命令 status info => 查看程序本身相关信息 args => 打印参数 breakpoints => 断点信息 files => 进程的地址空间详细内容 sharedlibrary => 加载的共享库 frame => 栈帧 line => 当前所在行 locals => 当前栈帧中的变量 registers => 寄存器信息 stack => 栈信息 source => 当前源码文件信息 auxv => 进程属性 address/symbol => symbol的地址/地址的symbol threads => 线程信息 tracepoints => tracepoint信息 vtbl => 某个类指针的虚函数表 watchpoints => 显示watchpoints信息 … show => 查看系统配置环境等信息 environment => 环境变量 endian => 大小端 print => 打印格式的相关配置 … breakpoints awatch/watch => 为某个表达式设置watchpoint break => 设置断点 clear => 清除断点 catch => 当发生下列某个时间时stop assert catch fork exec signal syscall throw vfork delete => 删除 breakpoints checkpoint tracepoints data disassemble => 反汇编某段代码 dump binary memory => 二进制形式dump内存 value => 二进制形式dump值 set => 修改gdb配置 stack backtrace/bt => 所有栈帧 down/up => 下一帧/上一帧 frame => 打印某一帧 技巧 设置watch points调试内存非法写等错误 … valgrind常用命令和技巧 命令 –tool memcheck => cachegrind => callgrind => helgrind => –trace-children => 多进程 –leak-check=no|summay|yes|full => 打印内存泄露信息 技巧 生成调用图 先用valgrind生成call.grind.out.xxx文件 生成dot文件: gprof2dot -f callgrind -n10 -s callgrind.out.xxx > out.dot 生成png: dot -Tpng out.dot -o out.png
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/50989571 本文主要记录Linux协议栈相关的主要系统调用的一些主要的函数调用栈,备忘。目前添加socket/connect 1.sys_socket bsd socket layer => sys_create net/socket.c => socket_create net/socket.c => __socket_create net/socket.c => sock_alloc net/socket.c inet sock layer => net_families[family]->create net/socket.c => inet_create net/ipv4/af_inet.c => list_for_each_entry_rcu(answer, &inetsw[sock->type], list) => sock->ops = answer->ops //绑定每个协议族的proto_ops到socket结构上,proto_ops通常是与下层传输层的接口比如inet_connect,inet_* => sk_alloc(net, PF_INET, GFP_KERNEL, answer->prot) net/core/sock.c => sk_prot_alloc net/core/sock.c => kmem_cache_alloc(prot->slab) net/core/sock.c => sk->sk_prot = prot; sk->family = family net/core/sock.c => sock_init_data net/core/sock.c => skb_queue_head_init(&sk->sk_receive_queue) net/core/sock.c skb_queue_head_init(&sk->sk_write_queue) skb_queue_head_init(&sk->sk_error_queue) => sk->sk_state = TCP_CLOSE sk_set_socket(sk, sock) => sk->sk_prot->init(sk) transport layer => tcp_v4_init_sock net/ipv4/tcp_ipv4.c => tcp_init_sock net/ipv4/tcp.c => icsk->icsk_af_ops = &ipv4_specific net/ipv4/tcp_ipv4.c //这里挂接传输层的读写处理,每个sock结构有一个inet_connection_sock的队列负责接收对端的socket 2.connect bsd socket layer => sys_connect net/socket.c => sock->ops->connect(sock->proto_ops->connect) net/socket.c inet sock layer => inet_stream_ops->connect net/ipv4/af_inet.c => inet_stream_connect net/ipv4/af_inet.c => __inet_stream_connect net/ipv4/af_inet.c => sock->sk->sk_prot->connect net/ipv4/af_inet.c transport layer => tcp_prot->connect(tcp_v4_connect) net/ipv4/tcp_ipv4.c => ip_route_connect net/ipv4/tcp_ipv4.c => ip_route_connect_init include/net/route.h => flowi4_init_output include/net/flow.h => tcp_set_state(sk, TCP_SYN_SENT) net/ipv4/tcp_ipv4.c => tcp_connect net/ipv4/tcp_output.c => tcp_connect_init net/ipv4/tcp_output.c => sk_stream_alloc_skb net/ipv4/tcp.c => __alloc_skb net/core/skbuff.c => kmem_cache_alloc_node => tcp_connect_queue_skb net/ipv4/tcp_output.c => __tcp_add_write_queue_tail include/net/tcp.h => tcp_transmit_skb net/ipv4/tcp_output.c => icsk->icsk_af_ops->queue_xmit ip layer => ip_queue_xmit net/ipv4/ip_output.c => ip_route_output_ports net/ipv4/ip_output.c 路由 => ip_local_out include/net/ip.h => ip_local_out_sk net/ipv4/ip_output.c => __ip_local_out net/ipv4/ip_output.c netfilter检查 => nf_hook (netfilter hooks) include/linux/netfilter.h => nf_hook_thresh include/linux/netfilter.h => nf_hook_slow include/linux/netfilter.h => nf_iterate net/netfilter/core.c => dst_output_sk include/net/dst.h => dst_entry->output include/net/dst.h => ip_output? net/ipv4/ip_output.c => ip_finish_output net/ipv4/ip_output.c => ip_fragment net/ipv4/ip_output.c => ip_finish_output2 net/ipv4/ip_output.c hardware related layer => dst_neigh_output include/net/dst.h => neigh_hh_output include/net/neighbour.h => dev_queue_xmit net/core/dev.c => __dev_queue_xmit net/core/dev.c => netdev_pick_tx net/core/flow_dissector.c => __dev_xmit_skb net/core/dev.c => q->enqueue => __qdisc_run(q) net/sched/sch_generic.c => qdisc_restart net/sched/sch_generic.c => dequeue_skb net/sched/sch_generic.c => skb_get_tx_queue net/sched/sch_generic.c => netdev_get_tx_queue net/sched/sch_generic.c => sch_direct_xmit net/sched/sch_generic.c => validate_xmit_skb_list net/sched/sch_generic.c => dev_hard_start_xmit net/core/dev.c => xmit_one net/core/dev.c => netdev_start_xmit include/linux/netdevice.h => net_device_ops->ndo_start_xmit include/linux/netdevice.h => driver layer? ref: 1. linux-3.19.3 src 2. Linux IP Networking A Guide to the Implementation and Modification of the Linux Protocol Stack
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/50850299 在上一篇文章中讨论了leader选举对于基本Paxos算法在实际工程应用中的必要性,这一篇文章首先结合raft的选举算法谈谈leader选举的实质和常用方法,然后结合raft算法选举后的日志恢复以及《Paxos Made Simple》里lamport勾勒的multi-paxos的日志恢复来详细分析一下选主后要做的两件重要事情以及俩算法在这块的差异。 1.raft的选主算法以及选主算法的实质 前面一篇文章中提到,选主本质上就是分布式共识问题,可以用基本Paxos解决,下面就raft选主算法与基本Paxos的对应关系来说明。 关于raft选主的详细描述可以参考原论文 raft选主时的term实际上对应基本Paxos中的proposal id raft选主时的要求即每个term期间只能最多有一个leader实际上对应于基本Paxos的每个proposal要么达成决议要么没达成决议 raft选主时的随机timeout实际上是为了防止基本Paxos livelock的问题,这也是FLP定理所决定的 raft选举时与基本Paxos的区别在于,raft选举不要求在某个term(proposal id)选出一个leader(达成决议后)不需要后续的某个term(proposal id)选出同一台机器作为leader(使用同一个值达成决议),而是可以每次重新选一个机器(proposal选不同值),当然我们可以使用一定方法,增大选某台机器的概率,比如为每台机器设置rank值。 raft选举时,当candidate和leader接受到更大的term时立即更新term转为follower,在下一次超时前自然不能再提proposal,实际上对应于基本Paxos第一阶段acceptor接收到proposal id更大的proposal时更新proposal id放弃当前的proposal(在选主中实际上就对应放弃我candidate和leader的身份,本质上就是proposer的身份) 所以选主本质上是可以通过基本Paxos算法来保证的,选主没有完全使用Paxos算法,可以看作使用了Paxos算法的某个子算法解决了比容错分布式一致问题限制稍微小的问题。当然,我们可以在选主时加上额外的限制条件,只要能保证可能选出一个主。 2.选主后日志的同步 选出新的leader后,它至少要负责做两件事情,一件是确定下一次客户请求应该用哪个日志槽位或者说项,另一件是确定整个集群的机器过去已经提交过的最近的项(或者说日志),确定这两个值的过程实际上就是日志恢复的过程,下面对两种算法具体分析。这里补充一点之前文章漏掉的东西,基本Paxos算法实际上有三个阶段,最后一个阶段是提交阶段,只是通常leader-based算法为了优化网络开销,将第三阶段和第二阶段合并了,而在每次执行第二阶段是带上leader已经提交过的日志号,所以新leader还需要确定最近被提交过的日志,而这种优化也引入了另外的复杂性。 对于raft来说 由于选主时额外的限制条件以及log replication时的consistency check保证(关于这两者是什么东西,不细说,基本上这就是raft简化了multi-paxos最核心的东西吧),所以每个新leader一定有最新的日志,所以对于下一条日志槽位的选取,只需要读取最后一条日志来判断就行了。关于raft的log replication,后面有机会再说。 而对于已提交日志的判断,由于存在可能已经形成多数派,也就是在内存中形成了多数派,但是还没有机器commited到磁盘,这时,新的leader无法判断这条日志是已经提交还是没有提交(参见原论文5.4.2节),raft的做法是不管这条可能被新leader覆盖掉的日志,只需要保证在新的term期间,提交一条日志,那么由于consistency check,自然会提交之前的日志。 对于multi-paxos来说 由于在log replication说,不像raft那样保证一个顺序应答(不能保证线性一致性,能保证顺序一致性),也就是保证一个日志槽位达成多数派后才接受下一个请求,multi-paxos可以在一个日志槽位还没有达成多数派时并发处理另外一个日志槽位,所以新leader在恢复确认下一个可用日志槽位以及已提交日志时更麻烦。 lamport原论文描述的方法是,对于明确知道已提交的日志(这一点我们可以通过给每一条已提交日志加一个标示,这样可以减少日志恢复的时间),不用再次进行基本Paxos的决议,而对于未明确知道已提交的日志,则进行基本Paxos的二个阶段来确认已达成多数派的值,对于中间空洞且之前没有达成过多数派的,直接写一条空操作的日志,至于为什么会产生这种情况,可以参考原论文。一旦所有日志都经过这种方法恢复后,下一个可用日志槽位和最近已提交日志号也就能确定了。 对比上面两者恢复的过程,我们可以看到raft是怎么简化multi-paxos的。一旦新的leader确定了上面那两件事情,就可以进入正常的log replication阶段了,也就仅仅是多数派的事情了。 3.log replication,客户端交互,membership管理,leader lease等 这一节为后面的文章做个铺垫,对于log replication实际上不会涉及太多状态的reason,所以也就比较容易理解,基本上是类似简化的两阶段提交,后面会介绍下raft的log replication。对于客户端交互,leader什么时候返回结果,客户端怎么超时重试,以及怎么保证请求的幂等,membership management,以及leader lease等一些优化手段。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/50843779 上一篇文章推导了基本Paxos算法,并引出了在实际使用中其存在的问题,然后说明了leader-based分布式一致算法的优势。这篇文章分析一下选主的本质,选出一个主对整个算法的影响,以及采用选主会存在的问题以及基本Paxos协议是怎么样保证这些问题不会影响一致性的。 1.为什么选主 至于为什么选主?个人认为有如下原因: 避免并发决议导致的livelock和新丢失的问题 可以采用一定方法在选主时(raft),选主中或者选主后保证leader上有最新的达成多数派(达成多数派应该用多数派已经将值写入持久化日志来判定),这样可以优化针对同一个项的读请求,不然每次客户端读请求也需要走一遍基本Paxos 选出leader可以保证在一个leader的统治期间内只有这一个leader可以接收客户端请求,发起决议(至于脑裂的问题,后面会分析), 2.不同的选主算法,其本质是什么? 前面说了在一个leader统治期间内,不可能存在多个leader同时对一个项达成多数派(如果一个leader也没有自然满足,包括脑裂后面会分析到也是满足的),但是对于选主本身来说,实际上其本质上就是一个分布式一致性问题,并且可能有多个proposer并发提出选主决议,所以可以使用基本Paxos来解决,这就回到了基本的Paxos算法了!所以我们需要为每次选主决议编号,比如raft算法的term,这个实际上就对应基本Paxos算法的proposal id。 3.选主后对整个算法造成什么影响? 前面提到了”选出leader可以保证在一个leader的统治期间内只有这一个leader可以接收客户端请求,发起决议”。这样实际上基本Paxos的第一阶段prepare就没有必要了,因为对于下一个项来说,在这个leader统治期内,在达成多数派之前,不可能有其他人提出决议并达成多数派,所以可以直接使用客户端的值进入第二阶段accept。 4.选主可能会导致的问题? 最大的问题应该是脑裂了,也就是说可能存在多个分区多个leader接收客户端响应,但是由于多数派的限制,只能最多有一个分区能达成多数派。我们假设最简单的情况,A/B/C/D/E五台机器,两个分区P1有三台A/B/C和P2有两台D/E,那么可能的情况是: (1).P1有leader;P2没有leader (2).P1有leader;P2也有leader 显然由于多数派的限制,只有P1可能达成决议 5.新的leader选出来后的操作 一般来说,新的leader选出来后,我们需要对leader进行日志恢复,以便leader决定下一次客户端请求的时候该用哪个日志槽位或者说哪个项吧,这里也是不同的算法差异较大的地方,比如raft,viewstamped replication,zab以及lamport 《Paxos Made Simple》里面第三节描述的方法。在lamport论文的描述中,还是采用基本的Paxos,对未明确知道达成多数派的项重新走一遍基本Paxos算法,具体可以参照原论文,细节还是挺多。对于raft来说,由于其保证日志是连续的,且保证在选主的时候只选择具有最新的日志的机器,所以选主之后,新的leader上的日志本身就是最新的。 下一篇会着重分析在新的leader选举后,新leader怎么恢复日志记录以及怎么确定已提交的日志,这一点还是通过对比lamport在《Paxos Made Simple》第三节提到的方法以及raft中的实现来说明。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/50829689 Paxos算法无疑是分布式系统理论中的经典,由于很多论文、博客都没有详细分析算法的背景以及实际中应用会产生的非常多的细节问题,所以导致很难理解,或者说很难完整理解,实现和测试则是更繁杂,不过这也是这个问题有意思的原因吧:-)。读过一些论文,思考过一些问题,希望能把这个问题的背景,以及各种容错分布式一致性算法设计的逻辑和背景记录下来,当然,内容实在太多,最早得追溯到迪杰斯特拉对并发与分布式问题的讨论吧,所以这个系列准备以Paxos算法为核心,介绍其一系列的衍生算法及其相关的问题。当然,很多东西是通过一些论文结合自己的理解去推测作者当时怎么去思考设计优化这些算法的,至于形式化证明的部分就弱化了。整个系列的最大目的是希望能理清分布式共识问题的背景,主要容错分布式一致性协议的设计逻辑以及它们的联系与区别,后面有机会分析下实际工程中的实现比如ZooKeeper/Etcd/Consul等,最后如果有机会自己简单实现一下:-) Contents: 并发,一致性,时序 分布式系统的基本概念与特点,包括系统模型/分布式共识/分布式一致性 容错分布式一致性协议与Paxos算法 leader-based容错分布式一致性协议 选主与同步 日志复制,membership management,客户端交互,leader租约等等 其他leader-based协议Viewstamped Replication/Zab的介绍以及Paxos与各种leader-based容错一致性协议的对比 开源实现与分析 这一篇文章主要简单介绍一下分布式共识和一致性协议的背景和Paxos算法,以及我所理解的它的设计逻辑,并按照这个逻辑尝试非形式化地重新设计Paxos算法,并与Lamport原始论文中的描述相对应。这篇文章主要指basic Paxos,而不是其他变形比如Multi-Paxos及其他的leader-based的分布式一致算法,比如Raft/Viewstamped Replication/Zab,后续文章会着重分析leader-based的分布式一致算法。 1. 分布式系统基本概念回顾 分布式系统的基本特点 部分故障 容错 没有全局时钟 事件定序 : 原子时钟,Lamport Clock,Vector Clock等 副本一致性问题 : 通常为了保证容错,需要使用多个副本,副本之间的复制需要保证强一致 通信延时影响性能和扩展性 保证系统正确性下较少消息传递,减少共享状态,使用缓存等等 系统模型 同步和异步 同步 异步(执行时间和消息传递时间没有上限) 网络模型 可靠 消息丢失,重复传递,消息乱序 故障模型 crash-failure fault byzantine fault 一致性 data-central 严格一致性(strict consistency) 线性一致性(linear consistency) 顺序一致性(sequential consistency) 因果一致性(casual consistency) 弱一致性(weak consistency) 最终一致性(eventual consistency) client-central 单调读一致性(Monotonic Reads Consistency) 单调写一致性(Monotonic Writes Consistency) 读写一致性(Read Your Writes Consistency) 写读一致性(Write Follows Read Consistency) 其他 2.分布式共识问题及容错分布式一致性协议 导致对Paxos理解困难的一个原因是对分布式共识问题本身没有较好的理解。先举个简单例子,然后再说明其需要满足的safety和liveness条件。 例子:多个人在食堂决定吃什么菜,不能事先商量好,每个人都可以同时提出一样菜,中间可能有些人临时去上厕所了,待会重新回来,要保证的是最终只有一种菜被接受,而且重新回来的人在需要的时候能够知道这次吃饭到底吃的是什么菜。这里需要注意的是:“同时”说明并发的,有些提议的值可能被覆盖的;“有人临时上厕所”说明需要容错,即在机器挂掉下的分布式一致;“重新回来”说明机器recover后能知道之前决议的结果; 分布式共识问题通常需要满足Safety和Liveness的要求,具体来说就是: Safety 只有被提出的值才有可能通过决议 最终只有一个值被接受 一个参与者只有在决议达成之后才可能知道决议的值 Liveness 最终能对某个值达成决议 如果有一个值达成了决议,那么这个值能最终被参与者学习到 对于Liveness的问题想多说点,在FLP定理中讨论的模型是完全异步,crash-failure fault但网络可靠这种假设比较严格的模型,并证明了在此系统模型下不存在完整的分布式一致性算法能解决分布式共识问题(注意是完整,如果我们放弃一些Safety或者Liveness的要求,比如保证严格的Safety而使用随机化等方法保证一定概率的Liveness,这样的算法是能实现的,而这也是Paxos一类算法的取舍,毕竟放弃了Safety没太大意义了),而通常像Paxos和类Paxos算法讨论的模型比FLP中的模型更松:完全异步,网络不可靠,crash-failure fault甚至byzantine fault,所以Paxos类算法本质上也没办法完美解决Liveness的问题,Lamport的原始论文中只提到选主(选出distinguished proposer)来解决这个问题,但是至于选主本身的Liveness和容错问题并没有详细讨论,这在后面选主相关部分还会涉及到。 3.多数派 这里把多数派拿出来的原因是因为我觉得他是设计容错分布式一致性算法的前提和基础。基于前面对分布式一致问题的说明以及其需要满足的条件,我们先来看看safety的要求,关于liveness在后面会分析。为了方便说明,我们把需要设置值的叫做一个项,比如下一个日志槽位,一次决议就是针对某个项设置值。 简单来说: => 对于某个项,在没有值时,可以从提出的多个值中任意选择一个(这里意味着多个参与者可以对同一个需要达成共识的项并发发起proposal,并且各自提出不同的值,无法保证按照提出的顺序,只是保证一旦对某个值达成决议,那么后续的proposal只能重新使用已经达成决议的值,其实这也是基本的safety要求啦,也是分布式共识问题的要求),并且保证后面的决议也只能设置同一个值。 => 那么,在容错的要求下,很显然我们必须保证后续的某次决议中至少有一台存活机器知道这个项的值,而且我们允许每次决议期间有一些机器能离开(网络分区,挂掉等) => 显然多数派能满足上面的要求,在2f+1台机器下,对于每次决议都允许最多f台机器挂掉,并且能保证之前达成决议的所有项的值都至少有一台存活的机器知道 好了,我们推导出了多数派能够为分布式一致性算法提供容错的基础,下面我们基于此来尝试设计Paxos算法。 4.Paxos算法 上面多数派保证了在每次决议时都有存活机器知道之前所有达成决议的项的值。那么,怎么保证后续针对之前某个项的决议只能设置项本身的值? 先简要回顾下Paxos算法的核心部分: 达成一轮共识的流程 对于每一轮,比如针对下一个日志槽位(其实Paxos完全可以乱序,并不一定要按照日志槽位顺序)达成某个值的共识来说,每个参与者需要记录并持久化的数据有当前已见过的最大的proposal number(last_seen_proposal_id),已经对某个proposer投票的最近的proposal number(last_voted_proposal_id)以及对应的值(last_voted_proposal_value)。 阶段1 proposer选择一个proposal number向多数派的acceptor发送prepare请求(注意可以并发) acceptor接受到prepare请求后,如果请求中的poposal number大于last_voted_proposal_id,则更新last_voted_proposal_id,如果last_voted_proposal_value不为空,则带上返回prepare-ack消息;反之,则拒绝这个proposal,不返回或者带上last_voted_proposal_id返回拒绝消息,提醒proposal更新last_seen_proposal_id提高性能(原论文描述是保证不再接受比请求的proposal number小的其他决议请求,并返回已经达成的决议值,如果有的话,这里只是用具体实现描述出来了) 阶段2 如果proposer收到acceptor多数派的prepare-ack消息,则从收到的消息中找出最大的proposal id以及其对应的proposal value,如果这个value不为空,则使用这个value作为最终决议值,否则可以使用任意值(比如客户端请求的值),然后发送accept消息 如果acceptor收到proposer的accept请求,则接受,除非收到新的更高proposal number的决议请求并投票了。 学习一个已经达成共识的值 每次acceptor受到决议的时候都将决议发送给learner。这里和membership management以及日志恢复等相关联了,后面会涉及到,这里不多说 进展性的解决 Paxos算法里Lamport只是简单提到选主来解决紧张性问题,没有具体分析 OK,回到本节开始的问题 => 自然而然,分两个阶段,因为我们事先不知道针对此项是否已经达成决议(这里实际上已经暗含着Paxos算法的主要设计原则之一,即给每个决议请求编号,区分已达成的决议,后发起的决议,以及过时的决议),所以需要prepare阶段询问存活的机器,如果已经达成过,那么至少会有一台机器知道这个值,那么我们就用这个值进入accept阶段,在accept阶段,如果有多数派都同意了这个值,那么决议达成。这就是Paxos的两阶段流程。另外,为了保证能正确恢复,Paxos算法的两阶段中,在请求响应的地方需要持久化某些状态值,这个可以参考原论文。 当然,其中采用全局递增的标识给决议编号在定义两个决议的两个阶段的互相交错时的行为上起着决定性作用(这一点实际上是由于并发提决议导致的,对于leader-based的算法比如raft实际上在一个term期间内只有一个有效的leader,所有决议只能由这个leader发出,所以不存在这个问题,对于每个“”客户端请求决议”term的值不需要增加,但是当进入选主的状态时,可能会有并发的candidate发起选主决议了,此时实际上又回到了基本的Paxos,raft采用随机timeout的简单方法来解决基本Paxos的livelock问题)这一点需要较形式化地分析,不好像上述那样以逻辑推演的方式一步一步导出,因为涉及的状态转换较多。 关于liveness的问题,可能存在多个proposer交替抢占导致的livelock问题,导致针对某个项无法达成某个值的决议。这个在前面也提到FLP定理所限制的。 5.leader-based容错分布式一致性算法 这一节为后面的文章做个铺垫:-)。从前面的分析可以看到,基本Paxos在面对多个proposer并发提起决议的时候可能导致livelock的问题,虽然Lamport原论文提到每一轮Paxos开始前选出一个distinguished proposer(leader/master),但是并没有详细说明与强化leader这个概念,这也是后面很多leader-based容错分布式一致性算法强调的一点,而强leader的概念能带来很多工程上实现的简化与优化。另外对于多个client的并发请求可能导致某些值的丢失,比如对于日志的replication,client1访问proposer1,client2访问proposer2,而proposer1和proposer2都同时针对当前下一个日志项,此时可能导致某个client的值的覆盖丢失。所以实际中往往会选出一个leader,唯一一个能接受客户端请求提起决议。 除了解决上面的问题,选主还能为算法优化与简化带来更大空间。比如raft对选主做限制,保证leader上的日志都是最新且连续的,在一定程度上简化了lamport在《paxos made simple》中简单提及的multi-Paxos在leader日志恢复的步骤,另外,batch决议请求,让leader保证最新日志优化读请求(leader lease/follower lease)等。 实际上选主避免并发决议的问题后一切都相对容易理解了,只是在后续leader的日志恢复以及新recover机器的日志恢复,以及整个集群的恢复方面还会走基本Paxos的两个阶段,而在这些具体的恢复方法和步骤在不同的算法中是不同的,而从Multi-Paxos/ViewStamp replication/Zab/Raft来看,尤其是近两年来的Raft,基本上是在保证基本的容错下的safety和liveness之外加上各种限制条件来简化leader选举,日志恢复,日志复制几个阶段以及其他比如membership management,snapshot等功能的。本质上在leader-based的一致性算法中,在leader选举和日志恢复可能会用到基本Paxos,选主后的log replication实际上就是仅仅用了多数派。后面会更详细讨论。 ref: 整理的一些资料
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/50790358 在简单证明KMP之前,先分析一下朴素算法以及一种模式串没有相同字符的特殊情况下的变形,方便一步一步导入KMP算法的思路中。 朴素算法 朴素算法比较明了,不再赘述,下面是简单的代码: // time : O(n*m), space : O(1) int naive(const std::string &text, const std::string &pattern) { // corner case int len1 = text.length(); int len2 = pattern.length(); if (len2 > len1) return -1; int end = len1 - len2; for (int i = 0; i <= end; ++i) { int j; for (j = 0; j < len2; ++j) { if (text[i + j] != pattern[j]) { break; } } if (j == len2) return i; } return -1; } 分析朴素算法我们会发现,实际上对于模式串某个不匹配的位置,我们没有充分利用不匹配时产生的信息,或者说不匹配位置之前 的已匹配的相同前缀的信息。 模式串不含有相同字符 这种情况下,当模式串的一个位置不匹配的时候,我们可以优化朴素算法直接跳过前面模式串已经匹配的长度,实际上这种思路和 KMP所做的优化挺类似的,下面是代码以及简单证明: // if pattern has different chars // we can optimize it to O(n) // proof: // assume match break in index j of pattern length m // current index i : T1 T2 T3 ..Tj.. Tm ... Tn // P1 P2 P3 ..Pj.. Pm // Tj != Pj // (Pk != Pt) for 1 <= k,t <= m and k != t // (Pk == Tk) for 1 <= k < j // => P1 != Pk for 1 <= k < j // => so move i to j int special_case(const std::string &text, const std::string &pattern) { int len1 = text.length(); int len2 = pattern.length(); if (len2 > len1) return -1; int end = len1 - len2; for (int i = 0; i <= end; ++i) { int j; for (j = 0; j < len2; ++j) { if (text[i + j] != pattern[j]) { break; } } if (j == len2) return i; // notice ++i if (j != 0) { i += (j - 1); } } return -1; } KMP KMP第一遍不是特别容易理解,所以就琢磨着给出一个证明,来加深理解,所以就想出了下面这么个不是很正规和形式化的证明。关于KMP算法的流程可以搜索相关文章,比如这篇挺不错的。 前提假设:目标文本串T的长度为n,模式串P的长度为m,Arr为所谓的next数组,i为在模式串的第i个位置匹配失败。 需要证明的问题:对于形如A B X1 X2… A B Y1 Y2… A B的模式串,为什么可以将模式串直接移到最后一个A B处进行下一次匹配,而不是在中间某个A B处?也就是说为什么以中间某个 A B开头进行匹配不可能成功。(注意这里为了方便只有A B两个字符,实际上可能是多个,并且中间的A B和第一个以及最后一个 A B使可能部分重合的)。 简单证明 首先,一次匹配成功则必然有在T中的对应的位置以A B开头,所以从T中最后一个A B处开始进行下一次匹配,成功是可能的。(即是KMP算法中下一次匹配移动模式串的位置) 下面证明为什么从中间某个位置的A B处匹配不可能成功 若序列X1 X2…与序列Y1 Y2…不完全相同,显然在第二个A B串处后面不可能匹配成功 若序列X1 X2…与序列Y1 Y2…完全相同,则显然A B X1 X2…A B与A B Y1 Y2… A B是相等的更长的前缀和后缀,这自然回到了next数组 虽然不是很正规(应该很不正规…),但是还是多少能帮助理解吧:-) 最后附上kmp代码 // longest common prefix and suffix of // substr of pattern[0, i] // use dyamic programming // time : O(m), space : O(m) std::vector<int> nextArray(const std::string &pattern) { int len = pattern.length(); if (len == 0) return std::vector<int>(); std::vector<int> res(len, 0); res[0] = 0; for (int i = 1; i < len; ++i) { if (pattern[res[i - 1]] == pattern[i]) { res[i] = res[i - 1] + 1; } res[i] = res[i - 1]; } //for (auto &&ele : res) { // std::cout << ele << std::endl; //} return res; } // time : O(n) + O(m), space : O(m) int kmp(const std::string &text, const std::string &pattern) { int len1 = text.length(); int len2 = pattern.length(); if (len2 > len1) return -1; // get next array std::vector<int> next = nextArray(pattern); int i = 0; int end = len1 - len2; for (; i <= end; ++i) { int j; for (j = 0; j < len2; ++j) { if (text[i + j] != pattern[j]) { break; } } // got one if (j == len2) return i; // move to next position // notice the ++i // we can skip j == 0 if (j != 0) { i += (j - next[j - 1]); } //std::cout << "j:" << j << " i:" << i << std::endl; } return -1; }
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/50562716 之前的一篇文章感觉分析得不太完整,所以再记录点东西。 故障组合情况 对于多个节点且每个节点有多个可能状态参与的分布式系统来说,假设在有限的某个时间点上发生故障的概率为0,对于coordinator(proposer/master/leader等),在发送接收的一轮交互中,可能在发送消息前(t < t1),发送部分消息(t1 < t < t2),发送所有消息后并且接收消息前(t2 < t < t3),接收到部分消息(t3 < t < t4),接收到所有消息后发生故障(t > t4);对于每个participant(acceptor/follower/slave)来说,在接收发送的一轮交互中,可能在接收消息前(t’ < t1’),接收到消息且未发送应答(t1’ < t’ < t2’),发送应答后(t’ > t2’)发生故障。 coordinator和participants在不同的时间段发生故障的组合会有不同的能够保持全局事务状态一致的故障发生时行为以及恢复策略,而且可能不存在能保持全局事务状态一致的相应行为以及恢复策略。对于发生故障时的行为,在程序实现上我们必须用上面提到的时间段来分析,而且假定coordinator广播消息这个动作的过程中不会出现故障(这其实是比较合理的,因为即使只发送了部分消息也可以看做是有一部分participants没有收到消息,这两种情况对于最终的系统全局状态是一样的),这样程序实现上相应能简化不少。而对于故障恢复的策略以及正确性,我们可以从有节点发生故障后最终整个系统可能处于的全局状态来详细分析论证,虽然对于n个参与节点来说,其状态组合指数级增长,但是其中大多数状态可以用全称量词和存在量词描述,因为很多状态对于恢复策略是一样的。下面以2PC和3PC为例来分析,从中可以比较容易地看出2PC存在的问题,以及3PC为什么能够解决这个问题。2PC和3PC的正常流程可以参考相应的资料,这里不赘述。 故障模型 首先我们这里只考虑fail-recover的故障模型 我们只考虑在有coordinator以及participant挂掉的情况,而且coordinator本身不具有participant的身份。对于没有participant挂掉但是coordinator挂掉的情况,只需要选择新的coordinator并向所有存活的participant发送最后一条日志记录的请求就可以确定发生故障时全局事务的状态,从而恢复,所以比较简单。对于有paticipant挂掉以及coordinator没有挂掉的情况,由于coordinator知道所有participants的响应消息,所以可以决定此次事务的最终状态,可能会阻塞等待participant的恢复,但不会造成不一致。 举一个coordinator没有故障但是paticipant故障的例子:对于2PC的阶段一(即有部分participant还未收到coordinator的proposal消息),如果coordinator未发生故障,但是有participant发生故障,这种情况下,只需要取消此次proposal即可,等到故障的participant恢复后询问coordinator要相应的日志记录,不会造成最终全局事务状态的不一致。(这里关于整个系统能不能progress可能有不同说法,如果我们将故障的participant移除coordinator活跃列表那么接下来的事务(如果这里的事务只是单纯的replication)可以正常进行,但是如果分布式事务本身必须要故障的participant参与,那么整个系统必须阻塞直到participant恢复,但总之不会造成恢复后系统全局状态的不一致)。 2PC故障恢复分析 下面考虑coordinator和participant故障的情况: 1.对于2PC的阶段一(即有部分participant还未收到coordinator的proposal消息) 此时新选出的coordinator询问剩余存活节点的消息后可以直接cancel,因为不可能有节点commit 2.对于2PC的阶段二,情况稍微复杂,故障发生时,所有剩余存活节点可能的状态只能是accept/refuse/commit/abort中的一个,并且只有以下组合 (1).存活节点中返回accept的数量满足0 <= n < N(存活节点总数) a. n中除去accept的剩余全是commit => commit b. n中除去accept的剩余全是abort => abort c. n中除去accept的全是refuse => abort d. n中除去accept的剩余部分是abort,部分是abort => abort 以上几种情况下新的coordinator的abort/commit选择在故障节点恢复后都不会造成不一致。 (2).存活节点全部返回accept,即n == N 此时故障的participant可能处于的状态有: a. accept b. refuse c. commit d. abort 可以看出,无论新的coordiantor选择commit还是abort,最终participant恢复时有可能是abort或者commit,这样会导致不一致,所以整个系统只有等故障participant恢复之后,新的coordinator才可能继续,整个系统才可能progress。这也是导致2PC缺陷的根本原因。 综合(1)(2)两种情况,在(2)中由于故障的节点可能成为唯一接收到commit/abort消息的节点,所以从剩余节点中我们没办法知道整个系统的状态。因此3PC引入了prepare-commit阶段,在真正提交(commit阶段)之前,让所有节点都能知道整个系统的状态是可以提交(即coordinator收到所有accept)还是cancel(abort,即coordinator没有收到所有accept),然后在commit阶段,如果有节点挂掉了,也可以通过其他其他节点得知整个系统此次事务投票的状态,从而progress。 3PC故障恢复分析 1.对于3PC的阶段一(即有部分participant还未收到coordinator的proposal消息) 此时新选出的coordinator询问剩余存活节点的消息后可以直接cancel,因为不可能有节点commit 2.对于3PC的阶段二和阶段三,情况比较复杂,故障发生时,所有剩余存活节点可能的状态只能是accept/refuse/prepare-commit/cancel/commit中的一个,并且只有以下组合 (1).存活节点中返回accept的数量满足0 <= n < N(存活节点总数) a. n中除去accept的全是refuse => abort b. n中除去accept的全是cancel => abort c. n中除去accept的部分是refuse,部分是cancel => abort d. n(==0)中除去accept的全是prepare-commit => commit e. n(==0)中除去accept的全是commit => commit f. n(==0)中除去accept的部分是commit,部分是prepare-commit => commit 可以看出,上述所有情况,新的coordinator都可以有确定的abort/commit选择,不会造成故障节点恢复后整个系统的不一致。 (2).存活节点全部返回accept,即n == N 此时故障节点可能处于的状态有: a. accept b. refuse c. prepare-commit d. cancel e. 不可能有commit(如果是commit那么必然所有存活的都是prepare-commit,这样就避免了2PC存在的问题!) 可以看出,3PC引入prepare-commit阶段后,(2)中解决了2PC的问题。(2)中a,b,c,d四种可能情况下由于不可能出现故障节点commit的情况,所以新的coordinator都可以采取abort,从而在故障节点恢复后不会造成不一致状态。但是3PC的一个局限在于无法容忍网络分区:比如如果发生了网络分区,其中一部分的coordinator收到那一部分所有存活节点都是prepare-commit,那么会决定commit;但是另外一部分的coordinator收到的存活节点中全是accept,那么会决定abort。这样导致了整个系统状态的不一致。 总结 本文对于每种恢复情况都做了一定考虑,对于只有一个coordinator和participant的情况,我们可以画出系统的全局状态图,从而判断不同故障组合是否会导致状态转换的不确定结果,即最终的全局状态既有commit又有abort,上述的分析本质上也是将一些状态分了类。但是对于多节点的组合,感觉始终没有太严格地形式证明,在思考代码实现的时候也是总感觉不具有百分之百的说服力…状态组合爆炸也是并发与分布式的一个比较难的问题吧。
之前的一篇文章感觉分析得不太完整,所以再记录点东西。 故障组合情况 对于多个节点且每个节点有多个可能状态参与的分布式系统来说,假设在有限的某个时间点上发生故障的概率为0,对于coordinator(proposer/master/leader等),在发送接收的一轮交互中,可能在发送消息前(t < t1),发送部分消息(t1 < t < t2),发送所有消息后并且接收消息前(t2 < t < t3),接收到部分消息(t3 < t < t4),接收到所有消息后发生故障(t > t4);对于每个participant(acceptor/follower/slave)来说,在接收发送的一轮交互中,可能在接收消息前(t’ < t1’),接收到消息且未发送应答(t1’ < t’ < t2’),发送应答后(t’ > t2’)发生故障。 coordinator和participants在不同的时间段发生故障的组合会有不同的能够保持全局事务状态一致的故障发生时行为以及恢复策略,而且可能不存在能保持全局事务状态一致的相应行为以及恢复策略。对于发生故障时的行为,在程序实现上我们必须用上面提到的时间段来分析,而且假定coordinator广播消息这个动作的过程中不会出现故障(这其实是比较合理的,因为即使只发送了部分消息也可以看做是有一部分participants没有收到消息,这两种情况对于最终的系统全局状态是一样的),这样程序实现上相应能简化不少。而对于故障恢复的策略以及正确性,我们可以从有节点发生故障后最终整个系统可能处于的全局状态来详细分析论证,虽然对于n个参与节点来说,其状态组合指数级增长,但是其中大多数状态可以用全称量词和存在量词描述,因为很多状态对于恢复策略是一样的。下面以2PC和3PC为例来分析,从中可以比较容易地看出2PC存在的问题,以及3PC为什么能够解决这个问题。2PC和3PC的正常流程可以参考相应的资料,这里不赘述。 故障模型 首先我们这里只考虑fail-recover的故障模型 我们只考虑在有coordinator以及participant挂掉的情况,而且coordinator本身不具有participant的身份。对于没有participant挂掉但是coordinator挂掉的情况,只需要选择新的coordinator并向所有存活的participant发送最后一条日志记录的请求就可以确定发生故障时全局事务的状态,从而恢复,所以比较简单。对于有paticipant挂掉以及coordinator没有挂掉的情况,由于coordinator知道所有participants的响应消息,所以可以决定此次事务的最终状态,可能会阻塞等待participant的恢复,但不会造成不一致。 举一个coordinator没有故障但是paticipant故障的例子:对于2PC的阶段一(即有部分participant还未收到coordinator的proposal消息),如果coordinator未发生故障,但是有participant发生故障,这种情况下,只需要取消此次proposal即可,等到故障的participant恢复后询问coordinator要相应的日志记录,不会造成最终全局事务状态的不一致。(这里关于整个系统能不能progress可能有不同说法,如果我们将故障的participant移除coordinator活跃列表那么接下来的事务(如果这里的事务只是单纯的replication)可以正常进行,但是如果分布式事务本身必须要故障的participant参与,那么整个系统必须阻塞直到participant恢复,但总之不会造成恢复后系统全局状态的不一致)。 2PC故障恢复分析 下面考虑coordinator和participant故障的情况: 1.对于2PC的阶段一(即有部分participant还未收到coordinator的proposal消息) 此时新选出的coordinator询问剩余存活节点的消息后可以直接cancel,因为不可能有节点commit 2.对于2PC的阶段二,情况稍微复杂,故障发生时,所有剩余存活节点可能的状态只能是accept/refuse/commit/abort中的一个,并且只有以下组合 (1).存活节点中返回accept的数量满足0 <= n < N(存活节点总数) a. n中除去accept的剩余全是commit => commit b. n中除去accept的剩余全是abort => abort c. n中除去accept的全是refuse => abort d. n中除去accept的剩余部分是abort,部分是abort => abort 以上几种情况下新的coordinator的abort/commit选择在故障节点恢复后都不会造成不一致。 (2).存活节点全部返回accept,即n == N 此时故障的participant可能处于的状态有: a. accept b. refuse c. commit d. abort 可以看出,无论新的coordiantor选择commit还是abort,最终participant恢复时有可能是abort或者commit,这样会导致不一致,所以整个系统只有等故障participant恢复之后,新的coordinator才可能继续,整个系统才可能progress。这也是导致2PC缺陷的根本原因。 综合(1)(2)两种情况,在(2)中由于故障的节点可能成为唯一接收到commit/abort消息的节点,所以从剩余节点中我们没办法知道整个系统的状态。因此3PC引入了prepare-commit阶段,在真正提交(commit阶段)之前,让所有节点都能知道整个系统的状态是可以提交(即coordinator收到所有accept)还是cancel(abort,即coordinator没有收到所有accept),然后在commit阶段,如果有节点挂掉了,也可以通过其他其他节点得知整个系统此次事务投票的状态,从而progress。 3PC故障恢复分析 1.对于3PC的阶段一(即有部分participant还未收到coordinator的proposal消息) 此时新选出的coordinator询问剩余存活节点的消息后可以直接cancel,因为不可能有节点commit 2.对于3PC的阶段二和阶段三,情况比较复杂,故障发生时,所有剩余存活节点可能的状态只能是accept/refuse/prepare-commit/cancel/commit中的一个,并且只有以下组合 (1).存活节点中返回accept的数量满足0 <= n < N(存活节点总数) a. n中除去accept的全是refuse => abort b. n中除去accept的全是cancel => abort c. n中除去accept的部分是refuse,部分是cancel => abort d. n(==0)中除去accept的全是prepare-commit => commit e. n(==0)中除去accept的全是commit => commit f. n(==0)中除去accept的部分是commit,部分是prepare-commit => commit 可以看出,上述所有情况,新的coordinator都可以有确定的abort/commit选择,不会造成故障节点恢复后整个系统的不一致。 (2).存活节点全部返回accept,即n == N 此时故障节点可能处于的状态有: a. accept b. refuse c. prepare-commit d. cancel e. 不可能有commit(如果是commit那么必然所有存活的都是prepare-commit,这样就避免了2PC存在的问题!) 可以看出,3PC引入prepare-commit阶段后,(2)中解决了2PC的问题。(2)中a,b,c,d四种可能情况下由于不可能出现故障节点commit的情况,所以新的coordinator都可以采取abort,从而在故障节点恢复后不会造成不一致状态。但是3PC的一个局限在于无法容忍网络分区:比如如果发生了网络分区,其中一部分的coordinator收到那一部分所有存活节点都是prepare-commit,那么会决定commit;但是另外一部分的coordinator收到的存活节点中全是accept,那么会决定abort。这样导致了整个系统状态的不一致。 总结 本文对于每种恢复情况都做了一定考虑,对于只有一个coordinator和participant的情况,我们可以画出系统的全局状态图,从而判断不同故障组合是否会导致状态转换的不确定结果,即最终的全局状态既有commit又有abort,上述的分析本质上也是将一些状态分了类。但是对于多节点的组合,感觉始终没有太严格地形式证明,在思考代码实现的时候也是总感觉不具有百分之百的说服力…状态组合爆炸也是并发与分布式的一个比较难的问题吧。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/50557511 这篇文章主要讨论下解决分布式一致性问题的两种算法:两阶段提交(2PC)和三阶段提交(3PC)。之前感觉2PC和3PC的流程挺简单的,但是真正仔细去分析过后,才发现很多的细节。而这些细节对理解Paxos,Raft,Viewstamp Replication,Atomic Broadcast等其他更复杂的一致性算法有很大的作用。所以才在此记录一下这些细节,尤其是从工程实现的角度来思考。 具体的术语,像coordinator,participant具体指代什么,不熟悉的可以参考其他讲2PC和3PC的文章。 1.正常交互流程 这里的正常是指coordinator和participant没有挂掉的。交互流程如下所示,比较容易理解。 2PC (1). coordinator ——(proposal)—–> all participants (2). all participants —-(accept/refuse)———-> coordinator (3.1). if any of participants is refuse, then coordinator ——-(abort)——–> all participants (3.2). else coordinator ——-(commit)——-> all participants 3PC (1). coordinator ——(proposal)—–> all participants (2). all participants —-(accept/refuse)———-> coordinator (3.1). if any of participants is refuse, then coordinator ——-(cancel)——–> all participants (3.2). else coordinator ——-(prepare-commit)——-> all participants (4). all participants ——(prepare-commit-received)——> coordinator (5). if coordinator received prepare-commit-received from all participants then coordiantor —(commit)—> all participants 2.有挂掉的情况 2PC和3PC很多细节其实是在这一部分,因为在不同时间点(阶段),不同类型节点挂掉的情况下,能不能recover以及recover的结果都是不一样的(也就是容错,比如fail-recover,fail-stop,network partition等的程度不同)。我觉得严格来讲,对于coordinator和participants的挂掉的不同组合以及相应的恢复策略,应该用各自接收和发送消息的时间点严格定义,而不是笼统地说阶段1,阶段2等。由于组合情况比较多,而且有些情况的recover方式相同,这里就简单总结分类一下。 2PC和3PC最主要的区别在于coordinator挂掉的情况下,如果存在participant挂掉,那么能不能recover保证liveness(或者整个系统progress)的问题。对于2PC来说是不能的,对于3PC来说是可以的,而prepare-commit阶段起了决定性作用,这一点后面会详细分析。 2PC 有节点挂掉的可能情况(主要以coordinator的视角) (1).coordinator在未发送proposal消息给任何participant以及之前挂掉了 (2).coordinator在给一部分participant发送proposal消息后挂掉 (3).coordinator在给所有participant发送proposal消息,但是没有发送所有commit/abort消息的情况下挂掉了 (4).coordinator在发送所有commit/abort后挂掉 上述(1)和(4)是相同的情况,对于(2)recover处理比较简单,对于(3)比较麻烦,因为participants可能存在一种状态,是在有至少一个participant挂掉的情况下,整个事务状态是无法确定的。下面具体分析。 2PC coordinator recovery 这里不讨论所有participant都返回(即没有participant挂掉的情况),因为只要所有的participant都返回了,判断事务的状态就能确定了 新的coordinator向剩余的所有participant发送query请求,获得其最后一条日志记录 如果返回至少一个refuse,则新的coordinator abort 如果返回至少一个commit,则新的coordinator commit 导致可能出现不一致的情况:如果其中有一个participant挂掉没返回,而且其他节点都返回accept,这种情况下,新的coordinator无法决定是abort还是commit,因为挂掉的节点可能处于accept/refuse/commit/abort的任何一个状态,如果coordiantor commit或者abort了,都可能导致次participant恢复后与其余participant不一致。2PC最主要的限制就在这一点 3PC 有节点挂掉的可能情况(主要以coordinator的视角) (1).coordinator在未发送proposal消息给任何participant以及之前挂掉了 (2).coordinator在给一部分participant发送proposal消息后挂掉 (3).发送全部proposal消息,但是没有发送全部prepare-commit/cancel消息 (4).发送全部prepare-commit消息,但是没有发送全部commit消息 (5).发送全部commit消息。 3PC coordinator recovery 这里不讨论所有participant都返回(即没有participant挂掉的情况),因为只要所有的participant都返回了,判断事务的状态就能确定了 新的coordinator向剩余的所有participant发送query请求,获得其最后一条日志记录 如果返回至少一个refuse,则新的coordinator abort 如果返回至少一个commit,则新的coordinator commit 如果返回的所有节点中有一个不是prepare-commit,则可以安全地abort,因为不可能有节点进入commit(其实包含了第一种情况) 如果返回的节点全部是prepare-commit,此时可能会有participant挂掉,但是其可能的状态为accept/prepare-commit/commit,这三种情况下此participant恢复的时候都能commit,所以此时新的coordinator可以决定提交,不会造成恢复后的不一致状态。这一点是与2PC最大的区别 3.总结 综上,最核心的还是recovery中2PC和3PC的最后一点,也是加入prepare-commit阶段后造成的本质区别。当然虽然3PC保证了participant挂掉的时候系统能够继续progress(也就是能容错),但是其也存在问题,比如在网络分区的时候,刚好coordinator所在的一部分能commit,但是另一部分重新选择coordinator后不能commit,这样分区恢复后会导致不一致,这种情况就是Paxos,Raft等算法能解决的,后面会结合这些更复杂一些的算法分析。其实,对于分布式一致性算法来说,了解其历史对了解算法本质是很有帮助的。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/50347281 本文主要提炼了《Paxos Make Simple》中的一些主要观点,然后加上自己的理解,使用通俗的语言尝试做一些解释。 关于Paxos算法背景和一致性相关问题可以参见原论文 算法涉及的主要对象 action 对一条记录(某个变量)的一次操作(这一点只是本人便于后面理解加上的) 这里选用操作这个词,而不是值,因为一个在对某个变量达成某个值的共识前可能已经经过多个更新操作,所以为了区别,使用操作作为每次proposal的对象,而操作的值代表具体的修改动作,而且这也算是状态机复制(SMR)的一个基本组成单元,个人感觉更易于理解。比如action(log_id, log_content),log_id全局标识了此action的唯一性,log_content通常是针对某条记录的修改,可看做action的值。 proposer 发起proposal的机器,注意在Paxos算法中允许多台机器同时发起proposal,并且有可能由于并发获取”需要达成一致的下一操作(action)”,从而使得不同的proposal针对同一个”需要达成一致的下一操作”达成共识,但是算法保证了其达成共识的action的值相同。 acceptor 接受来自proposer的proposal,并根据对于proposer的prepare和accept消息做出响应。 learner 从错误中恢复的机器,需要重新学习出错之前最后一次accpet的proposal id之后的所有proposal Paxos instance 针对某个”需要达成一致的操作(action)”运行一次Paxos算法的完整过程。 算法推导逻辑 P0. To ensure data integrity under fault tolerence, a proposal is succeeded only when more than half machines accepted the proposal. notice: P1[a] stands for requirement and algorithm for acceptors; P2[abc] stands for requirement and algorithm for proposer. P1. An acceptor must accept the first proposal that it receives => problem : maybe two proposal are proposed at the same time and two less-than-half machine quorums receive separately these two different proposals, then these two proposals can not be succeeded. => so we must allow each acceptor to receive multiple proposals for the same value => so we must give each proposal a global unique and increasing id P1a. An acceptor can accept a proposal numbered n iff it has not responed to a prepare request having a number greater than n => P1a -> P1 because this can ensure acceptor do not receive the before proposals which arrive later => so we ignore these proposal whose id is <= accepted and prepared id of acceptors P2. If a proposal with value v is chosen, then every higher-numbered proposal that is chosen has value v => this is because we must ensure there is only a specific value chosen for a specific paxos instance which may contains multiple proposals P2a. if a proposal with value v is chosen, then every higher-numbered proposal accepted by any acceptor has value v => notice: P2a -> P2 P2b. if a proposal with value v is chosen, then every higher-numbered proposal issued by any proposer has value v => notice: P2b -> P2a -> P2 P2c. for any v and n, if a proposal with value v and number n is issued, then there is a set S consisting of a majority of acceptors such that either (a) no acceptor in S has accepted any proposal numbered less than n (b) v is set to the value of the highest-numbered proposal among all proposals numbered less than n and accepted by the acceptors in S => notice: P2c -> P2b -> P2a -> P2 => this is the specific algorithm for proposer in prepare phase 总结 根据上面的推导,核心的就两点,P1a和P2c,P1a规定了acceptor的行为,P2c规定了proposer的行为,由于P2c的需求,决定了需要有prepare阶段,这阶段主要是为了accept阶段为当前proposal设置正确的值。 算法基本流程 论文上主要有prepare和accept两个阶段,省略了选action(值)和选proposal id的阶段 0.数据结构 每台机器需要记录最大accpeted的proposal id(latest_accepted_id)和对应的accepted的操作(latest_accepted_action)以及最大promised的proposal id(latest_promised_id),这些数据需要刷盘。 1.选择需要达成一致的操作 来自客户端的请求,比如 Action{write A 12} => 通常这作为需要达成的某个操作的值,还需要一个全局唯一的id标识这个操作,比如对于某个log记录达成一致,需要寻找下一次需要记录的log id,这就需要向其他节点询问其记录的最近log id,并取最大值+1作为下一次需要达成一致的”记录日志这个操作(action)”的action(log) id。而这个过程可能会产生并发问题,即不同的机器可能针对同一个log id发起proposal,这一点后面阶段保证了一旦达成了proposal,则后续所有proposal都以相同的操作(值)达成。 2.选择proposal id proposal id需要保证全局唯一递增(这个后面补充)。 3.prepare 假设2中选择的proposal id为n,proposer发送prepare(n)给大多数机器 对于acceptor,如果(n > latest_promised_id) /\ (n > latest_promised_id) 如果acceptor已经有latest_accepted_id(说明之前对于同一个操作已经达到proposal了),则返回对应于latest_accepted_id的操作的值,为了accept阶段保证当前的proposal和以前已经达成的proposal最终操作值一样。 如果acceptor没有latest_accepted_id(说明之前还未达成proposal),则不用返回值(accept阶段可以使用任意proposed的值) 令latest_accepted_id = latest_promised_id = n,并保证不再接受proposal id小于latest_promised_id的proposal 否则acceptor返回拒绝,重新开始算法。 4.accept proposer收到大多数的机器对prepare的回复 如果返回消息中latest_accepted_action集合不为空,则将当前proposal的action设置为对应于最大latest_accepted_id的latest_accepted_action,发送accept(n, action)消息 如果返回消息中latest_accepted_action集合为空,则直接使用当前proposal的action(也就是论文中所说的any value),发送accept(n, action)消息 如果acceptor收到accept(n, action)消息时 latest_promised_id > n(说明有更新的),则放弃当前proposal,重新进入算法。 否则接受proposal,完成此次proposal 如果proposer收到大多数acceptor的成功消息,则成功返回给客户端,否则重新进入算法,由于liveness requirement,一个proposed的value必须eventually chosen,所以要么客户端返回成功,要么客户端请求超时,对于超时,客户端需要重新发起读的请求,此时可能已经成功了,否则继续重新发超时请求。 几点辅助理解的说明 多数是为了保证至少会有一台机器记录了上次达成的proposal的值,这样保证在不多于n/2台机器挂掉的条件下,在每次proposal的过程中,至少有一台机器有前面所有的proposal值的记录,从而保证所有的数据的完整。 一轮paxos instance 是针对某个变量的一次操作的,而不是同一个变量。比如针对同一变量的一次操作打一次log,而这个log id应当是唯一的,而且针对这条log可能会有多次proposal,但是只要有一次proposal已经达成,那么针对这条log的proposal只能使用相同的log值更新(这也是为什么在prepare返回阶段,如果有一个acceptor已经达成过proposal,则返回其值替换当前proposal值)。 对某个唯一的记录比如log或者变量的某次操作达成一致,那么proposer在发起proposal之前必定要到某个地方取下次需要达成一致的值,比如下一条日志记录的id,某个变量的下一个版本(某个变量的下一次操作)。而由于proposer可能有多个,那么在并发发起proposal时,不同的proposal可能会针对相同的某次操作,这时对于后达成的proposal来说,只能将其propose的值换为已经达成的proposal的值,而这个过程是通过prepare阶段accptor返回的结果集是否空来判断的。如果结果集不为空,说明针对此次操作,之前已经达成了一致,则后续proposal只能使用相同值;如果为空,那么可以使用此次proposal的值(也就是论文中所说的any value)。另外,在accept阶段,如果有accptor的最小promise id大于当前proposal id,那么说明已经有更更大proposal id的proposal先到达了(此时不管之前是否已经达成一致),此时需要放弃当前次的proposal 下面给一个一轮Paxos算法的伪代码: # 一轮Paxos协议的流程 # 此处为了清晰将proposer和acceptor的逻辑分开写了,实际上原论文中一个server既可以做proposer也可以做acceptor # 对每一个值(比如logid唯一的一条日志)可能会同时发起写请求(比如两个客户端并发访问qorumn里面的两个server) # 所以此时这两个server都是proposer,针对同一条logid发起不同ballot number的决议请求。此时,如果是ballot number # 小的那个决议请求先达到多数派,那么应该保证后到的ballot number的请求使用相同的值。所以Acceptor需要做的事情如下: # prepare phase: # 1.如果请求的req_ballot_id比当前server已经应答过的last_ballot_id小,此时直接忽略,因为有更新的投票决议。 # 2.如果请求的req_ballot_id大于等于当前server已经应答过的last_ballot_id,此时使用req_ballot_id更新last_ballot_id,并返回last_voted_value,注意这个可以是空,说明要么是当前这个server以前未参与此值的多数派投票,要么是此值还未达成过多数派投票。 # commit phase: # 1.如果commit消息数据中的ballot_id与last_ballot_id不同,则放弃 # 2.否则更新相应的值,并写日志 class Acceptor(object): last_ballot_id = None #我正在等着 last_voted_value = None last_voted_ballot_id = None servers = [] def handleProposalRequest(self, reqData): req_ballot_id = reqData.ballot_id if req_ballot_id < self.last_ballot_id: pass # return nothing else: self.last_ballot_id = req_ballot_id return (self.last_ballot_id, self.last_voted_value, self.last_voted_ballot_id) def handleCommitRequest(self, reqData): commit_ballot_id = reqData.last_sent_ballot_id client_value = reqData.client_req_value if commit_ballot_id != self.last_ballot_id: pass self.last_ballot_id = self.last_voted_ballot_id = reqData.last_sent_ballot_id self.last_voted_value = client_value writelog((self.last_ballot_id, self.last_voted_ballot_id, self.last_voted_value)) def writelog(self, data): pass # 1.如果有acceptor接收到其他proposal发出的更大ballot_id的决议请求,那么放弃此次决议 # 2.如果为达到多数派,放弃此次决议 # 3.如果acceptor中返回的last_voted_value不为空,则将当前proposal的值设置为相同值,进入commit阶段,否则直接用client_req_value作为值进入commit class Proposer(object): last_sent_ballot_id = None client_req_value = None res_data = [] servers = [] quorumn_number = 5 def sendRequest(self, data): pass def sendProposal(self): self.last_sent_ballot_id += 1 reqData.ballot_id = self.last_sent_ballot_id for i in self.servers: sendRequest(i, reqData) def readData(server): pass def handleEachProposalResponse(self, server): resData = self.readData(server) self.res_data.append((resData.last_ballot_id, resData.last_voted_value, resData.last_voted_ballot_id)) def handleProposalResponse(self): for i in servers: handleEachProposalResponse(servers[i]) res_ballot_id = max([i[0] for i in self.res_data]) if res_ballot_id > self.last_sent_ballot_id: pass # maybe another proposer has finished a proposal for the value, what should we give back to client? elif len(res_data) < (self.quorumn_number / 2 + 1): pass # failed, maybe timeout due to network or server crash? else: voted_data = [(i[1], i[2]) for i in self.res_data] voted_data.sort() if voted_data[0][1] is not None: self.client_req_value = voted_data[0][1] self.commit((self.last_sent_ballot_id, self.client_req_value)) def commit(self, reqData): pass ref: Paxos Made Simple
本文主要提炼了《Paxos Make Simple》中的一些主要观点,然后加上自己的理解,使用通俗的语言尝试做一些解释。 关于Paxos算法背景和一致性相关问题可以参见原论文 算法涉及的主要对象 action 对一条记录(某个变量)的一次操作(这一点只是本人便于后面理解加上的) 这里选用操作这个词,而不是值,因为一个在对某个变量达成某个值的共识前可能已经经过多个更新操作,所以为了区别,使用操作作为每次proposal的对象,而操作的值代表具体的修改动作,而且这也算是状态机复制(SMR)的一个基本组成单元,个人感觉更易于理解。比如action(log_id, log_content),log_id全局标识了此action的唯一性,log_content通常是针对某条记录的修改,可看做action的值。 proposer 发起proposal的机器,注意在Paxos算法中允许多台机器同时发起proposal,并且有可能由于并发获取”需要达成一致的下一操作(action)”,从而使得不同的proposal针对同一个”需要达成一致的下一操作”达成共识,但是算法保证了其达成共识的action的值相同。 acceptor 接受来自proposer的proposal,并根据对于proposer的prepare和accept消息做出响应。 learner 从错误中恢复的机器,需要重新学习出错之前最后一次accpet的proposal id之后的所有proposal Paxos instance 针对某个”需要达成一致的操作(action)”运行一次Paxos算法的完整过程。 算法推导逻辑 P0. To ensure data integrity under fault tolerence, a proposal is succeeded only when more than half machines accepted the proposal. notice: P1[a] stands for requirement and algorithm for acceptors; P2[abc] stands for requirement and algorithm for proposer. P1. An acceptor must accept the first proposal that it receives => problem : maybe two proposal are proposed at the same time and two less-than-half machine quorums receive separately these two different proposals, then these two proposals can not be succeeded. => so we must allow each acceptor to receive multiple proposals for the same value => so we must give each proposal a global unique and increasing id P1a. An acceptor can accept a proposal numbered n iff it has not responed to a prepare request having a number greater than n => P1a -> P1 because this can ensure acceptor do not receive the before proposals which arrive later => so we ignore these proposal whose id is <= accepted and prepared id of acceptors P2. If a proposal with value v is chosen, then every higher-numbered proposal that is chosen has value v => this is because we must ensure there is only a specific value chosen for a specific paxos instance which may contains multiple proposals P2a. if a proposal with value v is chosen, then every higher-numbered proposal accepted by any acceptor has value v => notice: P2a -> P2 P2b. if a proposal with value v is chosen, then every higher-numbered proposal issued by any proposer has value v => notice: P2b -> P2a -> P2 P2c. for any v and n, if a proposal with value v and number n is issued, then there is a set S consisting of a majority of acceptors such that either (a) no acceptor in S has accepted any proposal numbered less than n (b) v is set to the value of the highest-numbered proposal among all proposals numbered less than n and accepted by the acceptors in S => notice: P2c -> P2b -> P2a -> P2 => this is the specific algorithm for proposer in prepare phase 总结 根据上面的推导,核心的就两点,P1a和P2c,P1a规定了acceptor的行为,P2c规定了proposer的行为,由于P2c的需求,决定了需要有prepare阶段,这阶段主要是为了accept阶段为当前proposal设置正确的值。 算法基本流程 论文上主要有prepare和accept两个阶段,省略了选action(值)和选proposal id的阶段 0.数据结构 每台机器需要记录最大accpeted的proposal id(latest_accepted_id)和对应的accepted的操作(latest_accepted_action)以及最大promised的proposal id(latest_promised_id),这些数据需要刷盘。 1.选择需要达成一致的操作 来自客户端的请求,比如 Action{write A 12} => 通常这作为需要达成的某个操作的值,还需要一个全局唯一的id标识这个操作,比如对于某个log记录达成一致,需要寻找下一次需要记录的log id,这就需要向其他节点询问其记录的最近log id,并取最大值+1作为下一次需要达成一致的”记录日志这个操作(action)”的action(log) id。而这个过程可能会产生并发问题,即不同的机器可能针对同一个log id发起proposal,这一点后面阶段保证了一旦达成了proposal,则后续所有proposal都以相同的操作(值)达成。 2.选择proposal id proposal id需要保证全局唯一递增(这个后面补充)。 3.prepare 假设2中选择的proposal id为n,proposer发送prepare(n)给大多数机器 对于acceptor,如果(n > latest_promised_id) /\ (n > latest_promised_id) 如果acceptor已经有latest_accepted_id(说明之前对于同一个操作已经达到proposal了),则返回对应于latest_accepted_id的操作的值,为了accept阶段保证当前的proposal和以前已经达成的proposal最终操作值一样。 如果acceptor没有latest_accepted_id(说明之前还未达成proposal),则不用返回值(accept阶段可以使用任意proposed的值) 令latest_accepted_id = latest_promised_id = n,并保证不再接受proposal id小于latest_promised_id的proposal 否则acceptor返回拒绝,重新开始算法。 4.accept proposer收到大多数的机器对prepare的回复 如果返回消息中latest_accepted_action集合不为空,则将当前proposal的action设置为对应于最大latest_accepted_id的latest_accepted_action,发送accept(n, action)消息 如果返回消息中latest_accepted_action集合为空,则直接使用当前proposal的action(也就是论文中所说的any value),发送accept(n, action)消息 如果acceptor收到accept(n, action)消息时 latest_promised_id > n(说明有更新的),则放弃当前proposal,重新进入算法。 否则接受proposal,完成此次proposal 如果proposer收到大多数acceptor的成功消息,则成功返回给客户端,否则重新进入算法,由于liveness requirement,一个proposed的value必须eventually chosen,所以要么客户端返回成功,要么客户端请求超时,对于超时,客户端需要重新发起读的请求,此时可能已经成功了,否则继续重新发超时请求。 几点辅助理解的说明 多数是为了保证至少会有一台机器记录了上次达成的proposal的值,这样保证在不多于n/2台机器挂掉的条件下,在每次proposal的过程中,至少有一台机器有前面所有的proposal值的记录,从而保证所有的数据的完整。 一轮paxos instance 是针对某个变量的一次操作的,而不是同一个变量。比如针对同一变量的一次操作打一次log,而这个log id应当是唯一的,而且针对这条log可能会有多次proposal,但是只要有一次proposal已经达成,那么针对这条log的proposal只能使用相同的log值更新(这也是为什么在prepare返回阶段,如果有一个acceptor已经达成过proposal,则返回其值替换当前proposal值)。 对某个唯一的记录比如log或者变量的某次操作达成一致,那么proposer在发起proposal之前必定要到某个地方取下次需要达成一致的值,比如下一条日志记录的id,某个变量的下一个版本(某个变量的下一次操作)。而由于proposer可能有多个,那么在并发发起proposal时,不同的proposal可能会针对相同的某次操作,这时对于后达成的proposal来说,只能将其propose的值换为已经达成的proposal的值,而这个过程是通过prepare阶段accptor返回的结果集是否空来判断的。如果结果集不为空,说明针对此次操作,之前已经达成了一致,则后续proposal只能使用相同值;如果为空,那么可以使用此次proposal的值(也就是论文中所说的any value)。另外,在accept阶段,如果有accptor的最小promise id大于当前proposal id,那么说明已经有更更大proposal id的proposal先到达了(此时不管之前是否已经达成一致),此时需要放弃当前次的proposal ref: Paxos Made Simple
无论是调试优化应用程序或者内核程序,知道怎么以及去哪查找与修改需要的程序信息是很重要的,下面总结了一些常见的几种方法。 基本配置与状态信息,主要是基于基本的配置文件,内核导出数据结构,编译内核生成的配置和符号文件以及基本的工具命令。比如: /etc,/proc,/sys,/boot/config*,/boot/System-*-.map 以及 top,netstat,sysstat包,sysctl,getconf,sar,free等工具。 常见调试工具,比如gdb,llvm-db,strace,valgrind,readelf/objdump/objcopy/nm等二进制工具。 各种tracer,比如perf,ftrace,systemtap,kprobe,uprobe等 用或者监控系统调用接口,获得想要的信息。 直接写内核模块,dump想要的数据结构的内存。 源码。
最近整理了下自己接触过的一些分布式系统与并行并发相关的一些不错资料,主要包括论文、书籍、MOOC、博客、社交问答等,由于太多,没有全部看完,所以质量可能因人而异,但是还是希望对感兴趣的同学有点帮助吧。主要涉及的内容包括(后面会继续更新): 资料地址 a reading list for distributed systems 分布式系统相关理论 time, order, logic clock, vector clock… fault-tolerence replication and consensus algorithms FLP, CAP, BASE distributed transaction formal method … 分布式计算框架 batch DAG memory stream graph … 分布式存储系统 fs database 集群管理系统 并行并发编程模型
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/49530991 本文接上一篇Linux内核协议栈-初始化流程分析,在上一篇中主要分析了了Linux内核协议栈涉及到的关键初始化函数,在这一篇文章中将分析协议栈的BSD socket和到传输层的流程。采取的方式是分析socket相关的主要系统调用。针对不同的系统调用,其到达的协议层深度可能不同,有的基本只到sock层就够了,但是有些可能需要会涉及到比如tcp的具体细节和更底层的细节。本文基本追溯到传输层的开始,再深入的细节后续文章分析。 1.准备 协议的基本分层: (A代表socket的某个系统调用) BSD socket system calls A => proto_ops->A => sock->A => tcp_prot => A BSD socket层和具体协议族某个类型的联系是通过struct proto_ops,在include/linux/net.h中定义了不同协议族如af_inet,af_unix等的通用操作函数指针的结构体struct proto_ops,具体的定义有各个协议族的某个类型的子模块自己完成。比如ipv4/af_inet.c中定义的af_inet family的tcp/udp等相应的struct proto_ops。 由于对于每个family的不同类型,其针对socket的某些需求可能不同,所以抽了一层struct sock出来,sock->sk_prot挂接到具体tcp/udp等传输层的struct proto上(具体定义在ipv4/tcp_ipv4.c,ipv4/udp.c) 另外,由于内容比较多,这一篇主要分析socket,bind,listen,accept几个系统调用,下一篇会涉及connect,send,recv等的分析 //不同协议族的通用函数hooks //比如af_inet相关的定义在ipv4/af_inet.c中 //除了创建socket为系统调用外,基本针对socket层的操作函数都在这里面 struct proto_ops { int family; struct module *owner; int (*release) (struct socket *sock); int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags); int (*socketpair)(struct socket *sock1, struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags); int (*getname) (struct socket *sock, struct sockaddr *addr, int *sockaddr_len, int peer); unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait); int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); #ifdef CONFIG_COMPAT int (*compat_ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); #endif int (*listen) (struct socket *sock, int len); int (*shutdown) (struct socket *sock, int flags); int (*setsockopt)(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen); /*省略部分*/ }; //传输层的proto //作为sock->sk_prot与具体传输层的hooks struct proto { void (*close)(struct sock *sk, long timeout); int (*connect)(struct sock *sk, struct sockaddr *uaddr, int addr_len); int (*disconnect)(struct sock *sk, int flags); struct sock * (*accept)(struct sock *sk, int flags, int *err); int (*ioctl)(struct sock *sk, int cmd, unsigned long arg); int (*init)(struct sock *sk); void (*destroy)(struct sock *sk); void (*shutdown)(struct sock *sk, int how); int (*setsockopt)(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen); int (*getsockopt)(struct sock *sk, int level, int optname, char __user *optval, int __user *option); #ifdef CONFIG_COMPAT int (*compat_setsockopt)(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen); int (*compat_getsockopt)(struct sock *sk, int level, int optname, char __user *optval, int __user *option); int (*compat_ioctl)(struct sock *sk, unsigned int cmd, unsigned long arg); #endif int (*sendmsg)(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len); int (*recvmsg)(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len, int noblock, int flags, int *addr_len); int (*sendpage)(struct sock *sk, struct page *page, int offset, size_t size, int flags); int (*bind)(struct sock *sk, struct sockaddr *uaddr, int addr_len); /*省略部分*/ }; 同时附上其他几个关键结构体: //bsd socket层 //include/linux/net.h struct socket { socket_state state; kmemcheck_bitfield_begin(type); short type; kmemcheck_bitfield_end(type); unsigned long flags; struct socket_wq __rcu *wq; struct file *file; struct sock *sk; const struct proto_ops *ops; }; //sock层 struct sock { sock_common __sk_common; #define sk_node __sk_common.skc_node #define sk_nulls_node __sk_common.skc_nulls_node #define sk_refcnt __sk_common.skc_refcnt #define sk_tx_queue_mapping __sk_common.skc_tx_queue_mapping #define sk_dontcopy_begin __sk_common.skc_dontcopy_begin #define sk_dontcopy_end __sk_common.skc_dontcopy_end #define sk_hash __sk_common.skc_hash #define sk_portpair __sk_common.skc_portpair #define sk_num __sk_common.skc_num #define sk_dport __sk_common.skc_dport #define sk_addrpair __sk_common.skc_addrpair #define sk_daddr __sk_common.skc_daddr #define sk_rcv_saddr __sk_common.skc_rcv_saddr #define sk_family __sk_common.skc_family #define sk_state __sk_common.skc_state #define sk_reuse __sk_common.skc_reuse #define sk_reuseport __sk_common.skc_reuseport #define sk_ipv6only __sk_common.skc_ipv6only #define sk_bound_dev_if __sk_common.skc_bound_dev_if #define sk_bind_node __sk_common.skc_bind_node #define sk_prot __sk_common.skc_prot #define sk_net __sk_common.skc_net #define sk_v6_daddr __sk_common.skc_v6_daddr #define sk_v6_rcv_saddr __sk_common.skc_v6_rcv_saddr unsigned long sk_flags; struct dst_entry *sk_rx_dst; struct dst_entry __rcu *sk_dst_cache; spinlock_t sk_dst_lock; atomic_t sk_wmem_alloc; atomic_t sk_omem_alloc; int sk_sndbuf; struct sk_buff_head sk_write_queue; /*省略部分*/ struct pid *sk_peer_pid; const struct cred *sk_peer_cred; long sk_rcvtimeo; long sk_sndtimeo; void *sk_protinfo; struct timer_list sk_timer; ktime_t sk_stamp; u16 sk_tsflags; u32 sk_tskey; struct socket *sk_socket; void *sk_user_data; struct page_frag sk_frag; struct sk_buff *sk_send_head; /*省略部分*/ }; 2.开始 主要追溯几个典型的socket相关的系统调用,如socket,bind,listen,accept等等 socket //创建socket的系统调用 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { int retval; struct socket *sock; int flags; /* Check the SOCK_* constants for consistency. */ BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC); BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK); flags = type & ~SOCK_TYPE_MASK; if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) return -EINVAL; type &= SOCK_TYPE_MASK; if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; //分配inode,返回inode中的一个成员作为sock retval = sock_create(family, type, protocol, &sock); if (retval < 0) goto out; //找个fd映射sock //得到空fd //分配伪dentry和file,并将socket file的operations与file挂接 retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); /*省略部分*/ } socketpair //创建socketpair,注意af_inet协议族下没有pair,af_unix下有 SYSCALL_DEFINE4(socketpair, int, family, int, type, int, protocol, int __user *, usockvec) { struct socket *sock1, *sock2; int fd1, fd2, err; struct file *newfile1, *newfile2; int flags; flags = type & ~SOCK_TYPE_MASK; if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) return -EINVAL; type &= SOCK_TYPE_MASK; if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; //创建socket1 err = sock_create(family, type, protocol, &sock1); if (err < 0) goto out; //创建socket2 err = sock_create(family, type, protocol, &sock2); if (err < 0) goto out_release_1; //调用socket operations的socketpair //关于不同协议层的函数hook,公共结构体是struct proto_ops //对于不同的family,比如af_inet协议族的定义在ipv4/af_inet.c // //对于af_inet没有socketpair //对于af_unix有socketpair err = sock1->ops->socketpair(sock1, sock2); if (err < 0) goto out_release_both; //后面部分就很类似了,找到空fd,分配file,绑定到socket,将file 安装到当前进程 fd1 = get_unused_fd_flags(flags); if (unlikely(fd1 < 0)) { err = fd1; goto out_release_both; } fd2 = get_unused_fd_flags(flags); if (unlikely(fd2 < 0)) { err = fd2; goto out_put_unused_1; } newfile1 = sock_alloc_file(sock1, flags, NULL); if (unlikely(IS_ERR(newfile1))) { err = PTR_ERR(newfile1); goto out_put_unused_both; } newfile2 = sock_alloc_file(sock2, flags, NULL); if (IS_ERR(newfile2)) { err = PTR_ERR(newfile2); goto out_fput_1; } err = put_user(fd1, &usockvec[0]); if (err) goto out_fput_both; err = put_user(fd2, &usockvec[1]); if (err) goto out_fput_both; audit_fd_pair(fd1, fd2); fd_install(fd1, newfile1); fd_install(fd2, newfile2); /* fd1 and fd2 may be already another descriptors. * Not kernel problem. */ return 0; bind SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen) { struct socket *sock; struct sockaddr_storage address; int err, fput_needed; //根据fd查找file,进而查找socket指针sock sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { //把用户态地址数据移到内核态 //调用copy_from_user err = move_addr_to_kernel(umyaddr, addrlen, &address); if (err >= 0) { //security hook err = security_socket_bind(sock, (struct sockaddr *)&address, addrlen); if (!err) //ok, 到具体family定义的proto_ops中的bind //比如对af_inet,主要是设置socket->sock->inet_sock的一些参数,比如接收地址,端口什么的 err = sock->ops->bind(sock, (struct sockaddr *) &address, addrlen); } fput_light(sock->file, fput_needed); } return err; } listen listen所做的事情也比较简单,从系统调用的listen(fd, backlog)到proto_ops 的inet_listen与前面类似,这里分析下inet_listen中的核心函数inet_csk_listen_start(位于ipv4/inet_connection_sock.c中)。 int inet_csk_listen_start(struct sock *sk, const int nr_table_entries) { //获得网络层inte_sock struct inet_sock *inet = inet_sk(sk); //管理request connection的结构体 struct inet_connection_sock *icsk = inet_csk(sk); //分配backlog个长度的accpet_queue的结构连接请求的队列 int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries); if (rc != 0) return rc; sk->sk_max_ack_backlog = 0; sk->sk_ack_backlog = 0; inet_csk_delack_init(sk); /* There is race window here: we announce ourselves listening, * but this transition is still not validated by get_port(). * It is OK, because this socket enters to hash table only * after validation is complete. */ //切换状态到listening sk->sk_state = TCP_LISTEN; if (!sk->sk_prot->get_port(sk, inet->inet_num)) { inet->inet_sport = htons(inet->inet_num); //更新dst_entry表 sk_dst_reset(sk); sk->sk_prot->hash(sk); return 0; } sk->sk_state = TCP_CLOSE; __reqsk_queue_destroy(&icsk->icsk_accept_queue); return -EADDRINUSE; } accept 上面socket, socketpair, bind基本只涉及到BSD socket, sock层相关的,过程比较简单,而accept层在sock层和tcp层交互稍微复杂,下面详细分析 //socket.c //accept系统调用 SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr, int __user *, upeer_addrlen, int, flags) { /*省略部分*/ err = -ENFILE; //for client socket newsock = sock_alloc(); if (!newsock) goto out_put; newsock->type = sock->type; newsock->ops = sock->ops; /* * We don't need try_module_get here, as the listening socket (sock) * has the protocol module (sock->ops->owner) held. */ __module_get(newsock->ops->owner); //得到当前进程空fd,分给newsock file newfd = get_unused_fd_flags(flags); if (unlikely(newfd < 0)) { err = newfd; sock_release(newsock); goto out_put; } //从flab分配空file结构 newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name); if (unlikely(IS_ERR(newfile))) { err = PTR_ERR(newfile); put_unused_fd(newfd); sock_release(newsock); goto out_put; } err = security_socket_accept(sock, newsock); if (err) goto out_fd; //proto_ops中的accept //accept从系统调用到具体协议族的某个type的struct proto_ops的accept如af_inet tcp的的accept,再到sock层的accept,然后sock层的accept实际上对应的是具体传输层的struct proto中的accpet,如tcp/udp的struct proto tcp_prot/udp_prot,然后放入newsock err = sock->ops->accept(sock, newsock, sock->file->f_flags); if (err < 0) goto out_fd; if (upeer_sockaddr) { if (newsock->ops->getname(newsock, (struct sockaddr *)&address, &len, 2) < 0) { err = -ECONNABORTED; goto out_fd; } //拷贝client socket addr storage到userspace err = move_addr_to_user(&address, len, upeer_sockaddr, upeer_addrlen); if (err < 0) goto out_fd; } fd_install(newfd, newfile); err = newfd; /*省略部分*/ } //ipv4/af_inet.c //inet family的tcp相关的proto_ops int inet_accept(struct socket *sock, struct socket *newsock, int flags) { struct sock *sk1 = sock->sk; int err = -EINVAL; //进入(网络)sock层,accept新sock struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err); if (!sk2) goto do_err; //锁住sock,因为需要操作sock内的request_socket请求队列头 wait_queue_head_t等数据 lock_sock(sk2); sock_rps_record_flow(sk2); WARN_ON(!((1 << sk2->sk_state) & (TCPF_ESTABLISHED | TCPF_SYN_RECV | TCPF_CLOSE_WAIT | TCPF_CLOSE))); sock_graft(sk2, newsock); //设置client socket状态 newsock->state = SS_CONNECTED; err = 0; release_sock(sk2); do_err: return err; } //ipv4/tcp_ipv4.c //这里进入struct proto tcp_prot中的accept struct sock *inet_csk_accept(struct sock *sk, int flags, int *err) { struct inet_connection_sock *icsk = inet_csk(sk); //icsk : inet_connection_sock 面向连接的客户端连接处理相关的信息 //接收队列 struct request_sock_queue *queue = &icsk->icsk_accept_queue; struct sock *newsk; struct request_sock *req; int error; //lock sock lock_sock(sk); //如果不是ACCPET状态转换过来,出错 error = -EINVAL; if (sk->sk_state != TCP_LISTEN) goto out_err; //如果request_sock队列是空的, 利用等待队列挂起当前进程到等待队列,并且将等待队列放入sock中的请求队列头 if (reqsk_queue_empty(queue)) { //如果非阻塞,0,否则为sk的接收时间 long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); error = -EAGAIN; if (!timeo) //如果非阻塞而且接收队列是空,直接返回-EAGAIN goto out_err; //阻塞情况下,等待timeo时间的超时 //利用了等待队列,下面会详细注解 error = inet_csk_wait_for_connect(sk, timeo); if (error) goto out_err; } //不是空,移出一个连接请求 req = reqsk_queue_remove(queue); //连接请求的sock newsk = req->sk; //减少backlog sk_acceptq_removed(sk); //fastopenq? if (sk->sk_protocol == IPPROTO_TCP && queue->fastopenq != NULL) { spin_lock_bh(&queue->fastopenq->lock); if (tcp_rsk(req)->listener) { /* We are still waiting for the final ACK from 3WHS * so can't free req now. Instead, we set req->sk to * NULL to signify that the child socket is taken * so reqsk_fastopen_remove() will free the req * when 3WHS finishes (or is aborted). */ req->sk = NULL; req = NULL; } spin_unlock_bh(&queue->fastopenq->lock); } //ok,清理,返回newsk /*省略部分*/ //ipv4/inet_connection_sock.c //accept连接请求的核心函数 static int inet_csk_wait_for_connect(struct sock *sk, long timeo) { struct inet_connection_sock *icsk = inet_csk(sk); //定义一个等待队列wait_queue_t wait 进程是当前进程 DEFINE_WAIT(wait); int err; for (;;) { //sk_leep(sk) : sock的wait_queue_head_t //wait : wait_queue_t //这里将current进程的wait_queue_t加入sk的wait_queue_head_t中,spin锁定 //wait_queue_head_t,设置current状态,然后spin解锁时可能重新schedule prepare_to_wait_exclusive(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); //被唤醒,解锁sock release_sock(sk); //如果请求队列为空,说明timeout了 if (reqsk_queue_empty(&icsk->icsk_accept_queue)) //schedule timeout timeo = schedule_timeout(timeo); //再锁住进行下次循环,准备再次进入TASK_INTERRUPTIBLE lock_sock(sk); err = 0; //检查是否有连接到达, 如果有,break,唤醒等待队列 if (!reqsk_queue_empty(&icsk->icsk_accept_queue)) break; err = -EINVAL; //如果不是listening 状态转过来的, 除错-EINVAL if (sk->sk_state != TCP_LISTEN) break; //检查interrupt错误 err = sock_intr_errno(timeo); //如果当前进程收到信号了,break if (signal_pending(current)) break; //如果传入的timeo为0,则回到nonblock的状态, break err = -EAGAIN; if (!timeo) break; } //ok, 有连接到达,设置state为running, 唤醒wait queue的第一个进程,移除wait_queue_t和wait_queue_head_t finish_wait(sk_sleep(sk), &wait); return err; }
本文接上一篇Linux内核协议栈-初始化流程分析,在上一篇中主要分析了了Linux内核协议栈涉及到的关键初始化函数,在这一篇文章中将分析协议栈的BSD socket和到传输层的流程。采取的方式是分析socket相关的主要系统调用。针对不同的系统调用,其到达的协议层深度可能不同,有的基本只到sock层就够了,但是有些可能需要会涉及到比如tcp的具体细节和更底层的细节。本文基本追溯到传输层的开始,再深入的细节后续文章分析。 1.准备 协议的基本分层: (A代表socket的某个系统调用) BSD socket system calls A => proto_ops->A => sock->A => tcp_prot => A BSD socket层和具体协议族某个类型的联系是通过struct proto_ops,在include/linux/net.h中定义了不同协议族如af_inet,af_unix等的通用操作函数指针的结构体struct proto_ops,具体的定义有各个协议族的某个类型的子模块自己完成。比如ipv4/af_inet.c中定义的af_inet family的tcp/udp等相应的struct proto_ops。 由于对于每个family的不同类型,其针对socket的某些需求可能不同,所以抽了一层struct sock出来,sock->sk_prot挂接到具体tcp/udp等传输层的struct proto上(具体定义在ipv4/tcp_ipv4.c,ipv4/udp.c) 另外,由于内容比较多,这一篇主要分析socket,bind,listen,accept几个系统调用,下一篇会涉及connect,send,recv等的分析 //不同协议族的通用函数hooks //比如af_inet相关的定义在ipv4/af_inet.c中 //除了创建socket为系统调用外,基本针对socket层的操作函数都在这里面 struct proto_ops { int family; struct module *owner; int (*release) (struct socket *sock); int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags); int (*socketpair)(struct socket *sock1, struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags); int (*getname) (struct socket *sock, struct sockaddr *addr, int *sockaddr_len, int peer); unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait); int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); #ifdef CONFIG_COMPAT int (*compat_ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); #endif int (*listen) (struct socket *sock, int len); int (*shutdown) (struct socket *sock, int flags); int (*setsockopt)(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen); /*省略部分*/ }; //传输层的proto //作为sock->sk_prot与具体传输层的hooks struct proto { void (*close)(struct sock *sk, long timeout); int (*connect)(struct sock *sk, struct sockaddr *uaddr, int addr_len); int (*disconnect)(struct sock *sk, int flags); struct sock * (*accept)(struct sock *sk, int flags, int *err); int (*ioctl)(struct sock *sk, int cmd, unsigned long arg); int (*init)(struct sock *sk); void (*destroy)(struct sock *sk); void (*shutdown)(struct sock *sk, int how); int (*setsockopt)(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen); int (*getsockopt)(struct sock *sk, int level, int optname, char __user *optval, int __user *option); #ifdef CONFIG_COMPAT int (*compat_setsockopt)(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen); int (*compat_getsockopt)(struct sock *sk, int level, int optname, char __user *optval, int __user *option); int (*compat_ioctl)(struct sock *sk, unsigned int cmd, unsigned long arg); #endif int (*sendmsg)(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len); int (*recvmsg)(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len, int noblock, int flags, int *addr_len); int (*sendpage)(struct sock *sk, struct page *page, int offset, size_t size, int flags); int (*bind)(struct sock *sk, struct sockaddr *uaddr, int addr_len); /*省略部分*/ }; 同时附上其他几个关键结构体: //bsd socket层 //include/linux/net.h struct socket { socket_state state; kmemcheck_bitfield_begin(type); short type; kmemcheck_bitfield_end(type); unsigned long flags; struct socket_wq __rcu *wq; struct file *file; struct sock *sk; const struct proto_ops *ops; }; //sock层 struct sock { sock_common __sk_common; #define sk_node __sk_common.skc_node #define sk_nulls_node __sk_common.skc_nulls_node #define sk_refcnt __sk_common.skc_refcnt #define sk_tx_queue_mapping __sk_common.skc_tx_queue_mapping #define sk_dontcopy_begin __sk_common.skc_dontcopy_begin #define sk_dontcopy_end __sk_common.skc_dontcopy_end #define sk_hash __sk_common.skc_hash #define sk_portpair __sk_common.skc_portpair #define sk_num __sk_common.skc_num #define sk_dport __sk_common.skc_dport #define sk_addrpair __sk_common.skc_addrpair #define sk_daddr __sk_common.skc_daddr #define sk_rcv_saddr __sk_common.skc_rcv_saddr #define sk_family __sk_common.skc_family #define sk_state __sk_common.skc_state #define sk_reuse __sk_common.skc_reuse #define sk_reuseport __sk_common.skc_reuseport #define sk_ipv6only __sk_common.skc_ipv6only #define sk_bound_dev_if __sk_common.skc_bound_dev_if #define sk_bind_node __sk_common.skc_bind_node #define sk_prot __sk_common.skc_prot #define sk_net __sk_common.skc_net #define sk_v6_daddr __sk_common.skc_v6_daddr #define sk_v6_rcv_saddr __sk_common.skc_v6_rcv_saddr unsigned long sk_flags; struct dst_entry *sk_rx_dst; struct dst_entry __rcu *sk_dst_cache; spinlock_t sk_dst_lock; atomic_t sk_wmem_alloc; atomic_t sk_omem_alloc; int sk_sndbuf; struct sk_buff_head sk_write_queue; /*省略部分*/ struct pid *sk_peer_pid; const struct cred *sk_peer_cred; long sk_rcvtimeo; long sk_sndtimeo; void *sk_protinfo; struct timer_list sk_timer; ktime_t sk_stamp; u16 sk_tsflags; u32 sk_tskey; struct socket *sk_socket; void *sk_user_data; struct page_frag sk_frag; struct sk_buff *sk_send_head; /*省略部分*/ }; 2.开始 主要追溯几个典型的socket相关的系统调用,如socket,bind,listen,accept等等 socket //创建socket的系统调用 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { int retval; struct socket *sock; int flags; /* Check the SOCK_* constants for consistency. */ BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC); BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK); flags = type & ~SOCK_TYPE_MASK; if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) return -EINVAL; type &= SOCK_TYPE_MASK; if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; //分配inode,返回inode中的一个成员作为sock retval = sock_create(family, type, protocol, &sock); if (retval < 0) goto out; //找个fd映射sock //得到空fd //分配伪dentry和file,并将socket file的operations与file挂接 retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); /*省略部分*/ } socketpair //创建socketpair,注意af_inet协议族下没有pair,af_unix下有 SYSCALL_DEFINE4(socketpair, int, family, int, type, int, protocol, int __user *, usockvec) { struct socket *sock1, *sock2; int fd1, fd2, err; struct file *newfile1, *newfile2; int flags; flags = type & ~SOCK_TYPE_MASK; if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) return -EINVAL; type &= SOCK_TYPE_MASK; if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; //创建socket1 err = sock_create(family, type, protocol, &sock1); if (err < 0) goto out; //创建socket2 err = sock_create(family, type, protocol, &sock2); if (err < 0) goto out_release_1; //调用socket operations的socketpair //关于不同协议层的函数hook,公共结构体是struct proto_ops //对于不同的family,比如af_inet协议族的定义在ipv4/af_inet.c // //对于af_inet没有socketpair //对于af_unix有socketpair err = sock1->ops->socketpair(sock1, sock2); if (err < 0) goto out_release_both; //后面部分就很类似了,找到空fd,分配file,绑定到socket,将file 安装到当前进程 fd1 = get_unused_fd_flags(flags); if (unlikely(fd1 < 0)) { err = fd1; goto out_release_both; } fd2 = get_unused_fd_flags(flags); if (unlikely(fd2 < 0)) { err = fd2; goto out_put_unused_1; } newfile1 = sock_alloc_file(sock1, flags, NULL); if (unlikely(IS_ERR(newfile1))) { err = PTR_ERR(newfile1); goto out_put_unused_both; } newfile2 = sock_alloc_file(sock2, flags, NULL); if (IS_ERR(newfile2)) { err = PTR_ERR(newfile2); goto out_fput_1; } err = put_user(fd1, &usockvec[0]); if (err) goto out_fput_both; err = put_user(fd2, &usockvec[1]); if (err) goto out_fput_both; audit_fd_pair(fd1, fd2); fd_install(fd1, newfile1); fd_install(fd2, newfile2); /* fd1 and fd2 may be already another descriptors. * Not kernel problem. */ return 0; bind SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen) { struct socket *sock; struct sockaddr_storage address; int err, fput_needed; //根据fd查找file,进而查找socket指针sock sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { //把用户态地址数据移到内核态 //调用copy_from_user err = move_addr_to_kernel(umyaddr, addrlen, &address); if (err >= 0) { //security hook err = security_socket_bind(sock, (struct sockaddr *)&address, addrlen); if (!err) //ok, 到具体family定义的proto_ops中的bind //比如对af_inet,主要是设置socket->sock->inet_sock的一些参数,比如接收地址,端口什么的 err = sock->ops->bind(sock, (struct sockaddr *) &address, addrlen); } fput_light(sock->file, fput_needed); } return err; } listen listen所做的事情也比较简单,从系统调用的listen(fd, backlog)到proto_ops 的inet_listen与前面类似,这里分析下inet_listen中的核心函数inet_csk_listen_start(位于ipv4/inet_connection_sock.c中)。 int inet_csk_listen_start(struct sock *sk, const int nr_table_entries) { //获得网络层inte_sock struct inet_sock *inet = inet_sk(sk); //管理request connection的结构体 struct inet_connection_sock *icsk = inet_csk(sk); //分配backlog个长度的accpet_queue的结构连接请求的队列 int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries); if (rc != 0) return rc; sk->sk_max_ack_backlog = 0; sk->sk_ack_backlog = 0; inet_csk_delack_init(sk); /* There is race window here: we announce ourselves listening, * but this transition is still not validated by get_port(). * It is OK, because this socket enters to hash table only * after validation is complete. */ //切换状态到listening sk->sk_state = TCP_LISTEN; if (!sk->sk_prot->get_port(sk, inet->inet_num)) { inet->inet_sport = htons(inet->inet_num); //更新dst_entry表 sk_dst_reset(sk); sk->sk_prot->hash(sk); return 0; } sk->sk_state = TCP_CLOSE; __reqsk_queue_destroy(&icsk->icsk_accept_queue); return -EADDRINUSE; } accept 上面socket, socketpair, bind基本只涉及到BSD socket, sock层相关的,过程比较简单,而accept层在sock层和tcp层交互稍微复杂,下面详细分析 //socket.c //accept系统调用 SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr, int __user *, upeer_addrlen, int, flags) { /*省略部分*/ err = -ENFILE; //for client socket newsock = sock_alloc(); if (!newsock) goto out_put; newsock->type = sock->type; newsock->ops = sock->ops; /* * We don't need try_module_get here, as the listening socket (sock) * has the protocol module (sock->ops->owner) held. */ __module_get(newsock->ops->owner); //得到当前进程空fd,分给newsock file newfd = get_unused_fd_flags(flags); if (unlikely(newfd < 0)) { err = newfd; sock_release(newsock); goto out_put; } //从flab分配空file结构 newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name); if (unlikely(IS_ERR(newfile))) { err = PTR_ERR(newfile); put_unused_fd(newfd); sock_release(newsock); goto out_put; } err = security_socket_accept(sock, newsock); if (err) goto out_fd; //proto_ops中的accept //accept从系统调用到具体协议族的某个type的struct proto_ops的accept如af_inet tcp的的accept,再到sock层的accept,然后sock层的accept实际上对应的是具体传输层的struct proto中的accpet,如tcp/udp的struct proto tcp_prot/udp_prot,然后放入newsock err = sock->ops->accept(sock, newsock, sock->file->f_flags); if (err < 0) goto out_fd; if (upeer_sockaddr) { if (newsock->ops->getname(newsock, (struct sockaddr *)&address, &len, 2) < 0) { err = -ECONNABORTED; goto out_fd; } //拷贝client socket addr storage到userspace err = move_addr_to_user(&address, len, upeer_sockaddr, upeer_addrlen); if (err < 0) goto out_fd; } fd_install(newfd, newfile); err = newfd; /*省略部分*/ } //ipv4/af_inet.c //inet family的tcp相关的proto_ops int inet_accept(struct socket *sock, struct socket *newsock, int flags) { struct sock *sk1 = sock->sk; int err = -EINVAL; //进入(网络)sock层,accept新sock struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err); if (!sk2) goto do_err; //锁住sock,因为需要操作sock内的request_socket请求队列头 wait_queue_head_t等数据 lock_sock(sk2); sock_rps_record_flow(sk2); WARN_ON(!((1 << sk2->sk_state) & (TCPF_ESTABLISHED | TCPF_SYN_RECV | TCPF_CLOSE_WAIT | TCPF_CLOSE))); sock_graft(sk2, newsock); //设置client socket状态 newsock->state = SS_CONNECTED; err = 0; release_sock(sk2); do_err: return err; } //ipv4/tcp_ipv4.c //这里进入struct proto tcp_prot中的accept struct sock *inet_csk_accept(struct sock *sk, int flags, int *err) { struct inet_connection_sock *icsk = inet_csk(sk); //icsk : inet_connection_sock 面向连接的客户端连接处理相关的信息 //接收队列 struct request_sock_queue *queue = &icsk->icsk_accept_queue; struct sock *newsk; struct request_sock *req; int error; //lock sock lock_sock(sk); //如果不是ACCPET状态转换过来,出错 error = -EINVAL; if (sk->sk_state != TCP_LISTEN) goto out_err; //如果request_sock队列是空的, 利用等待队列挂起当前进程到等待队列,并且将等待队列放入sock中的请求队列头 if (reqsk_queue_empty(queue)) { //如果非阻塞,0,否则为sk的接收时间 long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); error = -EAGAIN; if (!timeo) //如果非阻塞而且接收队列是空,直接返回-EAGAIN goto out_err; //阻塞情况下,等待timeo时间的超时 //利用了等待队列,下面会详细注解 error = inet_csk_wait_for_connect(sk, timeo); if (error) goto out_err; } //不是空,移出一个连接请求 req = reqsk_queue_remove(queue); //连接请求的sock newsk = req->sk; //减少backlog sk_acceptq_removed(sk); //fastopenq? if (sk->sk_protocol == IPPROTO_TCP && queue->fastopenq != NULL) { spin_lock_bh(&queue->fastopenq->lock); if (tcp_rsk(req)->listener) { /* We are still waiting for the final ACK from 3WHS * so can't free req now. Instead, we set req->sk to * NULL to signify that the child socket is taken * so reqsk_fastopen_remove() will free the req * when 3WHS finishes (or is aborted). */ req->sk = NULL; req = NULL; } spin_unlock_bh(&queue->fastopenq->lock); } //ok,清理,返回newsk /*省略部分*/ //ipv4/inet_connection_sock.c //accept连接请求的核心函数 static int inet_csk_wait_for_connect(struct sock *sk, long timeo) { struct inet_connection_sock *icsk = inet_csk(sk); //定义一个等待队列wait_queue_t wait 进程是当前进程 DEFINE_WAIT(wait); int err; for (;;) { //sk_leep(sk) : sock的wait_queue_head_t //wait : wait_queue_t //这里将current进程的wait_queue_t加入sk的wait_queue_head_t中,spin锁定 //wait_queue_head_t,设置current状态,然后spin解锁时可能重新schedule prepare_to_wait_exclusive(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); //被唤醒,解锁sock release_sock(sk); //如果请求队列为空,说明timeout了 if (reqsk_queue_empty(&icsk->icsk_accept_queue)) //schedule timeout timeo = schedule_timeout(timeo); //再锁住进行下次循环,准备再次进入TASK_INTERRUPTIBLE lock_sock(sk); err = 0; //检查是否有连接到达, 如果有,break,唤醒等待队列 if (!reqsk_queue_empty(&icsk->icsk_accept_queue)) break; err = -EINVAL; //如果不是listening 状态转过来的, 除错-EINVAL if (sk->sk_state != TCP_LISTEN) break; //检查interrupt错误 err = sock_intr_errno(timeo); //如果当前进程收到信号了,break if (signal_pending(current)) break; //如果传入的timeo为0,则回到nonblock的状态, break err = -EAGAIN; if (!timeo) break; } //ok, 有连接到达,设置state为running, 唤醒wait queue的第一个进程,移除wait_queue_t和wait_queue_head_t finish_wait(sk_sleep(sk), &wait); return err; }
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/49509993 本文主要针对Linux-3.19.3版本的内核简单分析内核协议栈初始化涉及到的主要步骤和关键函数,不针对协议的解析以及数据包的处理流程做具体分析,后续有机会再详细分析 1.准备 Linux内核协议栈本身构建在虚拟文件系统之上,所以对Linux VFS不太了解的可以参考内核源码根目录下Documentation/filesystems/vfs.txt,另外,socket接口层,协议层,设备层的许多数据结构涉及到内存管理,所以对基本虚拟内存管理,slab缓存,页高速缓存不太了解的也可以查阅相关文档。 源码涉及的主要文件位于net/socket.c,net/core,include/linux/net*. 基本上整个初始化过程主要与net,net_namespace,/proc,/proc/sys相关结构的初始化和文件的建立,主要使用register_pernet_subsystem钩子注册和调用各种操作.init和.exit 2.开始 开始分析前,这里有些小技巧可以快速定位到主要的初始化函数,在分析其他子系统源码时也可以采用这个技巧 grep _initcall socket.c find ./core/ -name "*.c" |xargs cat | grep _initcall grep net_inuse_init tags 这里*__initcall宏是设置初始化函数位于内核代码段.initcall#id.init的位置其中id代表优先级level,小的一般初始化靠前,定义在include/linux/init.h,使用gcc的attribute扩展。而各个level的初始化函数的调用流程基本如下: start_kernel -> rest_init -> kernel_init内核线程 -> kernel_init_freeable -> do_basic_setup -> do_initcalls -> do_initcall_level -> do_one_initcall -> *(initcall_t) 3.详细分析 可以看到pure_initcall(net_ns_init)位于0的初始化level,基本不依赖其他的初始化子系统,所以从这个开始 //core/net_namespace.c //基本上这个函数主要的作用是初始化net结构init_net的一些数据,比如namespace相关,并且调用注册的pernet operations的init钩子针对net进行各自需求的初始化 pure_initcall(net_ns_init); static int __init net_ns_init(void) { struct net_generic *ng; //net namespace相关 #ifdef CONFIG_NET_NS //分配slab缓存 net_cachep = kmem_cache_create("net_namespace", sizeof(struct net),SMP_CACHE_BYTES,SLAB_PANIC, NULL); /* Create workqueue for cleanup */ netns_wq = create_singlethread_workqueue("netns"); if (!netns_wq) panic("Could not create netns workq"); #endif ng = net_alloc_generic(); if (!ng) panic("Could not allocate generic netns"); rcu_assign_pointer(init_net.gen, ng); mutex_lock(&net_mutex); //初始化net namespace相关的对象, 传入初始的namespace init_user_ns //设置net结构的初始namespace //对每个pernet_list中注册的pernet operation,调用其初始化net中的对应数据对象 if (setup_net(&init_net, &init_user_ns)) panic("Could not setup the initial network namespace"); rtnl_lock(); //加入初始net结构的list中 list_add_tail_rcu(&init_net.list, &net_namespace_list); rtnl_unlock(); mutex_unlock(&net_mutex); //加入pernet_list链表,并且调用pernet operation的init函数初始化net register_pernet_subsys(&net_ns_ops); return 0; } 下面分析core_init(sock_init): //socket.c //在.initcall1.init代码段注册,以便内核启动时do_initcalls中调用 //从而注册socket filesystem core_initcall(sock_init); /* early initcall */ 进入core_init(sock_init): static int __init sock_init(void) { int err; //sysctl 支持 err = net_sysctl_init(); if (err) goto out; //初始化skbuff_head_cache 和 skbuff_clone_cache的slab缓存区 skb_init(); //与vfs挂接,为sock inode分配slab缓存 init_inodecache(); //注册socket 文件系统 err = register_filesystem(&sock_fs_type); if (err) goto out_fs; //通过kern_mount内核层接口调用mount系统调用,最终调用 //fs_type->mount 而socket filesystem 使用mount_pesudo伪挂载 sock_mnt = kern_mount(&sock_fs_type); if (IS_ERR(sock_mnt)) { err = PTR_ERR(sock_mnt); goto out_mount; } //协议与设备相关的数据结构等初始化在后续的各子模块subsys_init操作中 /* The real protocol initialization is performed in later initcalls. */ //netfilter初始化 #ifdef CONFIG_NETFILTER err = netfilter_init(); if (err) goto out; #endif /*省略部分*/ } core_init(net_inuse_init) //core/sock.c //主要功能是为net分配inuse的percpu标识 core_initcall(net_inuse_init); static int __net_init sock_inuse_init_net(struct net *net) { net->core.inuse = alloc_percpu(struct prot_inuse); return net->core.inuse ? 0 : -ENOMEM; } static void __net_exit sock_inuse_exit_net(struct net *net) { free_percpu(net->core.inuse); } static struct pernet_operations net_inuse_ops = { .init = sock_inuse_init_net, .exit = sock_inuse_exit_net, }; static __init int net_inuse_init(void) { if (register_pernet_subsys(&net_inuse_ops)) panic("Cannot initialize net inuse counters"); return 0; } core_init(netpoll_init) //core/netpoll.c //主要功能就是把预留的sk_buffer poll初始化成队列 core_initcall(netpoll_init); static int __init netpoll_init(void) { skb_queue_head_init(&skb_pool); return 0; } subsys_initcall(proto_init) //core/sock.c //涉及的操作主要是在/proc/net域下建立protocols文件,注册相关文件操作函数 subsys_initcall(proto_init); // /proc/net/protocols支持的文件操作 static const struct file_operations proto_seq_fops = { .owner = THIS_MODULE, .open = proto_seq_open, //打开 .read = seq_read, //读 .llseek = seq_lseek,//seek .release = seq_release_net, }; static __net_init int proto_init_net(struct net *net) { //创建/proc/net/protocols if (!proc_create("protocols", S_IRUGO, net->proc_net, &proto_seq_fops)) return -ENOMEM; return 0; } static __net_exit void proto_exit_net(struct net *net) { remove_proc_entry("protocols", net->proc_net); } static __net_initdata struct pernet_operations proto_net_ops = { .init = proto_init_net, .exit = proto_exit_net, }; //注册 pernet_operations, 并用.init钩子初始化net,此处即创建proc相关文件 static int __init proto_init(void) { return register_pernet_subsys(&proto_net_ops); } subsys_initcall(net_dev_init) //core/dev.c //基本上是建立net device在/proc,/sys相关的数据结构,并且开启网卡收发中断 //初始化net device static int __init net_dev_init(void) { int i, rc = -ENOMEM; BUG_ON(!dev_boot_phase); //主要也是在/proc/net/下建立相应的属性文件,如dev网卡信息文件 if (dev_proc_init()) goto out; //注册/sys文件系统,添加相关属性项 //注册网络内核对象namespace相关的一些操作 //注册net interface(dev)到 /sys/class/net if (netdev_kobject_init()) goto out; INIT_LIST_HEAD(&ptype_all); for (i = 0; i < PTYPE_HASH_SIZE; i++) INIT_LIST_HEAD(&ptype_base[i]); INIT_LIST_HEAD(&offload_base); //注册并调用针对每个net的设备初始化操作 if (register_pernet_subsys(&netdev_net_ops)) goto out; //对每个cpu,初始化数据包处理相关队列 for_each_possible_cpu(i) { struct softnet_data *sd = &per_cpu(softnet_data, i); //入 skb_queue_head_init(&sd->input_pkt_queue); skb_queue_head_init(&sd->process_queue); INIT_LIST_HEAD(&sd->poll_list); //出 sd->output_queue_tailp = &sd->output_queue; #ifdef CONFIG_RPS sd->csd.func = rps_trigger_softirq; sd->csd.info = sd; sd->cpu = i; #endif sd->backlog.poll = process_backlog; sd->backlog.weight = weight_p; } //只在boot phase调用一次, 防止重复调用 dev_boot_phase = 0; /* The loopback device is special if any other network devices * is present in a network namespace the loopback device must * be present. Since we now dynamically allocate and free the * loopback device ensure this invariant is maintained by * keeping the loopback device as the first device on the * list of network devices. Ensuring the loopback devices * is the first device that appears and the last network device * that disappears. */ //回环设备的建立与初始化 if (register_pernet_device(&loopback_net_ops)) goto out; //退出的通用操作 if (register_pernet_device(&default_device_ops)) goto out; //开启收发队列的中断 open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action); hotcpu_notifier(dev_cpu_callback, 0); //destination cache related? dst_init(); rc = 0; out: return rc; } fs_initcall(sysctl_core_init) //core/sysctl_net_core.c //主要是建立sysctl中与net相关的一些配置参数(见下图) static __init int sysctl_core_init(void) { register_net_sysctl(&init_net, "net/core", net_core_table); return register_pernet_subsys(&sysctl_core_ops); } static __net_init int sysctl_core_net_init(struct net *net) { struct ctl_table *tbl; net->core.sysctl_somaxconn = SOMAXCONN; tbl = netns_core_table; if (!net_eq(net, &init_net)) { tbl = kmemdup(tbl, sizeof(netns_core_table), GFP_KERNEL); if (tbl == NULL) goto err_dup; tbl[0].data = &net->core.sysctl_somaxconn; if (net->user_ns != &init_user_ns) { tbl[0].procname = NULL; } } net->core.sysctl_hdr = register_net_sysctl(net, "net/core", tbl); if (net->core.sysctl_hdr == NULL) goto err_reg; return 0; err_reg: if (tbl != netns_core_table) kfree(tbl); err_dup: return -ENOMEM; } static __net_exit void sysctl_core_net_exit(struct net *net) { struct ctl_table *tbl; tbl = net->core.sysctl_hdr->ctl_table_arg; unregister_net_sysctl_table(net->core.sysctl_hdr); BUG_ON(tbl == netns_core_table); kfree(tbl); } static __net_initdata struct pernet_operations sysctl_core_ops = { .init = sysctl_core_net_init, .exit = sysctl_core_net_exit, }; 4.总结 本文主要按照关于内核协议栈的各个子系统的*_initcall的调用顺序分析了几个核心的初始化步骤,包括socket层,协议层,设备层等,整个初始化过程还是比较简单的,主要涉及一些数据结构和缓存等的初始化,但是整个内核协议栈的对数据包的处理流程并不能很好地呈现,后续有机会再分析从系统调用开始整个数据包的收发流程。 ref: Linux 3.19.3 source tree
本文主要针对Linux-3.19.3版本的内核简单分析内核协议栈初始化涉及到的主要步骤和关键函数,不针对协议的解析以及数据包的处理流程做具体分析,后续有机会再详细分析 1.准备 Linux内核协议栈本身构建在虚拟文件系统之上,所以对Linux VFS不太了解的可以参考内核源码根目录下Documentation/filesystems/vfs.txt,另外,socket接口层,协议层,设备层的许多数据结构涉及到内存管理,所以对基本虚拟内存管理,slab缓存,页高速缓存不太了解的也可以查阅相关文档。 源码涉及的主要文件位于net/socket.c,net/core,include/linux/net* 2.开始 开始分析前,这里有些小技巧可以快速定位到主要的初始化函数,在分析其他子系统源码时也可以采用这个技巧 grep _initcall socket.c find ./core/ -name "*.c" |xargs cat | grep _initcall grep net_inuse_init tags 这里*__initcall宏是设置初始化函数位于内核代码段.initcall#id.init的位置其中id代表优先级level,小的一般初始化靠前,定义在include/linux/init.h,使用gcc的attribute扩展。而各个level的初始化函数的调用流程基本如下: start_kernel -> rest_init -> kernel_init内核线程 -> kernel_init_freeable -> do_basic_setup -> do_initcalls -> do_initcall_level -> do_one_initcall -> *(initcall_t) 3.详细分析 可以看到pure_initcall(net_ns_init)位于0的初始化level,基本不依赖其他的初始化子系统,所以从这个开始 //core/net_namespace.c //基本上这个函数主要的作用是初始化net结构init_net的一些数据,比如namespace相关,并且调用注册的pernet operations的init钩子针对net进行各自需求的初始化 pure_initcall(net_ns_init); static int __init net_ns_init(void) { struct net_generic *ng; //net namespace相关 #ifdef CONFIG_NET_NS //分配slab缓存 net_cachep = kmem_cache_create("net_namespace", sizeof(struct net),SMP_CACHE_BYTES,SLAB_PANIC, NULL); /* Create workqueue for cleanup */ netns_wq = create_singlethread_workqueue("netns"); if (!netns_wq) panic("Could not create netns workq"); #endif ng = net_alloc_generic(); if (!ng) panic("Could not allocate generic netns"); rcu_assign_pointer(init_net.gen, ng); mutex_lock(&net_mutex); //初始化net namespace相关的对象, 传入初始的namespace init_user_ns //设置net结构的初始namespace //对每个pernet_list中注册的pernet operation,调用其初始化net中的对应数据对象 if (setup_net(&init_net, &init_user_ns)) panic("Could not setup the initial network namespace"); rtnl_lock(); //加入初始net结构的list中 list_add_tail_rcu(&init_net.list, &net_namespace_list); rtnl_unlock(); mutex_unlock(&net_mutex); //加入pernet_list链表,并且调用pernet operation的init函数初始化net register_pernet_subsys(&net_ns_ops); return 0; } 下面分析core_init(sock_init): //socket.c //在.initcall1.init代码段注册,以便内核启动时do_initcalls中调用 //从而注册socket filesystem core_initcall(sock_init); /* early initcall */ 进入core_init(sock_init): static int __init sock_init(void) { int err; //sysctl 支持 err = net_sysctl_init(); if (err) goto out; //初始化skbuff_head_cache 和 skbuff_clone_cache的slab缓存区 skb_init(); //与vfs挂接,为sock inode分配slab缓存 init_inodecache(); //注册socket 文件系统 err = register_filesystem(&sock_fs_type); if (err) goto out_fs; //通过kern_mount内核层接口调用mount系统调用,最终调用 //fs_type->mount 而socket filesystem 使用mount_pesudo伪挂载 sock_mnt = kern_mount(&sock_fs_type); if (IS_ERR(sock_mnt)) { err = PTR_ERR(sock_mnt); goto out_mount; } //协议与设备相关的数据结构等初始化在后续的各子模块subsys_init操作中 /* The real protocol initialization is performed in later initcalls. */ //netfilter初始化 #ifdef CONFIG_NETFILTER err = netfilter_init(); if (err) goto out; #endif /*省略部分*/ } core_init(net_inuse_init) //core/sock.c //主要功能是为net分配inuse的percpu标识 core_initcall(net_inuse_init); static int __net_init sock_inuse_init_net(struct net *net) { net->core.inuse = alloc_percpu(struct prot_inuse); return net->core.inuse ? 0 : -ENOMEM; } static void __net_exit sock_inuse_exit_net(struct net *net) { free_percpu(net->core.inuse); } static struct pernet_operations net_inuse_ops = { .init = sock_inuse_init_net, .exit = sock_inuse_exit_net, }; static __init int net_inuse_init(void) { if (register_pernet_subsys(&net_inuse_ops)) panic("Cannot initialize net inuse counters"); return 0; } core_init(netpoll_init) //core/netpoll.c //主要功能就是把预留的sk_buffer poll初始化成队列 core_initcall(netpoll_init); static int __init netpoll_init(void) { skb_queue_head_init(&skb_pool); return 0; } subsys_initcall(proto_init) //core/sock.c //涉及的操作主要是在/proc/net域下建立protocols文件,注册相关文件操作函数 subsys_initcall(proto_init); // /proc/net/protocols支持的文件操作 static const struct file_operations proto_seq_fops = { .owner = THIS_MODULE, .open = proto_seq_open, //打开 .read = seq_read, //读 .llseek = seq_lseek,//seek .release = seq_release_net, }; static __net_init int proto_init_net(struct net *net) { //创建/proc/net/protocols if (!proc_create("protocols", S_IRUGO, net->proc_net, &proto_seq_fops)) return -ENOMEM; return 0; } static __net_exit void proto_exit_net(struct net *net) { remove_proc_entry("protocols", net->proc_net); } static __net_initdata struct pernet_operations proto_net_ops = { .init = proto_init_net, .exit = proto_exit_net, }; //注册 pernet_operations, 并用.init钩子初始化net,此处即创建proc相关文件 static int __init proto_init(void) { return register_pernet_subsys(&proto_net_ops); } subsys_initcall(net_dev_init) //core/dev.c //基本上是建立net device在/proc,/sys相关的数据结构,并且开启网卡收发中断 //初始化net device static int __init net_dev_init(void) { int i, rc = -ENOMEM; BUG_ON(!dev_boot_phase); //主要也是在/proc/net/下建立相应的属性文件,如dev网卡信息文件 if (dev_proc_init()) goto out; //注册/sys文件系统,添加相关属性项 //注册网络内核对象namespace相关的一些操作 //注册net interface(dev)到 /sys/class/net if (netdev_kobject_init()) goto out; INIT_LIST_HEAD(&ptype_all); for (i = 0; i < PTYPE_HASH_SIZE; i++) INIT_LIST_HEAD(&ptype_base[i]); INIT_LIST_HEAD(&offload_base); //注册并调用针对每个net的设备初始化操作 if (register_pernet_subsys(&netdev_net_ops)) goto out; //对每个cpu,初始化数据包处理相关队列 for_each_possible_cpu(i) { struct softnet_data *sd = &per_cpu(softnet_data, i); //入 skb_queue_head_init(&sd->input_pkt_queue); skb_queue_head_init(&sd->process_queue); INIT_LIST_HEAD(&sd->poll_list); //出 sd->output_queue_tailp = &sd->output_queue; #ifdef CONFIG_RPS sd->csd.func = rps_trigger_softirq; sd->csd.info = sd; sd->cpu = i; #endif sd->backlog.poll = process_backlog; sd->backlog.weight = weight_p; } //只在boot phase调用一次, 防止重复调用 dev_boot_phase = 0; /* The loopback device is special if any other network devices * is present in a network namespace the loopback device must * be present. Since we now dynamically allocate and free the * loopback device ensure this invariant is maintained by * keeping the loopback device as the first device on the * list of network devices. Ensuring the loopback devices * is the first device that appears and the last network device * that disappears. */ //回环设备的建立与初始化 if (register_pernet_device(&loopback_net_ops)) goto out; //退出的通用操作 if (register_pernet_device(&default_device_ops)) goto out; //开启收发队列的中断 open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action); hotcpu_notifier(dev_cpu_callback, 0); //destination cache related? dst_init(); rc = 0; out: return rc; } fs_initcall(sysctl_core_init) //core/sysctl_net_core.c //主要是建立sysctl中与net相关的一些配置参数(见下图) static __init int sysctl_core_init(void) { register_net_sysctl(&init_net, "net/core", net_core_table); return register_pernet_subsys(&sysctl_core_ops); } static __net_init int sysctl_core_net_init(struct net *net) { struct ctl_table *tbl; net->core.sysctl_somaxconn = SOMAXCONN; tbl = netns_core_table; if (!net_eq(net, &init_net)) { tbl = kmemdup(tbl, sizeof(netns_core_table), GFP_KERNEL); if (tbl == NULL) goto err_dup; tbl[0].data = &net->core.sysctl_somaxconn; if (net->user_ns != &init_user_ns) { tbl[0].procname = NULL; } } net->core.sysctl_hdr = register_net_sysctl(net, "net/core", tbl); if (net->core.sysctl_hdr == NULL) goto err_reg; return 0; err_reg: if (tbl != netns_core_table) kfree(tbl); err_dup: return -ENOMEM; } static __net_exit void sysctl_core_net_exit(struct net *net) { struct ctl_table *tbl; tbl = net->core.sysctl_hdr->ctl_table_arg; unregister_net_sysctl_table(net->core.sysctl_hdr); BUG_ON(tbl == netns_core_table); kfree(tbl); } static __net_initdata struct pernet_operations sysctl_core_ops = { .init = sysctl_core_net_init, .exit = sysctl_core_net_exit, }; 4.总结 本文主要按照关于内核协议栈的各个子系统的*_initcall的调用顺序分析了几个核心的初始化步骤,包括socket层,协议层,设备层等,整个初始化过程还是比较简单的,主要涉及一些数据结构和缓存等的初始化,但是整个内核协议栈的对数据包的处理流程并不能很好地呈现,后续有机会再分析从系统调用开始整个数据包的收发流程。 ref: Linux 3.19.3 source tree
Lambda Calculus是非经典逻辑中的一种,形式比图灵机模型和一阶谓词逻辑等简洁优雅许多,是函数式编程语言的理论支柱,本文主要简单梳理了untyped Lambda Calculus以及Church数的构造。 Functional Programming Languages Properties based-on lambda calculus closure(functor) and high-order function lazy evaluation recursion reference transparently no side-effects expression Lambda Calculus Four core components expression variable(value) function application Grammar (expression) := (variable) | (function) | (application) (function) := lambda (variable).(expression) (application) := (expression)(expression) examples function definition : lambda x.x ==> Identity function I function application : (lambda x.x)(y) = y free and bound variables lambda x.xy ==> x bound but y free substitution and reduction alpha substitution beta reduction numbers definition(Church numbers) S : lambda wyx.y(wyx) (Successor function) 0 : lambda sz.z 1 : lambda sz.s(z) S(0) = (lambda wyx.y(wyx))(lambda sz.z) = lambda yx.y((lambda sz.z)(y)x) = lambda yx.y(x) = 1 2 : lambda sz.s(s(z)) S(1) = (lambda wyx.y(wyx))(lambda sz.s(z)) = lambda yx.y((lambda sz.s(z))yx) = lambda yx.y((lambda z.y(z))x) = lambda yx.y(y(x)) = 2 3 : lambda sz.s(s(s(z))) 3(Func)(var) ==> apply 3 Func times on var addition ’+’ : lambda wyx.y(wyx) (successor function) 1 + 2 = 1S(2) (lambda sz.s(z)) (lambda wyx.y(wyx)) (lambda ab.a(a(b))) = (lambda z.(lambda wyx.y(wyx))(z)) (2) = (lambda zyx.y(zyx))(2) = S(2) 2 + 2 = 2S(2) (lambda sz.s(s(z))) (lambda wyx.y(wyx)) (2) = (lambda z.S(S(z)))(2) = S(S(2)) multiplication ’*’ : lambda xyz.x(yz) 1*2 = (lambda abc.a(bc))(1,2) = (lambda bc.1(bc))(2) = (lambda c.1(2(c))) = (lambda c.(lambda sz.s(z))(lambda sz.s(s(z)))(c)) = lambda c.(lambda cz.c(c(z))) = 2 Condition T : lambda xy.x F : lambda xy.y logic operation && : lambda xy.xyF &&(T,T) = (lambda x1y1.x1)(lambda x2y2.x2)(lambda xy.y) = lambda x2y2.x2 = T &&(F,Any) = (lambda x1y1.y1)(Any)(lambda xy.y) = lambda xy.y = F | : lambda xy.xTy |(F,F) = (lambda x1y1.y1)(lambda xy.x)(lambda x2y2.y2) = lambda x2y2.y2 = F |(T,Any) = (lambda x1y1.x1)(lambda xy.x)(Any) = lambda xy.x = T ~ : lambda x.xFT ~(F) = (lambda xy.y)(lambda x1y1.y1)(lambda x2y2.x2) = lambda x2y2.x2 = T ~(T) = (lambda xy.x)(lambda x1y1.y1)(lambda x2y2.x2) = lambda x1y1.y1 = F conditional test Z : lambda x.xF~F ==> T if x==0 else F Z(0) = 0F(~F) = (lambda sz.z)F~F = ~F = T Z(1) = (lambda sz.s(z))F~F = F(~)F = (lambda xy.y)(~)(F) = IF = F predecessor p : lambda zxy.xy ==> a pair (x,y) Inc : lambda pz.z(S(pT))(pT) ==> increase each element of one pair (x,x-1) -> (x+1,x) P : (lambda n.n(In(lambda z.z00)))F nP(0) = 0 equality and inequality >= : lambda xy.Z(xPy) [if x>=y return True else False] <= : lambda xy.Z(yPx) [if x<=y return True else False] = : lambda xy.^(Z(xPy))(Z(yPx)) recursion Y combinator : Y = lambda f.(lambda x.f(xx))(lambda x.f(xx)) = f((lambda x.f(xx))(lambda x.f(xx))) Yf = f(Yf) [Yf ==> recursion of f] example 1+2+3…+n : f = lambda rn.(Zn)(0)(nS(r(Pn))) Yf = f(Yf) = lambda Yfn.(Zn)(0)(nS(Yf(Pn))) ==> Yf recursion ref:《A Tutorial Introduction to the Lambda Calculus》
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/49280283 Linux内核的VFS是非常经典的抽象,不仅抽象出了flesystem,super_block,inode,dentry,file等结构,而且还提供了像页高速缓存层的通用接口,当然,你可以自己选择是否使用或者自己定制使用方式。本文主要根据自己阅读Linux Kernel 3.19.3系统调用read相关的源码来追踪页高速缓存在整个流程中的痕迹,以常规文件的页高速缓存为例,了解页高速缓存的实现过程,不过于追究具体bio请求的底层细节。另外,在写操作的过程中,页高速缓存的处理流程有所不同(回写),涉及的东西更多,本文主要关注读操作。Linux VFS相关的重要数据结构及概念可以参考Document目录下的vfs.txt。 1.与页高速缓存相关的重要数据结构 除了前述基本数据结构以外,struct address_space 和 struct address_space_operations也在页高速缓存中起着极其重要的作用。 address_space结构通常被struct page的一个字段指向,主要存放已缓存页面的相关信息,便于快速查找对应文件的缓存页面,具体查找过程是通过radix tree结构的相关操作实现的。 address_space_operations结构定义了具体读写页面等操作的钩子,比如生成并发送bio请求,我们可以定制相应的函数实现自己的读写逻辑。 //include/linux/fs.h struct address_space { //指向文件的inode,可能为NULL struct inode *host; //存放装有缓存数据的页面 struct radix_tree_root page_tree; spinlock_t tree_lock; atomic_t i_mmap_writable; struct rb_root i_mmap; struct list_head i_mmap_nonlinear; struct rw_semaphore i_mmap_rwsem; //已缓存页的数量 unsigned long nrpages; unsigned long nrshadows; pgoff_t writeback_index; //address_space相关操作,定义了具体读写页面的钩子 const struct address_space_operations *a_ops; unsigned long flags; struct backing_dev_info *backing_dev_info; spinlock_t private_lock; struct list_head private_list; void *private_data; } __attribute__((aligned(sizeof(long)))); //include/linux/fs.h struct address_space_operations { //具体写页面的操作 int (*writepage)(struct page *page, struct writeback_control *wbc); //具体读页面的操作 int (*readpage)(struct file *, struct page *); int (*writepages)(struct address_space *, struct writeback_control *); //标记页面脏 int (*set_page_dirty)(struct page *page); int (*readpages)(struct file *filp, struct address_space *mapping, struct list_head *pages, unsigned nr_pages); int (*write_begin)(struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned flags, struct page **pagep, void **fsdata); int (*write_end)(struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned copied, struct page *page, void *fsdata); sector_t (*bmap)(struct address_space *, sector_t); void (*invalidatepage) (struct page *, unsigned int, unsigned int); int (*releasepage) (struct page *, gfp_t); void (*freepage)(struct page *); ssize_t (*direct_IO)(int, struct kiocb *, struct iov_iter *iter, loff_t offset); int (*get_xip_mem)(struct address_space *, pgoff_t, int, void **, unsigned long *); int (*migratepage) (struct address_space *, struct page *, struct page *, enum migrate_mode); int (*launder_page) (struct page *); int (*is_partially_uptodate) (struct page *, unsigned long, unsigned long); void (*is_dirty_writeback) (struct page *, bool *, bool *); int (*error_remove_page)(struct address_space *, struct page *); /* swapfile support */ int (*swap_activate)(struct swap_info_struct *sis, struct file *file, sector_t *span); void (*swap_deactivate)(struct file *file); }; 2.系统调用read流程与页高速缓存相关代码分析 关于挂载和打开文件的操作,不赘述(涉及的细节也很多…),(极其)简陋地理解,挂载返回挂载点的root dentry,并且读取磁盘数据生成了super_block链接到全局超级块链表中,这样,当前进程就可以通过root dentry找到其inode,从而找到并生成其子树的dentry和inode信息,从而实现查找路径的逻辑。打开文件简单理解就是分配fd,通过dentry将file结构与对应inode挂接,最后安装到进程的打开文件数组中,这里假设已经成功打开文件,返回了fd,我们从系统调用read开始。 定义系统调用read //定义系统调用read //fs/read_write.c SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count) { //根据fd number获得struct fd struct fd f = fdget_pos(fd); ssize_t ret = -EBADF; if (f.file) { //偏移位置 loff_t pos = file_pos_read(f.file); //进入vfs_read //参数:file指针,用户空间buffer指针,长度,偏移位置 //主要做一些验证工作,最后进入__vfs_read ret = vfs_read(f.file, buf, count, &pos); if (ret >= 0) file_pos_write(f.file, pos); fdput_pos(f); } return ret; } 进入__vfs_read //fs/read_write.c ssize_t __vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { ssize_t ret; //注意这,我们可以在file_operations中定义自己的read操作,使不使用页高速缓存可以自己控制 if (file->f_op->read) ret = file->f_op->read(file, buf, count, pos); else if (file->f_op->aio_read) //会调用f_ops->read_iter ret = do_sync_read(file, buf, count, pos); else if (file->f_op->read_iter) //会调用f_ops->read_iter //这里ext2中又将read_iter直接与generic_file_read_iter挂接,使用内核自带的read操作,稍后会以ext2为例分析 ret = new_sync_read(file, buf, count, pos); else ret = -EINVAL; return ret; } 以ext2为例,进入ext2的file_operations->read //fs/ext2/file.c const struct file_operations ext2_file_operations = { .llseek = generic_file_llseek, .read = new_sync_read, //重定向到read_iter此处即generic_file_read_iter .write = new_sync_write, .read_iter = generic_file_read_iter, //使用内核自带的通用读操作,这里会进入页高速缓冲的部分 .write_iter = generic_file_write_iter, .unlocked_ioctl = ext2_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = ext2_compat_ioctl, #endif .mmap = generic_file_mmap, .open = dquot_file_open, .release = ext2_release_file, .fsync = ext2_fsync, .splice_read = generic_file_splice_read, .splice_write = iter_file_splice_write, }; 进入generic_file_read_iter ssize_t generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter) { struct file *file = iocb->ki_filp; ssize_t retval = 0; loff_t *ppos = &iocb->ki_pos; loff_t pos = *ppos; /* coalesce the iovecs and go direct-to-BIO for O_DIRECT */ if (file->f_flags & O_DIRECT) { struct address_space *mapping = file->f_mapping; struct inode *inode = mapping->host; size_t count = iov_iter_count(iter); loff_t size; if (!count) goto out; /* skip atime */ size = i_size_read(inode); //先写? retval = filemap_write_and_wait_range(mapping, pos, pos + count - 1); if (!retval) { struct iov_iter data = *iter; retval = mapping->a_ops->direct_IO(READ, iocb, &data, pos); } if (retval > 0) { *ppos = pos + retval; iov_iter_advance(iter, retval); } /* * Btrfs can have a short DIO read if we encounter * compressed extents, so if there was an error, or if * we've already read everything we wanted to, or if * there was a short read because we hit EOF, go ahead * and return. Otherwise fallthrough to buffered io for * the rest of the read. */ if (retval < 0 || !iov_iter_count(iter) || *ppos >= size) { file_accessed(file); goto out; } } //进入真正read,在address_space的radix tree中查找 //偏移的page,如果找到,直接copy到用户空间如果未找到, //则调用a_ops->readpage读取发起bio,分配cache page, //读入数据,加入radix,然后拷贝到用户空间,完成读取数据的过程. retval = do_generic_file_read(file, ppos, iter, retval); out: return retval; } EXPORT_SYMBOL(generic_file_read_iter); 进入do_generic_file_read 这个函数基本是整个页高速缓存的核心了,在具体的bio操作请求操作之前判断是否存在缓存页面,如果存在拷贝数据到用户空间,否则分配新页面,调用具体文件系统address_space_operations->readpage读取块数据到页面中,并且加入到radix tree中。 static ssize_t do_generic_file_read(struct file *filp, loff_t *ppos,struct iov_iter *iter, ssize_t written) { /* 省略部分 */ for (;;) { struct page *page; pgoff_t end_index; loff_t isize; unsigned long nr, ret; //读页面的过程中可能重新调度 cond_resched(); find_page: //redix tree中查找 page = find_get_page(mapping, index); //没找到 if (!page) { //先读到页缓存 //分配list page_pool //调用a_ops->readpages or a_ops->readpage读取数据 //a_ops->readpage负责提交bio page_cache_sync_readahead(mapping, ra, filp, index, last_index - index); //再找 page = find_get_page(mapping, index); //还是没找到... if (unlikely(page == NULL)) //去分配页面再读 goto no_cached_page; } //readahead related if (PageReadahead(page)) { page_cache_async_readahead(mapping, ra, filp, page, index, last_index - index); } //不是最新 if (!PageUptodate(page)) { if (inode->i_blkbits == PAGE_CACHE_SHIFT || !mapping->a_ops->is_partially_uptodate) goto page_not_up_to_date; if (!trylock_page(page)) goto page_not_up_to_date; if (!page->mapping) goto page_not_up_to_date_locked; if (!mapping->a_ops->is_partially_uptodate(page, offset, iter->count)) goto page_not_up_to_date_locked; unlock_page(page); } page_ok: //好,拿到的cached page正常了 /* 省略其他检查部分 */ //到这,从磁盘中读取块到page cache或者本身page cache存在,一切正常,拷贝到用户空间 ret = copy_page_to_iter(page, offset, nr, iter); offset += ret; index += offset >> PAGE_CACHE_SHIFT; offset &= ~PAGE_CACHE_MASK; prev_offset = offset; //释放页面 page_cache_release(page); written += ret; if (!iov_iter_count(iter)) goto out; if (ret < nr) { error = -EFAULT; goto out; } //继续 continue; page_not_up_to_date: /* Get exclusive access to the page ... */ error = lock_page_killable(page); if (unlikely(error)) goto readpage_error; page_not_up_to_date_locked: /* Did it get truncated before we got the lock? */ if (!page->mapping) { unlock_page(page); page_cache_release(page); continue; } /* Did somebody else fill it already? */ if (PageUptodate(page)) { unlock_page(page); goto page_ok; } readpage: //为了no_cached_page /* * A previous I/O error may have been due to temporary * failures, eg. multipath errors. * PG_error will be set again if readpage fails. */ ClearPageError(page); /* Start the actual read. The read will unlock the page. */ //还是调用a_ops->readpage error = mapping->a_ops->readpage(filp, page); if (unlikely(error)) { if (error == AOP_TRUNCATED_PAGE) { page_cache_release(page); error = 0; goto find_page; } goto readpage_error; } if (!PageUptodate(page)) { error = lock_page_killable(page); if (unlikely(error)) goto readpage_error; if (!PageUptodate(page)) { if (page->mapping == NULL) { /* * invalidate_mapping_pages got it */ unlock_page(page); page_cache_release(page); goto find_page; } unlock_page(page); shrink_readahead_size_eio(filp, ra); error = -EIO; goto readpage_error; } unlock_page(page); } //page ok goto page_ok; readpage_error: /* UHHUH! A synchronous read error occurred. Report it */ page_cache_release(page); goto out; no_cached_page: /* * Ok, it wasn't cached, so we need to create a new * page.. */ //从冷页面链表中拿一个page page = page_cache_alloc_cold(mapping); if (!page) { error = -ENOMEM; goto out; } //加入cache error = add_to_page_cache_lru(page, mapping, index, GFP_KERNEL); if (error) { page_cache_release(page); if (error == -EEXIST) { error = 0; goto find_page; } goto out; } goto readpage; } /* 省略部分 */ ref: Linux Kernel 3.19.3 source code
Linux内核的VFS是非常经典的抽象,不仅抽象出了flesystem,super_block,inode,dentry,file等结构,而且还提供了像页高速缓存层的通用接口,当然,你可以自己选择是否使用或者自己定制使用方式。本文主要根据自己阅读Linux Kernel 3.19.3系统调用read相关的源码来追踪页高速缓存在整个流程中的痕迹,以常规文件的页高速缓存为例,了解页高速缓存的实现过程,不过于追究具体bio请求的底层细节。另外,在写操作的过程中,页高速缓存的处理流程有所不同(回写),涉及的东西更多,本文主要关注读操作。Linux VFS相关的重要数据结构及概念可以参考Document目录下的vfs.txt。 1.与页高速缓存相关的重要数据结构 除了前述基本数据结构以外,struct address_space 和 struct address_space_operations也在页高速缓存中起着极其重要的作用。 address_space结构通常被struct page的一个字段指向,主要存放已缓存页面的相关信息,便于快速查找对应文件的缓存页面,具体查找过程是通过radix tree结构的相关操作实现的。 address_space_operations结构定义了具体读写页面等操作的钩子,比如生成并发送bio请求,我们可以定制相应的函数实现自己的读写逻辑。 //include/linux/fs.h struct address_space { //指向文件的inode,可能为NULL struct inode *host; //存放装有缓存数据的页面 struct radix_tree_root page_tree; spinlock_t tree_lock; atomic_t i_mmap_writable; struct rb_root i_mmap; struct list_head i_mmap_nonlinear; struct rw_semaphore i_mmap_rwsem; //已缓存页的数量 unsigned long nrpages; unsigned long nrshadows; pgoff_t writeback_index; //address_space相关操作,定义了具体读写页面的钩子 const struct address_space_operations *a_ops; unsigned long flags; struct backing_dev_info *backing_dev_info; spinlock_t private_lock; struct list_head private_list; void *private_data; } __attribute__((aligned(sizeof(long)))); //include/linux/fs.h struct address_space_operations { //具体写页面的操作 int (*writepage)(struct page *page, struct writeback_control *wbc); //具体读页面的操作 int (*readpage)(struct file *, struct page *); int (*writepages)(struct address_space *, struct writeback_control *); //标记页面脏 int (*set_page_dirty)(struct page *page); int (*readpages)(struct file *filp, struct address_space *mapping, struct list_head *pages, unsigned nr_pages); int (*write_begin)(struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned flags, struct page **pagep, void **fsdata); int (*write_end)(struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned copied, struct page *page, void *fsdata); sector_t (*bmap)(struct address_space *, sector_t); void (*invalidatepage) (struct page *, unsigned int, unsigned int); int (*releasepage) (struct page *, gfp_t); void (*freepage)(struct page *); ssize_t (*direct_IO)(int, struct kiocb *, struct iov_iter *iter, loff_t offset); int (*get_xip_mem)(struct address_space *, pgoff_t, int, void **, unsigned long *); int (*migratepage) (struct address_space *, struct page *, struct page *, enum migrate_mode); int (*launder_page) (struct page *); int (*is_partially_uptodate) (struct page *, unsigned long, unsigned long); void (*is_dirty_writeback) (struct page *, bool *, bool *); int (*error_remove_page)(struct address_space *, struct page *); /* swapfile support */ int (*swap_activate)(struct swap_info_struct *sis, struct file *file, sector_t *span); void (*swap_deactivate)(struct file *file); }; 2.系统调用read流程与页高速缓存相关代码分析 关于挂载和打开文件的操作,不赘述(涉及的细节也很多…),(极其)简陋地理解,挂载返回挂载点的root dentry,并且读取磁盘数据生成了super_block链接到全局超级块链表中,这样,当前进程就可以通过root dentry找到其inode,从而找到并生成其子树的dentry和inode信息,从而实现查找路径的逻辑。打开文件简单理解就是分配fd,通过dentry将file结构与对应inode挂接,最后安装到进程的打开文件数组中,这里假设已经成功打开文件,返回了fd,我们从系统调用read开始。 定义系统调用read //定义系统调用read //fs/read_write.c SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count) { //根据fd number获得struct fd struct fd f = fdget_pos(fd); ssize_t ret = -EBADF; if (f.file) { //偏移位置 loff_t pos = file_pos_read(f.file); //进入vfs_read //参数:file指针,用户空间buffer指针,长度,偏移位置 //主要做一些验证工作,最后进入__vfs_read ret = vfs_read(f.file, buf, count, &pos); if (ret >= 0) file_pos_write(f.file, pos); fdput_pos(f); } return ret; } 进入__vfs_read //fs/read_write.c ssize_t __vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { ssize_t ret; //注意这,我们可以在file_operations中定义自己的read操作,使不使用页高速缓存可以自己控制 if (file->f_op->read) ret = file->f_op->read(file, buf, count, pos); else if (file->f_op->aio_read) //会调用f_ops->read_iter ret = do_sync_read(file, buf, count, pos); else if (file->f_op->read_iter) //会调用f_ops->read_iter //这里ext2中又将read_iter直接与generic_file_read_iter挂接,使用内核自带的read操作,稍后会以ext2为例分析 ret = new_sync_read(file, buf, count, pos); else ret = -EINVAL; return ret; } 以ext2为例,进入ext2的file_operations->read //fs/ext2/file.c const struct file_operations ext2_file_operations = { .llseek = generic_file_llseek, .read = new_sync_read, //重定向到read_iter此处即generic_file_read_iter .write = new_sync_write, .read_iter = generic_file_read_iter, //使用内核自带的通用读操作,这里会进入页高速缓冲的部分 .write_iter = generic_file_write_iter, .unlocked_ioctl = ext2_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = ext2_compat_ioctl, #endif .mmap = generic_file_mmap, .open = dquot_file_open, .release = ext2_release_file, .fsync = ext2_fsync, .splice_read = generic_file_splice_read, .splice_write = iter_file_splice_write, }; 进入generic_file_read_iter ssize_t generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter) { struct file *file = iocb->ki_filp; ssize_t retval = 0; loff_t *ppos = &iocb->ki_pos; loff_t pos = *ppos; /* coalesce the iovecs and go direct-to-BIO for O_DIRECT */ if (file->f_flags & O_DIRECT) { struct address_space *mapping = file->f_mapping; struct inode *inode = mapping->host; size_t count = iov_iter_count(iter); loff_t size; if (!count) goto out; /* skip atime */ size = i_size_read(inode); //先写? retval = filemap_write_and_wait_range(mapping, pos, pos + count - 1); if (!retval) { struct iov_iter data = *iter; retval = mapping->a_ops->direct_IO(READ, iocb, &data, pos); } if (retval > 0) { *ppos = pos + retval; iov_iter_advance(iter, retval); } /* * Btrfs can have a short DIO read if we encounter * compressed extents, so if there was an error, or if * we've already read everything we wanted to, or if * there was a short read because we hit EOF, go ahead * and return. Otherwise fallthrough to buffered io for * the rest of the read. */ if (retval < 0 || !iov_iter_count(iter) || *ppos >= size) { file_accessed(file); goto out; } } //进入真正read,在address_space的radix tree中查找 //偏移的page,如果找到,直接copy到用户空间如果未找到, //则调用a_ops->readpage读取发起bio,分配cache page, //读入数据,加入radix,然后拷贝到用户空间,完成读取数据的过程. retval = do_generic_file_read(file, ppos, iter, retval); out: return retval; } EXPORT_SYMBOL(generic_file_read_iter); 进入do_generic_file_read 这个函数基本是整个页高速缓存的核心了,在具体的bio操作请求操作之前判断是否存在缓存页面,如果存在拷贝数据到用户空间,否则分配新页面,调用具体文件系统address_space_operations->readpage读取块数据到页面中,并且加入到radix tree中。 static ssize_t do_generic_file_read(struct file *filp, loff_t *ppos,struct iov_iter *iter, ssize_t written) { /* 省略部分 */ for (;;) { struct page *page; pgoff_t end_index; loff_t isize; unsigned long nr, ret; //读页面的过程中可能重新调度 cond_resched(); find_page: //redix tree中查找 page = find_get_page(mapping, index); //没找到 if (!page) { //先读到页缓存 //分配list page_pool //调用a_ops->readpages or a_ops->readpage读取数据 //a_ops->readpage负责提交bio page_cache_sync_readahead(mapping, ra, filp, index, last_index - index); //再找 page = find_get_page(mapping, index); //还是没找到... if (unlikely(page == NULL)) //去分配页面再读 goto no_cached_page; } //readahead related if (PageReadahead(page)) { page_cache_async_readahead(mapping, ra, filp, page, index, last_index - index); } //不是最新 if (!PageUptodate(page)) { if (inode->i_blkbits == PAGE_CACHE_SHIFT || !mapping->a_ops->is_partially_uptodate) goto page_not_up_to_date; if (!trylock_page(page)) goto page_not_up_to_date; if (!page->mapping) goto page_not_up_to_date_locked; if (!mapping->a_ops->is_partially_uptodate(page, offset, iter->count)) goto page_not_up_to_date_locked; unlock_page(page); } page_ok: //好,拿到的cached page正常了 /* 省略其他检查部分 */ //到这,从磁盘中读取块到page cache或者本身page cache存在,一切正常,拷贝到用户空间 ret = copy_page_to_iter(page, offset, nr, iter); offset += ret; index += offset >> PAGE_CACHE_SHIFT; offset &= ~PAGE_CACHE_MASK; prev_offset = offset; //释放页面 page_cache_release(page); written += ret; if (!iov_iter_count(iter)) goto out; if (ret < nr) { error = -EFAULT; goto out; } //继续 continue; page_not_up_to_date: /* Get exclusive access to the page ... */ error = lock_page_killable(page); if (unlikely(error)) goto readpage_error; page_not_up_to_date_locked: /* Did it get truncated before we got the lock? */ if (!page->mapping) { unlock_page(page); page_cache_release(page); continue; } /* Did somebody else fill it already? */ if (PageUptodate(page)) { unlock_page(page); goto page_ok; } readpage: //为了no_cached_page /* * A previous I/O error may have been due to temporary * failures, eg. multipath errors. * PG_error will be set again if readpage fails. */ ClearPageError(page); /* Start the actual read. The read will unlock the page. */ //还是调用a_ops->readpage error = mapping->a_ops->readpage(filp, page); if (unlikely(error)) { if (error == AOP_TRUNCATED_PAGE) { page_cache_release(page); error = 0; goto find_page; } goto readpage_error; } if (!PageUptodate(page)) { error = lock_page_killable(page); if (unlikely(error)) goto readpage_error; if (!PageUptodate(page)) { if (page->mapping == NULL) { /* * invalidate_mapping_pages got it */ unlock_page(page); page_cache_release(page); goto find_page; } unlock_page(page); shrink_readahead_size_eio(filp, ra); error = -EIO; goto readpage_error; } unlock_page(page); } //page ok goto page_ok; readpage_error: /* UHHUH! A synchronous read error occurred. Report it */ page_cache_release(page); goto out; no_cached_page: /* * Ok, it wasn't cached, so we need to create a new * page.. */ //从冷页面链表中拿一个page page = page_cache_alloc_cold(mapping); if (!page) { error = -ENOMEM; goto out; } //加入cache error = add_to_page_cache_lru(page, mapping, index, GFP_KERNEL); if (error) { page_cache_release(page); if (error == -EEXIST) { error = 0; goto find_page; } goto out; } goto readpage; } /* 省略部分 */ ref: Linux Kernel 3.19.3 source code
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/49181571 简单描述了x86 32位体系结构下Linux内核的用户进程和内核线程的线性地址空间和物理内存的联系,分析了高端内存的引入与缺页中断的具体处理流程。先介绍了用户态进程的执行流程,然后对比了内核线程,引入高端内存的概念,最后分析了缺页中断的流程。 用户进程 fork之后的用户态进程已经建立好了所需的数据结构,比如task struct,thread info,mm struct等,将编译链接好的可执行程序的地址区域与进程结构中内存区域做好映射,等开始执行的时候,访问并未经过映射的用户地址空间,会发生缺页中断,然后内核态的对应中断处理程序负责分配page,并将用户进程空间导致缺页的地址与page关联,然后检查是否有相同程序文件的buffer,因为可能其他进程执行同一个程序文件,已经将程序读到buffer里边了,如果没有,则将磁盘上的程序部分读到buffer,而buffer head通常是与分配的页面相关联的,所以实际上会读到对应页面代表的物理内存之中,返回到用户态导致缺页的地址继续执行,此时经过mmu的翻译,用户态地址成功映射到对应页面和物理地址,然后读取指令执行。在上述过程中,如果由于内存耗尽或者权限的问题,可能会返回-NOMEM或segment fault错误给用户态进程。 内核线程 没有独立的mm结构,所有内核线程共享一个内核地址空间与内核页表,由于为了方便系统调用等,在用户态进程规定了内核的地址空间是高1G的线性地址,而低3G线性地址空间供用户态使用。注意这部分是和用户态进程的线性地址是重合的,经过mmu的翻译,会转换到相同的物理地址,即前1G的物理地址(准确来讲后128M某些部分的物理地址可能会变化),内核线程访问内存也是要经过mmu的,所以借助用户态进程的页表,虽然内核有自己的内核页表,但不直接使用(为了减少用户态和内核态页表切换的消耗?),用户进程页表的高1G部分实际上是共享内核页表的映射的,访问高1G的线性地址时能访问到低1G的物理地址。而且,由于从用户进程角度看,内核地址空间只有3G-4G这一段(内核是无法直接访问0-3G的线性地址空间的,因为这一段是用户进程所有,一方面如果内核直接读写0-3G的线性地址可能会毁坏进程数据结构,另一方面,不同用户态进程线性地址空间实际映射到不同的物理内存地址,所以可能此刻内核线程借助这个用户态进程的页表成功映射到某个物理地址,但是到下一刻,借助下一个用户态进程的页表,相同的线性地址就可能映射到不同的物理内存地址了)。 高端内存 那么,如何让内核访问到大于1G的物理内存?由此引入高端内存的概念,基本思路就是将3G-4G这1G的内核线性地址空间(从用户进程的角度看,从内核线程的角度看是0-1G)取出一部分挪作他用,而不是固定映射,即重用部分内核线性地址空间,映射到1G之上的物理内存。所以,对于x86 32位体系上的Linux内核将3G-4G的线性地址空间分为0-896m和896m-1G的部分,前面部分使用固定映射,当内核使用进程页表访问3G-3G+896m的线性地址时,不会发生缺页中断,但是当访问3G+896m以上的线性地址时,可能由于内核页表被更新,而进程页表还未和内核页表同步,此时会发生内核地址空间的缺页中断,从而将内核页表同步到当前进程页表。注意,使用vmalloc分配内存的时候,可能已经设置好了内核页表,等到下一次借助进程页表访问内核空间地址发生缺页时才会触发内核页表和当前页表的同步。 Linux x86 32位下的线性地址空间与物理地址空间 (图片出自《understanding the linux virtual memory manager》) 缺页 page fault的处理过程如下:在用户空间上下文和内核上下文下都可能访问缺页的线性地址导致缺页中断,但有些情况没有实际意义。 如果缺页地址位于内核线性地址空间 如果在vmalloc区,则同步内核页表和用户进程页表,否则挂掉。注意此处未分具体上下文 如果发生在中断上下文或者!mm,则检查exception table,如果没有则挂掉。 如果缺页地址发生在用户进程线性地址空间 如果在内核上下文,则查exception table,如果没有,则挂掉。这种情况没多大实际意义 如果在用户进程上下文 查找vma,找到,先判断是否需要栈扩张,否则进入通常的处理流程 查找vma,未找到,bad area,通常返回segment fault 具体的缺页中断流程图及代码如下: (图片出自《understanding the linux virtual memory manager》) (Linux 3.19.3 arch/x86/mm/fault.c 1044) /* * This routine handles page faults. It determines the address, * and the problem, and then passes it off to one of the appropriate * routines. * * This function must have noinline because both callers * {,trace_}do_page_fault() have notrace on. Having this an actual function * guarantees there's a function trace entry. */ //处理缺页中断 //参数:寄存器值,错误码,缺页地址 static noinline void __do_page_fault(struct pt_regs *regs, unsigned long error_code, unsigned long address) { struct vm_area_struct *vma; struct task_struct *tsk; struct mm_struct *mm; int fault, major = 0; unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE; tsk = current; mm = tsk->mm; /* * Detect and handle instructions that would cause a page fault for * both a tracked kernel page and a userspace page. */ if (kmemcheck_active(regs)) kmemcheck_hide(regs); prefetchw(&mm->mmap_sem); if (unlikely(kmmio_fault(regs, address))) return; /* * We fault-in kernel-space virtual memory on-demand. The * 'reference' page table is init_mm.pgd. * * NOTE! We MUST NOT take any locks for this case. We may * be in an interrupt or a critical region, and should * only copy the information from the master page table, * nothing more. * * This verifies that the fault happens in kernel space * (error_code & 4) == 0, and that the fault was not a * protection error (error_code & 9) == 0. */ //如果缺页地址位于内核空间 if (unlikely(fault_in_kernel_space(address))) { if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) { //位于内核上下文 if (vmalloc_fault(address) >= 0) //如果位于vmalloc区域 vmalloc_sync_one同步内核页表进程页表 return; if (kmemcheck_fault(regs, address, error_code)) return; } /* Can handle a stale RO->RW TLB: */ if (spurious_fault(error_code, address)) return; /* kprobes don't want to hook the spurious faults: */ if (kprobes_fault(regs)) return; /* * Don't take the mm semaphore here. If we fixup a prefetch * fault we could otherwise deadlock: */ bad_area_nosemaphore(regs, error_code, address); return; } /* kprobes don't want to hook the spurious faults: */ if (unlikely(kprobes_fault(regs))) return; if (unlikely(error_code & PF_RSVD)) pgtable_bad(regs, error_code, address); if (unlikely(smap_violation(error_code, regs))) { bad_area_nosemaphore(regs, error_code, address); return; } /* * If we're in an interrupt, have no user context or are running * in an atomic region then we must not take the fault: */ //如果位于中断上下文或者!mm, 出错 if (unlikely(in_atomic() || !mm)) { bad_area_nosemaphore(regs, error_code, address); return; } /* * It's safe to allow irq's after cr2 has been saved and the * vmalloc fault has been handled. * * User-mode registers count as a user access even for any * potential system fault or CPU buglet: */ if (user_mode_vm(regs)) { local_irq_enable(); error_code |= PF_USER; flags |= FAULT_FLAG_USER; } else { if (regs->flags & X86_EFLAGS_IF) local_irq_enable(); } perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address); if (error_code & PF_WRITE) flags |= FAULT_FLAG_WRITE; /* * When running in the kernel we expect faults to occur only to * addresses in user space. All other faults represent errors in * the kernel and should generate an OOPS. Unfortunately, in the * case of an erroneous fault occurring in a code path which already * holds mmap_sem we will deadlock attempting to validate the fault * against the address space. Luckily the kernel only validly * references user space from well defined areas of code, which are * listed in the exceptions table. * * As the vast majority of faults will be valid we will only perform * the source reference check when there is a possibility of a * deadlock. Attempt to lock the address space, if we cannot we then * validate the source. If this is invalid we can skip the address * space check, thus avoiding the deadlock: */ if (unlikely(!down_read_trylock(&mm->mmap_sem))) { if ((error_code & PF_USER) == 0 && !search_exception_tables(regs->ip)) { bad_area_nosemaphore(regs, error_code, address); return; } retry: down_read(&mm->mmap_sem); } else { /* * The above down_read_trylock() might have succeeded in * which case we'll have missed the might_sleep() from * down_read(): */ might_sleep(); } //缺页中断地址位于用户空间 //查找vma vma = find_vma(mm, address); //没找到,出错 if (unlikely(!vma)) { bad_area(regs, error_code, address); return; } //检查在vma的地址的合法性 if (likely(vma->vm_start <= address)) goto good_area; if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) { bad_area(regs, error_code, address); return; } //如果在用户上下文 if (error_code & PF_USER) { /* * Accessing the stack below %sp is always a bug. * The large cushion allows instructions like enter * and pusha to work. ("enter $65535, $31" pushes * 32 pointers and then decrements %sp by 65535.) */ if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) { bad_area(regs, error_code, address); return; } } //栈扩张 if (unlikely(expand_stack(vma, address))) { bad_area(regs, error_code, address); return; } /* * Ok, we have a good vm_area for this memory access, so * we can handle it.. */ //vma合法 good_area: if (unlikely(access_error(error_code, vma))) { bad_area_access_error(regs, error_code, address); return; } /* * If for any reason at all we couldn't handle the fault, * make sure we exit gracefully rather than endlessly redo * the fault. Since we never set FAULT_FLAG_RETRY_NOWAIT, if * we get VM_FAULT_RETRY back, the mmap_sem has been unlocked. */ //调用通用的缺页处理 fault = handle_mm_fault(mm, vma, address, flags); major |= fault & VM_FAULT_MAJOR; /* * If we need to retry the mmap_sem has already been released, * and if there is a fatal signal pending there is no guarantee * that we made any progress. Handle this case first. */ if (unlikely(fault & VM_FAULT_RETRY)) { /* Retry at most once */ if (flags & FAULT_FLAG_ALLOW_RETRY) { flags &= ~FAULT_FLAG_ALLOW_RETRY; flags |= FAULT_FLAG_TRIED; if (!fatal_signal_pending(tsk)) goto retry; } /* User mode? Just return to handle the fatal exception */ if (flags & FAULT_FLAG_USER) return; /* Not returning to user mode? Handle exceptions or die: */ no_context(regs, error_code, address, SIGBUS, BUS_ADRERR); return; } up_read(&mm->mmap_sem); if (unlikely(fault & VM_FAULT_ERROR)) { mm_fault_error(regs, error_code, address, fault); return; } /* * Major/minor page fault accounting. If any of the events * returned VM_FAULT_MAJOR, we account it as a major fault. */ if (major) { tsk->maj_flt++; perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1, regs, address); } else { tsk->min_flt++; perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, regs, address); } check_v8086_mode(regs, address, tsk); } NOKPROBE_SYMBOL(__do_page_fault);
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/48605863 (额…觉得Linux编译链接过程和启动过程还是有那么点作用的哈,要理清楚细节非常多…趟了不少源码…记此备忘) 编译流程 1.编译除arch/x86/boot目录外的其他目录,生成各模块的built_in.o,将静态编译进内核的模块链接成ELF格式的文件vmlinux大约100M,置于源码根目录之下 2.通过objcopy将源码根目录下的vmlinux去掉符号等信息置于arch/x86/boot/compressed/vmlinux.bin,大约15M,将其压缩为boot/vmlinux.bin.gz(假设配置的压缩工具是gzip)。 3.使用生成的compressed/mkpiggy为compressed/vmlinux.bin.gz添加解压缩程序头,生成compressed/piggy.S,进而生成compressed/piggy.o。 4.将compressed/head_64.o,compressed/misc.o,compressed/piggy.o链接为compressed/vmlinux。 5.回到boot目录,用objcopy为compressed/vmlinux去掉符号等信息生成boot/vmlinux.bin。 6.将boot/setup.bin与boot/vmlinux.bin链接,生成bzImage。 7.将各个设置为动态编译的模块链接为内核模块kmo。 8.over,maybe copy bzImage to /boot and kmods to /lib. 下面是内核镜像的组成: 启动流程 早期版本的linux内核,如0.1,是通过自带的bootsect.S/setup.S引导,现在需要通过bootloader如grub/lilo来引导。grub的作用大致如下: 1.grub安装时将stage1 512字节和所在分区文件系统类型对应的stage1.5文件分别写入mbr和之后的扇区。 2.bios通过中断加载mbr的512个字节的扇区到0x7c00地址,跳转到0x07c0:0x0000执行。 3.通过bios中断加载/boot/grub下的stage2,读取/boot/grub/menu.lst配置文件生成启动引导菜单。 4.加载/boot/vmlinuz-xxx-xx与/boot/inird-xxx,将控制权交给内核。 下面是较为详细的步骤: 1.BIOS加载硬盘第一个扇区(MBR 512字节)到0000:07C00处,MBR包含引导代码(446字节,比如grub第一阶段的引导代码),分区表(64字节)信息,结束标志0xAA55(2字节) 2.MBR开始执行加载活跃分区,grub第一阶段代码加载1.5阶段的文件系统相关的代码(通过bios中断读活跃分区的扇区) 3.有了grub1.5阶段的文件系统相关的模块,接下来读取位于文件系统的grub第2阶段的代码,并执行 4.grub第2阶段的代码读取/boot/grub.cfg文件,生成引导菜单 5.加载对应的压缩内核vmlinuz和initrd(到哪个地址?) 6.实模式下执行vmlinuz头setup部分(bootsect和setup)[head.S[calll main],main.c[go_to_protected_mode]] ==> 准备进入32位保护模式 7.跳转到过渡的32位保护模式执行compressed/head_64.S[startup_32,startup_64] ==> 进入临时的32位保护模式 8.解压缩剩余的vmlinuz,设置页表等,设置64位环境,跳转到解压地址执行 ==> 进入64位 9.arch/x86/kernel/head_64.S[startup_64] 10.arch/x86/kernel/head64.c[x86_64_start_up] 11.init/main.c[start_kernel] 12.然后后面的事情就比较好知道了:) ref: Linux source code 3.19.3
(额…觉得Linux编译链接过程和启动过程还是有那么点作用的哈,要理清楚细节非常多…趟了不少源码…记此备忘) 编译流程 1.编译除arch/x86/boot目录外的其他目录,生成各模块的built_in.o,将静态编译进内核的模块链接成ELF格式的文件vmlinux大约100M,置于源码根目录之下 2.通过objcopy将源码根目录下的vmlinux去掉符号等信息置于arch/x86/boot/compressed/vmlinux.bin,大约15M,将其压缩为boot/vmlinux.bin.gz(假设配置的压缩工具是gzip)。 3.使用生成的compressed/mkpiggy为compressed/vmlinux.bin.gz添加解压缩程序头,生成compressed/piggy.S,进而生成compressed/piggy.o。 4.将compressed/head_64.o,compressed/misc.o,compressed/piggy.o链接为compressed/vmlinux。 5.回到boot目录,用objcopy为compressed/vmlinux去掉符号等信息生成boot/vmlinux.bin。 6.将boot/setup.bin与boot/vmlinux.bin链接,生成bzImage。 7.将各个设置为动态编译的模块链接为内核模块kmo。 8.over,maybe copy bzImage to /boot and kmods to /lib. 下面是内核镜像的组成: 启动流程 早期版本的linux内核,如0.1,是通过自带的bootsect.S/setup.S引导,现在需要通过bootloader如grub/lilo来引导。grub的作用大致如下: 1.grub安装时将stage1 512字节和所在分区文件系统类型对应的stage1.5文件分别写入mbr和之后的扇区。 2.bios通过中断加载mbr的512个字节的扇区到0x7c00地址,跳转到0x07c0:0x0000执行。 3.通过bios中断加载/boot/grub下的stage2,读取/boot/grub/menu.lst配置文件生成启动引导菜单。 4.加载/boot/vmlinuz-xxx-xx与/boot/inird-xxx,将控制权交给内核。 下面是较为详细的步骤: 1.BIOS加载硬盘第一个扇区(MBR 512字节)到0000:07C00处,MBR包含引导代码(446字节,比如grub第一阶段的引导代码),分区表(64字节)信息,结束标志0xAA55(2字节) 2.MBR开始执行加载活跃分区,grub第一阶段代码加载1.5阶段的文件系统相关的代码(通过bios中断读活跃分区的扇区) 3.有了grub1.5阶段的文件系统相关的模块,接下来读取位于文件系统的grub第2阶段的代码,并执行 4.grub第2阶段的代码读取/boot/grub.cfg文件,生成引导菜单 5.加载对应的压缩内核vmlinuz和initrd(到哪个地址?) 6.实模式下执行vmlinuz头setup部分(bootsect和setup)[head.S[calll main],main.c[go_to_protected_mode]] ==> 准备进入32位保护模式 7.跳转到过渡的32位保护模式执行compressed/head_64.S[startup_32,startup_64] ==> 进入临时的32位保护模式 8.解压缩剩余的vmlinuz,设置页表等,设置64位环境,跳转到解压地址执行 ==> 进入64位 9.arch/x86/kernel/head_64.S[startup_64] 10.arch/x86/kernel/head64.c[x86_64_start_up] 11.init/main.c[start_kernel] 12.然后后面的事情就比较好知道了:) ref: Linux source code 3.19.3
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/44811085 In this article,I will talk about the ELF files for Linux,and their diffs as to comprehend the linking process in a diff view angle.There are many articles talking about that topic in the way of compiling->linking->loading->executing.I think once we understand the ELF files,then we can understand why and how and understand the whole process more precisely. ELF(Executable and Linkable Format) is the default file format of executable files,object files,shared object files and core file for Linux.Why should we know about the ELF?I think there are at least the following reasons: Guess what the back end of compilers(esp for c) wants to achieve(yeah,it’s “guess”,if you want to learn more about compilers,you should learn related theories more precisely) Learn about the linking and loading process Better understanding of the organization of our code,that’s helpful to debug and profiling(such the effects of static/local/global/extern,debug coredump file…) guess how the OS loader loads the exec file to memory and run it(yeah,guess again^_^,I will analyze the loading process in some article later) Better understanding of how the disk exec file mapped into memory space of process … I will choose the first three representative ELF files in Linux to analyze,that is the object files,executable files,and shared object files.Actually,the are very simmilar except some differences(bacause they share elf struct which is defined in the elf.h^_^) Here is the code used for the analysis purpose of this article(main.c and print.c): //main.c //note that we define diff kinds of vars so we can test where they are lied in within diff sections or segments,thus we can better understand the layout of c file #include <stdio.h> int main_global_unitialized; int main_global_initialized = 1; static int main_local_uninitialized; static int main_local_uninitialized = 2; extern void print(int); void print1(int); static void print2(int); int main() { int main_stack_uninitialized; int main_stack_initialized = 3; static main_func_local_uninitialized; static main_func_local_initialized = 4; print(2); print1(2); print2(2); return 0; } void print1(int a) { fprintf(stdout, "%d\n", a); } static void print2(int a) { fprintf(stdout, "%d\n", a); } //print.c #include <stdio.h> //in module vars int print_global_uninitialized; int print_global_initialized = 1; static int print_local_static_uninitialized; static int print_local_static_initialized = 2; void print(int a) { int print_stack_uninitialized; int print_stack_initialized = 3; static int print_func_local_static_uninitialized; static int print_func_local_static_initialized = 4; fprintf(stdout, "%d\n", a); } static void print2(int a) { fprintf(stdout, "%d\n", a); } We use the above two souce files to generate the following file: gcc -o print.o -c print.c gcc -o main.o -c main.c gcc -shared -fPIC -o print.so print.c gcc -o exec main.o print.o //and the tools we may use: //readelf/objdump/nm/size... //the platform is Linux tan 3.13.0-51-generic #84-Ubuntu SMP x86_64 x86_64 x86_64 GNU/Linux We will look into some diffs of the aboved mentioned 3 kinds of ELF files,we mainly use readelf. Usually,ELF files (may) consists of the following parts: file headers: describe file info such as start point,section header start poit etc program header table: info of mapping sections to segments section header table: section entry info sections: each section contents such as .data .text etc … 1.file headers executable file headers: object file headers: shared object file headers: As we can see,the file header of exec/.o/.so are almost the same except some options like type,start point…: #define EI_NIDENT 16 typedef struct { //magic number denotes the file,16 byte,for ELF:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 unsigned char e_ident[EI_NIDENT]; //ELF file type:exec/.o/.so/core/unknown uint16_t e_type; uint16_t e_machine; uint32_t e_version; //_start point of the program Elf64_Addr e_entry; //program header table offset,no for .o file Elf64_Off e_phoff; //section header table offset Elf64_Off e_shoff; uint32_t e_flags; //header size uint16_t e_ehsize; //entry size for program header table uint16_t e_phentsize; //program entry numbers uint16_t e_phnum; //entry size for section header table uint16_t e_shentsize; //section entry number uint16_t e_shnum; uint16_t e_shstrndx; } Elf64_Ehdr; Conclusions or diffs: + type is different + no start point for .o file + no program header for .o file 2.section header table exec file section header table: .so file section header table: .o file section header table: typedef struct { uint32_t sh_name;// section name uint32_t sh_type;//section type:RELA,STRTAB,SYMTAB... uint64_t sh_flags;//rwe... Elf64_Addr sh_addr; //virtual address(no for .o file) Elf64_Off sh_offset; uint64_t sh_size; uint32_t sh_link; uint32_t sh_info; uint64_t sh_addralign;//address align uint64_t sh_entsize; } Elf64_Shdr; Conclusions or diffs: + the sections of exec file and .so file are almost the same + more sections in exec and .so file than .o file since some sections like .fini/.init_array/.fini_array/.got/.got.plt/.init … are added during linking process + the virtual address of every section in .o file is 0,but not in exec and .so file 3.program header table exec file program header: .so file program header: .o file program header: typedef struct { uint32_t p_type; //segment type uint32_t p_flags; //permission Elf64_Off p_offset; // offset Elf64_Addr p_vaddr; //virtual address Elf64_Addr p_paddr; //phisical address uint64_t p_filesz; //size uint64_t p_memsz; uint64_t p_align; } Elf64_Phdr; Conclusions or diffs: + there is no program header for .o file + some sections are divided into one segment according to the permisson flag 4.common sections explain .intern #path for elf interpretor #here is an article about changing the ld #http://nixos.org/patchelf.html .dynsym #the dynamic linking symbol table .dynstr #strings needed for dynamic linking .init #executable instructions that contribute to the process initialization code(before main) .plt #procedure linkage table #for GOT dynamic linker .text #executable instructions of a program .fini #executable instructions that contribute to the process termination code .rodata # read-only data .dynamic #dynamic linking information .got #global offset table #for dynamic linker to resolve global elements .data # initialized data .bss #uninitialized data .comment #version control info .shstrtab #section names .strtab #string table .symtab #symble table .debug #debug info for C++: .ctors #initialized pointers to the C++ constructor functions .dtors #initialized pointers to the C++ destructor functions The sections contain important data for linking and relocation , while segments contain information that is necessary for runtime execution of the file. About program header:http://www.sco.com/developers/gabi/latest/ch5.pheader.html here is picture portraits the diff views for sections and segments: 5.code mapped into sections In the last part we take a look at the symble table for our code in main.c and print.c and verify the scope of vars. //get symble info use nm nm exec | egrep "main|print" ---------------------------- the 3 columns: --vaddress (dynamic linking is NULL) --symble type and local or global 1.uppercase stands for global,lowercase is local 2. B|b:uninitialized(BSS) D|d:initialized data section T|t:text (code) section R|r: read only data section ... --symble So we can check the symbles in main.c and print.c,you can find how the code and data are mapper to each sections,and then organized into segments when linked into executable elf files,so it can be easily loaded into memory(actually mapped) by the OS loader(to some extent we can think it’s execve syscall) 6 to the end In all,I talk about the three parts of the elf files in Linux especially some diffs between exec,.so and .o files and the organization for sections and segments. If you want to know more precisely about these topics,you can refer to the following materials: Ref: some articles: http://man7.org/linux/man-pages/man5/elf.5.html(man manual is so powerful^_^) https://en.wikipedia.org/wiki/Executable_and_Linkable_Format http://www.sco.com/developers/gabi/latest/ch5.pheader.html http://tech.meituan.com/linker.html some books: 1.CSAPP 2.《程序员的自我修养》
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/48420871 1.根文件系统的挂载 mount_root[init/do_mounts.c] create_dev(“/dev/root”, ROOT_DEV) ==> how to do this sys_unlink(“/dev/root”) sys_mknod(“/dev/root”,…) mount_block_root(“/dev/root”, flags) get_fs_names -> copy root_fs_names do_mount_root ==> 调用系统调用sys_mount,sys_chdir(“/root”),为当 前进程current->fs.pwd.dentry设置挂载返回的root dentry sys_mount ==>进入系统调用 2.进入系统调用的挂载,也是用户态挂载文件系统的入口 sys_mount[fs/namespace.c] copy_mount_string copy_mount_options do_mount user_path(dirname, &path) -> path_lookupat ==> 获取挂载路径的struct path,查找路径是很复杂的过程:) do_remount do_loopback do_change_type do_move_mount do_new_mount ==> 构建虚拟挂载点vsfmount,检查设置namespace get_fs_type ==> 在file_systems查找传入的文件系统类型 vfs_kernel_mount ==> 调用具体文件系统的mount,并将返回的root dentry与分配的vfsmount挂接 alloc_vfsmnt ==> 从slob中分配虚拟挂载点struct vfsmount root = mount_fs type->mount ==> 调用具体文件系统的mount,返回root dentry 设置vfsmount结构内容 3.进入具体文件系统类型的mount,此处以ext2为例分析 模块初始化时 init_inodecache分配slab作为inode的cache register_filesystem加入模块全局变量file_systems struct file_system_type ext2_fs_type ext2_mount mount_bdev ==> 传入ext2_fill_super,返回root dentry blkdev_get_by_path ==> 通过设备节点路径名构造block_device结构 sget ==> 获取或分配super block结构体,并将bdev绑定到sb上 fill_super ==> 填充super block sb_bread ==> 读取super block的count块到buffer head,先查找lru缓存 __find_get_clock_slow ==> 根据bdev中的inode及inode中的i_mapping信息将page cache读到buffer head里 kill_block_super
1.根文件系统的挂载 mount_root[init/do_mounts.c] create_dev(“/dev/root”, ROOT_DEV) ==> how to do this sys_unlink(“/dev/root”) sys_mknod(“/dev/root”,…) mount_block_root(“/dev/root”, flags) get_fs_names -> copy root_fs_names do_mount_root ==> 调用系统调用sys_mount,sys_chdir(“/root”),为当 前进程current->fs.pwd.dentry设置挂载返回的root dentry sys_mount ==>进入系统调用 2.进入系统调用的挂载,也是用户态挂载文件系统的入口 sys_mount[fs/namespace.c] copy_mount_string copy_mount_options do_mount user_path(dirname, &path) -> path_lookupat ==> 获取挂载路径的struct path,查找路径是很复杂的过程:) do_remount do_loopback do_change_type do_move_mount do_new_mount ==> 构建虚拟挂载点vsfmount,检查设置namespace get_fs_type ==> 在file_systems查找传入的文件系统类型 vfs_kernel_mount ==> 调用具体文件系统的mount,并将返回的root dentry与分配的vfsmount挂接 alloc_vfsmnt ==> 从slob中分配虚拟挂载点struct vfsmount root = mount_fs type->mount ==> 调用具体文件系统的mount,返回root dentry 设置vfsmount结构内容 3.进入具体文件系统类型的mount,此处以ext2为例分析 模块初始化时 init_inodecache分配slab作为inode的cache register_filesystem加入模块全局变量file_systems struct file_system_type ext2_fs_type ext2_mount mount_bdev ==> 传入ext2_fill_super,返回root dentry blkdev_get_by_path ==> 通过设备节点路径名构造block_device结构 sget ==> 获取或分配super block结构体,并将bdev绑定到sb上 fill_super ==> 填充super block sb_bread ==> 读取super block的count块到buffer head,先查找lru缓存 __find_get_clock_slow ==> 根据bdev中的inode及inode中的i_mapping信息将page cache读到buffer head里 kill_block_super
1.builtin types 基本定义在Zend/zend_types.h和Zend/zend.h中 主要的几种: 原始类型:zend_bool,zend_uchar,zend_intptr_t.. 封装的用户直接接触的类型 zend_string,zend_array(HashTable),zend_object,zend_resource,zend_function.. 内部使用: zval,zend_value,zend_class_entry,zend_reference,zend_refcounted,zend_ast,zend_ast_ref,Bucket,zend_execute_data,HashTableIterator.. 基本上是围绕以上几种类型构建整个数据操作的,尤其是 zend_object,HashTable,zend_string,zval的操作很常用 2.a couple of important globals 整个PHP环境和Zend环境会涉及多个全局变量,下面是几个比较重要的: php_core_globals core_globals(main/php_globals.h) ==> PG PHP调度sapi和zend engine的整个环境的全局变量存放位置 sapi_globals_struct sapi_globals(main/SAPI.c) ==> SG SAPI模块的环境变量,主要是包括每次PHP请求的数据 zend_compiler_globals compiler_globals(Zend/zend_compile.c) ==> CG Zend engine 编译过程需要的全局变量 zend_executor_globals executor_globals(Zend/zend_compile.c) ==> EG Zend engine 执行过程中需要的全局变量 zend_alloc_globals alloc_globals(Zend/zend_alloc.c) ==> AG Zend engine memory management globals zend_gc_globals gc_globals(Zend/zend_gc.c) ==> GC_G Zend engine gc related globals module globals(用Zend/zend_API.h中的DECLARE宏定义在相应模块中) ==> PHP扩展模块的全局变量 以上几个全局变量基本是从PHP环境到SAPI环境到Zend engine编译执行以及我们写自己的扩展时会涉及到的。 3.sapi sapi模块主要是提供一套外界使用zend engine的统一接口,提供不同的服务模式,比如典型的几个模式: cli cgi apache module 不同的服务模式在模块的初始化,请求的初始化不太相同,对于服务类的通常只需要初始化一次sapi模块,sapi_startup–>sapi_module.startup 包括上述全局变量/PHP扩展模块初始化/Zend扩展的初始化等,之后只需要针对每次请求调用php_request_startup,zend_activate,sapi_activate,来激活request,sapi,zend engine,重新设置请求的数据,响应请求。 4.startup process 基本调用流程: //初次调用流程 sapi_startup(&sapi_module) sapi_module.startup() php_module_startup() sapi_activate() gc_global_ctor() zend_startup() zend_mm_startup() sapi_deactivate() //请求流程 php_request_startup() zend_activate() init_gc() init_compiler() init_executor() sapi_activate() php_request_shutdown() zend_deactivate() sapi_deactivate() or full zend_memory_shutdown or sapi_module.shutdown 5.memory manager (1)内存管理相关的重要数据结构 zend_mm_heap zemd_mm_storage zend_mm_chunck zend_mm_page zend_mm_page_map zend_mm_bin zend_arena zend_mm_huge_list zend_mm_free_slot (2)sapi模块第一次初始化时: zend_mm_startup alloc_globals_ctor zend_mm_init初始化zend_mm_heap,分配main_chunck,并初始化main_chunck中的zend_mm_heap结构体, main_chunck负责存储相关信息,只有main_chunck使用了heap结构体,并将heap与AG(mm_heap)挂接 (3)每次请求init_compiler时分配zend_memory_arena init_compiler zend_arena_create(64M) 放入CG(arena) emalloc _emalloc zend_mm_alloc_heap(AG(mm_heap)…) < 3072字节 zend_mm_alloc_small 根据size计算free_slots中是否还有空的小块item,如果有直接返回指针,否则zend_mm_alloc_small_slow zend_mm_alloc_pages根据bin_pages数组得到需要分配的页面, 如果空闲页面不足则重新zend_mm_chunck_alloc(mmap)分配chunck < 2M-4096字节 zend_mm_alloc_large 直接使用zend_mm_alloc_pages分配 zend_mm_alloc_huge 使用zend_mm_chunck_alloc,加入heap的hugeblock_list 6.compiler and executor (1)编译接口及编译流程: zend_eval_string(zend_execute_API.c) zend_eval_stringl zend_compile_string(zend_compile.c) compile_string(zend_language_scanner.c) zendparse(就是yyparse)(zend_language_parse.y) ==> 通过parser调用lexer,生成抽象语法树ast_list,存到CG(ast) zend_compile_top_stmt ==> 编译ast生成oparray pass_two(oparray) ==> 优化? (2)执行流程 zend_execute(zend_vm_execute.h) zend_vm_stack_push_call_frame i_init_execute_data zend_execute_ex execute_ex(zend_vm_execute.h) zend_vm_stack_free_call_frame 后续部分:gc/php modules and zend extensions/grammar/ast/thread safety ref:php-src
(一)配置详解 (注:本文是以前几篇博客的简单合并,未做更新) 鉴于目前资料大多数都是基于spring3的配置,本人在最初搭建的时候遇到很多问题,由此记录下来仅供参考 使用的jar文件 springframework4.0.6(为了方便整个先导入) hibernate4.3.6 /required/*下所有jar 以及 /optional下的c3p0(为了使用c3p0作为dataSource,使用其连接池) jstl.jar standard.jar ——为了使用jstl标签库 apoalliance.jar ——在AOP一些地方依赖这个库 commons-logging.jar 配置详细步骤 第一步,配置web.xml <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <display-name>app</display-name> <!-- context启动时加载hibennate的配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-*.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 设置spring的前端分发器,接受请求 --> <servlet> <servlet-name>myservlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>myservlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 默认错误页面的设置 --> <error-page> <error-code>404</error-code> <location>/WEB-INF/jsp/404.jsp</location> </error-page> <!-- <error-page> <exception-type>java.lang.Exception</exception-type> <location>/WEB-INF/jsp/exception.jsp</location> </error-page> --> </web-app> 第二步,myservlet-servlet.xml(DispatcherServlet)的配置文件相关部分,注意,如果在配置中用到了aop,tx,mvc等标签,须在xmlns中导入。 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd"> <!-- 扫描注解配置的包 --> <context:component-scan base-package="com.tan.*" /> <!-- 基于注释的事务,当注释中发现@Transactional时,使用id为“transactionManager”的事务管理器 --> <!-- 如果没有设置transaction-manager的值,则spring以缺省默认的事务管理器来处理事务,默认事务管理器为第一个加载的事务管理器 --> <tx:annotation-driven transaction-manager="transactionManager" /> <!-- 设置spring的mvc用注解 --> <mvc:annotation-driven /> <!-- 设置handler的映射方式,前面注解是其中一种 --> <!-- HandlerMapping <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/> <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/> --> <!-- 设置试图的解析ViewResolver --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <!-- <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> 此处不加这行似乎也能在jsp中用jstl,只要正确引入了tag--> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> <!-- 可以使用基于url的handlermapping <bean name="/hello" class="com.tan.controller.MyController"/> --> </beans> 第三步,hibenate相关的配置,spring-hibernate.xml。配置数据源->交给sessionFactory->交给spring事物管理transactionManager->spring接手 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd"> <!-- Hibernate4 --> <!-- shiyongproperties文件保存jdbs以及hibernate的相关变量,在具体配置处使用属性zhi值,必须在Spring配置文件的最前面加载,放在src目录 --> <context:property-placeholder location="classpath:persistence-mysql.properties" /> <!-- 获取数据源的几种方式DriverManagerDataSource、dbcp、c3p0,后两种支持连接池 --> <!-- class="org.apache.tomcat.dbcp.dbcp.BasicDataSource" 有连接池作用 --> <!-- class="org.springframework.jdbc.datasource.DriverManagerDataSource"无连接池作用 --> <!-- <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.user}" /> <property name="password" value="${jdbc.pass}" /> </bean> --> <!-- c3p0 有连接池作用,使用properties文件下的属性值,也可以直接填--> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driverClassName}" /> <property name="jdbcUrl" value="${jdbc.url}" /> <property name="user" value="${jdbc.user}" /> <property name="password" value="${jdbc.pass}" /> <property name="minPoolSize" value="2" /> <property name="maxPoolSize" value="50" /> <property name="initialPoolSize" value="10" /> <property name="maxIdleTime" value="60" /> <property name="acquireIncrement" value="2" /> </bean> <!-- 配置sessionFactory,统一管理一个数据库的连接 --> <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="packagesToScan"> <list> <!-- 可以加多个包,需要hibenate映射的类的包 --> <value>com.tan.model</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop> <prop key="hibernate.dialect">${hibernate.dialect}</prop> <prop key="hibernate.show_sql">${hibernate.show_sql}</prop> <!-- <prop key="hibernate.current_session_context_class">thread</prop> --> </props> </property> </bean> <!-- 配置Hibernate事务管理器 --> <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory" /> </bean> <!-- 配置事务异常封装 --> <bean id="persistenceExceptionTranslationPostProcessor" class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" /> </beans> 第四步,添加persistence-mysql.properties文件,如果在spring-hibenate.xml直接配置数据源的值,就不需要 # jdbc.X jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/springmvc(你的数据库名)?createDatabaseIfNotExist=true&amp;useUnicode=true&amp;characterEncoding=utf8 jdbc.user=root jdbc.pass= # hibernate.X hibernate.connection.driverClass=com.mysql.jdbc.Driver hibernate.connection.url=jdbc:mysql://localhost:3306/springmvc(你的数据库名) hibernate.dialect=org.hibernate.dialect.MySQL5Dialect hibernate.connection.username=root hibernate.connection.password= hibernate.show_sql=true hibernate.hbm2ddl.auto=update #如果没有表则创建,如果表结构更改则自动更新 (二)一些名词 MVC模型-视图-控制器 Spring MVC中的mvc和通常的web框架如,django、ROR、thinkphp、lavarel等都是很类似的,个人感觉区别在于Spring MVC中的model层功能更多的是有DAO+Service两层来完成,像front dispatcher、controller、viewresolver和其他框架都是很类似的 DAO数据访问层 DAO封装了对数据库的操作,可能是原子级的操作由Service组装,也可能是稍复杂的操作。供Service业务层调用 Service业务逻辑层 封装了业务逻辑的相关操作,由控制器层调用,这一层不是必须的,也可经控制器直接调用DAO中的方法 AOP面向切面编程 面向切面编程简单地理解就是把多个业务逻辑的共同功能模块提取出来,如回话管理、权限认证等。在django的auth模块,以及thinkphp的Behavior行为驱动层都是AOP的使用例子 DI依赖注入 依赖注入的一个简单例子就是Service层中注入需要用到的一个或多个DAO,在Controller中注入Service,使用annotation注入极大简化了代码的编写 ORM对象关系映射 这个与大多数orm框架的理念都是类似的,在hibernate中使用对象映射表,对象的实例映射表中一条记录 sessionFactory生成session的工厂 负责创建session,session是hibernate暴露给开发者的操作数据库的接口,在DAO中使用session通过save/update/saveorupdate/delete等方法或者hsql、原生sql访问数据库 Tansaction事务 这个与通常的数据库的事务的概念差不多,在springmvc与hibenate集成中,hibernate将transacition交给HibernateTransactionManager管理 Datasource 数据源就是hibernate的连接数据库的方式。通常有springmvc自带的DriverManagerDataSource、c3p0、dbcp,后两种支持连接池 (三)完整实例 POJO类,负责和数据库映射 package com.tan.model; import javax.persistence.*; @Entity(name = "users") //映射到users表 public class Users { public Users() { super(); } @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Integer id; @Column(name = "first_name", length = 32) private String first_name; @Column(name = "age") private Integer age; @Column(name = "last_name", length = 32) private String last_name; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getFirst_name() { return first_name; } public void setFirst_name(String first_name) { this.first_name = first_name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getLast_name() { return last_name; } public void setLast_name(String last_name) { this.last_name = last_name; } } DAO层 package com.tan.model; import java.util.List; import javax.annotation.Resource; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.stereotype.Repository; @Repository public class UsersDAO { @Resource(name = "sessionFactory") //使用annotation注入sessionFactory private SessionFactory sessionFactory; public void setSessionFactory(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } public SessionFactory getSessionFactory() { return sessionFactory; } public List<Users> getAllUser() { String hsql = "from users"; Session session = sessionFactory.getCurrentSession(); Query query = session.createQuery(hsql); return query.list(); } public void insert(Users u) { Session session = sessionFactory.getCurrentSession(); session.save(u); } } Service层 package com.tan.service; import java.util.List; import javax.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.tan.model.Users; import com.tan.model.UsersDAO; @Service("userService") @Transactional //开启事务 public class UserService { @Resource private UsersDAO userDao; public int userCount() { return userDao.getAllUser().size(); } @Transactional(readOnly = true) public List<Users> getAllUsers() { return userDao.getAllUser(); } public void insert(Integer age, String first_name, String last_name) { Users u = new Users(); u.setAge(age); u.setFirst_name(first_name); u.setLast_name(last_name); userDao.insert(u); } public void insert1(Users u) { userDao.insert(u); } public UsersDAO getUserDao() { return userDao; } public void setUserDao(UsersDAO userDao) { this.userDao = userDao; } } controller层 package com.tan.controller; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.servlet.ModelAndView;//不要引入错误的modelandview import com.tan.model.Users; import com.tan.service.UserService; import javax.annotation.Resource; @Controller @RequestMapping("/user") //映射url public class UserController { @Resource(name = "userService") //注入service层 private UserService service; @RequestMapping(value = "/man", method = RequestMethod.GET) public ModelAndView hello2() { ModelAndView mv = new ModelAndView(); mv.addObject("message", "HelloMVC"); // mv.setViewName("users"); return mv; } @RequestMapping(value = "/count", method = RequestMethod.GET) public ModelAndView count() { int c = service.userCount(); ModelAndView mv = new ModelAndView(); mv.addObject("message", c); mv.setViewName("user/count"); return mv; } @RequestMapping(value = "/list", method = RequestMethod.GET) @ResponseBody //返回json public ModelAndView list() { List<Users> u = service.getAllUsers(); ModelAndView mv = new ModelAndView(); mv.addObject("u", u); // mv.setViewName("users"); return mv; } @RequestMapping(value = "/add", method = RequestMethod.GET) public String insert() { return "user/add"; } /* * @RequestMapping(value="/insert",method=RequestMethod.POST) * * @ResponseBody public ModelAndView insert( * * @RequestParam("age") Integer age, * * @RequestParam("first_name") String first_name, * * @RequestParam("last_name") String last_name ){ * * service.insert(age,first_name,last_name); ModelAndView mv = new * ModelAndView(); return mv; * * } */ @RequestMapping(value = "/insert", method = RequestMethod.POST) public String insert(Users u) { service.insert1(u); return "redirect:/user/list"; } @RequestMapping("/path/{id}") //pathinfo模式 @ResponseBody public Integer path(@PathVariable("id") Integer id) { return id; } } (四)注解式事务 配置和使用 在myservlet-servlet.xml里面添加 <tx:annotation-driven transaction-manager="transactionManager" /> 不要加到spring-hibernate配置文件中,会产生session null错误 确保数据库本身是支持事务的引擎 在Service层使用事务,在整个类中用@Transactional声明会作用于所有public方法,在function里的@Transactional会覆盖class的. insert方法上没使用@Transactional,如果此时class上也没使用,则会报错,而insert1不会. package com.tan.service; import java.util.List; import javax.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.tan.model.Users; import com.tan.model.UsersDAO; @Service("userService") @Transactional(readOnly = true) //开启事务 public class UserService { @Resource private UsersDAO userDao; public int userCount() { return userDao.getAllUser().size(); } @Transactional(readOnly = true) public List<Users> getAllUsers() { return userDao.getAllUser(); } public void insert(Integer age, String first_name, String last_name) { Users u = new Users(); u.setAge(age); u.setFirst_name(first_name); u.setLast_name(last_name); userDao.insert(u); } @Transactional(propagation = Propagation.REQUIRED, readOnly = false) public void insert1(Users u) { userDao.insert(u); } public UsersDAO getUserDao() { return userDao; } public void setUserDao(UsersDAO userDao) { this.userDao = userDao; } } 事务属性 传播行为(propagation=..) 常用的几个 PROPAGATION_REQUIRED—-必须运行在事务中 PROPAGATION_SUPPORTS—-事务不必须,但如果有事务则运行在事务中 PROPAGATION_NEVER——-不能运行在事务中,如果有则抛出异常 …. 隔离级别(isolation = ) 默认完全隔离,可以适当放松隔离优化性能 只读(readOnly = true) 超时(timeout = ) 回滚规则(rollback-for = ) (五)简单Session登录管理 session登录实例,实现了四个方法login/logout/doLogin/testLogin,加入前面UserController中 @RequestMapping(value = "/doLogin", method = RequestMethod.POST) public String doLogin(HttpServletRequest req,@RequestParam("user") String username, @RequestParam("pass") String password) { //System.out.println(username+"_"+password); if (username.equals( "user") && password.equals( "pass")) { HttpSession s = req.getSession(); s.setAttribute("isLogin", "y"); return "redirect:/user/testLogin"; } else { return "redirect:/user/login"; } } @RequestMapping("/login") public String login() { return "user/login"; } @RequestMapping("/testLogin") public String testLogin(HttpServletRequest req) { if (req.getSession().getAttribute("isLogin") == "y") { return "user/home"; } else { return "user/login"; } } @RequestMapping("/logout") public String logout(HttpServletRequest req) { req.getSession().setAttribute("isLogin", null); return "user/login"; } 相应视图 //login.jsp// <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <form action="/app/user/doLogin" method="post"> user:<input type="text" name="user"><br /> pass:<input type="text" name="pass"><br /> <input type="submit" value="login"> </form> </body> </html> //home.jsp// <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> yes,login <br /> <a href="/app/user/logout">logout</a> </body> </html>
题目描述:0和1构成的二进制数,求被3除的余数 改变题目:0和1构成的十进制数,求被3除的余数 (对于改变的题目,可以求1的个数,再%3就行了,这里只是用来和二进制情况做个对比) 题解: 假设二进制数表示如下 An−1An−1An−3...A0 写成十进制为 S=An−1∗2n−1+An−2∗2n−2...+A0 注意这样一个事实 m∗2n=m∗(3−1)nm∗2n%3=m%3(n>=0,m−>int) 所以对于S前两项之和除以3的余数实际上等于 Mn−2=(2∗An−1+An−2)%3Mn−2=(2∗Mn−1+An−2)%3 同理,再增加一项An−2∗2n−2后前三项的余数为 Mn−3=(2∗Mn−2+An−3)%3 由此得递推公式 Mn=(2∗Mn−1+An−1)%3 所以可得到一个状态机,基于状态机用O(n)的复杂度即可求出原题的解 最后,附上一道用自动机能不错地解决的题数的合法字符串表示
写本文的目的是通过javascript/c++11/java8/python/scala等几种语言对lambda和闭包的支持的对比,探讨下lambda和闭包的区别与联系,以及作用域的trick。 在阅读这篇文章前,首先熟悉以下几个概念(有些概念不会谈,只是和本文所谈的lambda和闭包对比理解),摘自维基百科: --Closure-- In programming languages, closures (also lexical closures or function closures) are a technique for implementing lexically scoped name binding in languages with first-class functions. Operationally, a closure is a data structure storing a function[a] together with an environment:[1] a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or storage location the name was bound to at the time the closure was created.[b] A closure—unlike a plain function—allows the function to access those captured variables through the closure's reference to them, even when the function is invoked outside their scope. --lexical scope(static scope)-- With lexical scope, a name always refers to its (more or less) local lexical environment. This is a property of the program text and is made independent of the runtime call stack by the language implementation. Because this matching only requires analysis of the static program text, this type of scoping is also called static scoping --dynamic scope-- With dynamic scope, a global identifier refers to the identifier associated with the most recent environment, and is uncommon in modern languages.[4] In technical terms, this means that each identifier has a global stack of bindings. Introducing a local variable with name x pushes a binding onto the global x stack (which may have been empty), which is popped off when the control flow leaves the scope. Evaluating x in any context always yields the top binding. Note that this cannot be done at compile-time because the binding stack only exists at run-time, which is why this type of scoping is called dynamic scoping. --anonymous function(common lambda expression)-- In computer programming, an anonymous function (also function literal or lambda abstraction) is a function definition that is not bound to an identifier. Anonymous functions are often:[1] -passed as arguments to higher-order functions, or -used to construct the result of a higher-order function that needs to return a function. --Lambda calculus-- Lambda calculus (also written as λ-calculus) is a formal system in mathematical logic for expressing computation based on function abstraction and application using variable binding and substitution. Lambda expressions are composed of -variables v1, v2, ..., vn, ... -the abstraction symbols lambda 'λ' and dot '.' -parentheses ( ) The set of lambda expressions, Λ, can be defined inductively: -If x is a variable, then x ∈ Λ -If x is a variable and M ∈ Λ, then (λx.M) ∈ Λ -If M, N ∈ Λ, then (M N) ∈ Λ -Instances of rule 2 are known as abstractions and instances of rule 3 are known as applications. 关于lambda(这里具体指匿名函数,而不是lambda calculus)与闭包的关系,我自己的理解主要是: 1、闭包实现可以通过类,函数实现,而匿名函数可以用来更方便地实现函数闭包,但通常比嵌套函数实现闭包局限更大,比如后面会会提到: python的lambda实现的闭包不能使用statement,需返回expression(value) 如:lambda : a if a>0 else -a 但是java的lambda提供了statement和expression两种 如:()->value,()->{return value;} 其他 2、闭包主要是对作用域的trick,与编程语言本身采用的lexical scope或者dynamic scope有关,两个非常重要的点是闭包中的操作对局部变量的获取方式(是否存在side-effect),值捕获(immutable,比如python2、python3不用nonlocal、java8、c++使用[=]capture、scala等)还是引用捕获(mutable,比如javascript、python3nonlocal、c++使用[&]capture等),这些操作是交给程序员(比如C++),还是留给compiler? 3、匿名函数通常在直接支持function为first-class的编程语言中用起来更顺畅,作为返回值或者参数传给高阶函数,尤其是动态语言如Scheme或者Python以及支持类型推导的静态语言如scala和c++。像java,虽然有java.util.function等包对函数式编程支持,但是没有类型推导,用起来还是稍显麻烦。 比如: python f = lambda x:x+1 scala val f = {a => a+1} c++11 auto f = ->void{return []{};}() 而java: Supplier f = (x)->x+1; Supplier f1 = (x)->{return x+1;} 4、闭包实质上是程序员对作用域的控制与改变,通过一定的trick来达到自己需求的变量生命周期,从而实现一些有意思的功能。如果想深入匿名函数相关,还得好好学习functional program,以及lambda calculus。 下面针对javascript、python、java、c++、scala具体举例: (注:下文的side-effect指修改局部变量,不包括IO等) javascript的lambda匿名函数和闭包: 值得注意的地方: 1、javascript中,闭包内函数inner capture局部变量是先在与 inner同一作用域中查找,不管变量的定义代码是在inner定义之上还是之下,只需要在同一作用域,这一点和python的nested function闭包类似,不过和c++就不同。 2、javascript不支持显示的函数式编程的expression,需要显示return,返回值,而且匿名函数里面是statement而不是expression,这一点与函数式语言像scala,python不同。 3、javascript支持闭包side-effect //javascript anonymous function closure can direct change the local vars var outer = function(){ var a = 0; return (function(){ //support lambda statements not direct support lambda expressions,need return explicitly a++; console.log(a); console.log(this); return 1212; //explicit return value //1212 //will not cause error,but also will not be returned }); }; var inner = outer(); var res = inner(); //1 inner(); //2 console.log(res); // 1212 var outer1 = (function(){ var a = 12; var func = function(){ a++; console.log(a); }; a = 1222; //notice the scope of javascript,similar to python return func; }); var inner1 = outer1(); inner1(); //1223 inner1(); //1224 python的lambda和闭包: 值得注意的地方: 1、python匿名函数体是expression,不能有statement,通常返回值(value) 2、闭包内部函数对局部变量的捕获与前面javascript类似 3、python2不支持对局部变量的修改,而python3引入nonlocal关键字后能支持闭包的side-effect。 #python anonymous function closure can not change the local vars #support lambda expressions not support lambda statements def outer(): a = 1 return lambda:a if a>0 else -a print(inner()) #pyyhon3 add nonlocal to allow closure change local vars def outer1(): a = 1 def inner1(): nonlocal a a = a+2; return a a = 3 #scope similar to javascript return inner1 inner1 = outer1() print(inner1()) print(inner1()) #before nonlocal,we can ref local vars def outer2(): a = 1 def inner2(): print(a) return inner2 inner2 = outer2() inner2() java中的lambda和闭包: 值得注意的地方: 1、java的lambda函数体支持statement和expression 2、java没有提供对局部变量的修改方式(不支持side-effect避免concurrency下的问题) 3、由于java没有nested function,可以使用内部类、局部类模拟 ref: http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/Lambda-QuickStart/index.html#section4 http://stackoverflow.com/questions/7367714/nested-functions-in-java https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html http://www.lambdafaq.org/what-are-the-reasons-for-the-restriction-to-effective-immutability/ import java.util.function.*; class Test{ public static void main(String args[]) { Test t = new Test(); Supplier<Integer> f = t.outer(); System.out.println(f.get()); //1 } Supplier<Integer> outer() { //Predicate<T> //Consumer<T> //Supplier<R> //Function<T,R> int a = 1; //lambda expressions way //return ()->a+1; //lambda statements way return ()->{System.out.println(a+1);return a;}; //error a should be immutable //return ()->{a++;return a;}; } } c++的lambda和闭包 值得注意的点: 1、c++将局部变量的capture方式交给了程序员capture by value and capture by reference 2、同java一样没有直接对nested function的支持,如std::function #include <iostream> #include <functional> std::function<void(void)> returnFunc() { int x = 0; std::function<void(void)> f = [=]()->void{ std::cout << x << std::endl; }; x = 12; return f; } std::function<void(void)> returnFunc1() { int x = 0; //不能保存变量x std::function<void(void)> f = [&]()->void{ x++; std::cout << x << std::endl; }; x = 12; return f; } int _tmain(int argc, _TCHAR* argv[]) { //Solution::test(); int a = 1; auto f = [=]()->int{return a; }; a = 2; std::cout << f() << std::endl; //1 but not 2 auto f1 = [&]()->int{a = 3; return a; }; std::cout << f1() << std::endl; //3 std::function<void(void)> f2 = returnFunc(); f2(); //0 not 12 f2(); //0 not 12 std::function<void(void)> f3 = returnFunc1(); f3(); //-858993459 not 1 f3(); //-858993459 not 2 return 0; } scala的lambda和闭包 由于函数式编程语言本身对first-class function、imutable、high-order function、type-inference等较好的支持,所以在函数式语言中使用lambda和闭包很顺畅。 值得注意的几点: 1、从test1可以看出,scala捕获局部变量的方式与js和python类似 2、从test2可以看出,scala支持闭包变量的保留,与js和python类似,而不同于c++ object YCombinator { def main(args:Array[String]) = { test1() //3 not 2 var f = test2() println(f()) //1 println(f()) //2 println(f()) //3 } def test1() = { var x = 1 val f = ()=>{x=x+1;x} x = 2 println(f()) //3 not 2 } def test2():()=>Int = { var a = 0 var b = 1 () => { println(b+"---") val t = a a = b b = t + b b } } } 总结 闭包和lambda的区别和联系在开头已经说过了,不再赘述。以上各种语言对闭包的支持大致可以归纳如下: javascript天生支持side-effect的闭包,而匿名函数的函数体支持statement,不直接支持表达式返回值,需显示返回值。局部变量的capture最近作用域是与闭包内部函数体同一级,而部分变量定义在之前还是之后。 python3中引入nonlocal关键字后支持闭包的side-effect,匿名函数体不支持statement,表达式直接返回值,nested function闭包内的函数体支持statement。局部变量的capture最近作用域与javascript类似。 java不支持闭包side-effect,匿名函数体支持statement,或者直接表达式返回值。 c++将匿名函数闭包的side-effect交给程序员控制,函数体不支持表达式直接返回,局部变量的capture最近作用域为闭包内部函数同一作用域且位于内部函数之前。 scala支持闭包的side-effect,匿名函数体支持statement和表达式,局部变量的capture最近作用域与javascript和python类似,基本提供了动态语言中使用lambda和闭包的方便性。
由于匿名函数(通常成为lambda函数但是跟lambda calculus不同)在递归时无法获得函数名,从而导致一些问题,而Y Combinator能很好地解决这个问题。利用不动点的原理,可以利用一般的函数来辅助得到匿名函数的递归形式,从而间接调用无法表达的真正的匿名函数。下面以一个阶乘的递归来说明。 #Python版本,后面会加上C++版本 #F(f) = f def F(f,n): return 1 if n==0 else n*f(n-1) #或者用lambda #F = lambda f,n: 1 if n==0 else n*f(n-1) #Y不能用lambda,因为Y会调用自己 #Y(F) = f = F(f) = F(Y(F)) def Y(F): return lambda n: F(Y(F),n) a = Y(F) # 6 print a(3) 一些解释: F是伪递归函数,将真正的我们假设的匿名函数作为参数,有性质 F(f)=f. 好了以上是我们的已知条件,为了得到f的间接表达式,我们引入Y函数 使得Y(F) = f 所以有Y(F) = f = F(f) = F(Y(F)) (最终的目标是要用YF的组合表示f),所以很容易就得到了Y(F)的函数表达式为F(Y(F)),而Y不是匿名函数,所以能自身调用(其实感觉这东西没想象中那么玄乎~),上面的代码也就比较好理解了。我们假设的函数只有一个额外参数n,这完全可以自己添加其他参数,只需稍微修改Y中F的调用。 最后附上一段C++的实现代码: //需要C++11支持 #include <iostream> #include <functional> //F(f) = f int F(std::function<int(int)> f, int n) { return n==0 ? 1 : n*f(n-1); } //或者 //auto F1 = [](std::function<int(int)> f, int n) { // return n==0 ? 1 : n*f(n-1); //}; //Y(F) = f = F(f) = F(Y(F)) std::function<int(int)> Y(std::function<int(std::function<int(int)>,int)> F) { return std::bind(F, std::bind(Y,F), std::placeholders::_1); } int main(int argc, char *argv[]) { auto f = Y(F); std::cout << f(3) << std::endl; //6 return 0; }
C++返回值优化和具名返回值优化是编译器的优化,在大多数情况下能提高性能,但是却难以受程序员控制。C++11中加入了move语义的支持,由此对RVO和NRVO会造成一定影响。下面以一段代码来说明。 RVO和NRVO在分别在copy/move construct,copy/move assignment八种简单情况,测试条件是g++ 4.8.2和clang++ 3.4,默认优化。 #include <iostream> #include <vector> #include <string> struct Test { Test() { std::cout << "construct a Test object" << std::endl; } Test(const Test&) { std::cout << "copy construct a Test object" << std::endl; } Test& operator=(const Test&) { std::cout << "copy assignment a Test object" << std::endl; return *this; } Test(Test&&) { std::cout << "move construct a Test object" << std::endl; } /* Test& operator=(Test &&t) { std::cout << "move assignment a Test object" << std::endl; return *this; } */ ~Test() { std::cout << "destruct a Test object" << std::endl; } }; Test getTest() { return Test(); } Test getTestWithName() { Test temp; return temp; } int main() { std::cout << "=============RVO==============" << std::endl; std::cout << "++Test obj rvo for copy construct" << std::endl; auto obj1 = getTest(); std::cout << "--------------" << std::endl; std::cout << "++Test obj rvo for move construct" << std::endl; auto obj111 = std::move(getTest()); std::cout << "--------------" << std::endl; std::cout << "++Test obj rvo for copy assignment" << std::endl; Test obj11; obj11 = getTest(); std::cout << "--------------" << std::endl; std::cout << "++Test object rvo for move assignment" << std::endl; Test obj1111; obj1111 = std::move(getTest()); std::cout << "=============NRVO==============" << std::endl; std::cout << "++Test obj nrvo for copy construct" << std::endl; auto obj2 = getTestWithName(); std::cout << "--------------" << std::endl; std::cout << "++Test obj nrvo for move construct" << std::endl; auto obj222 = std::move(getTestWithName()); std::cout << "--------------" << std::endl; std::cout << "++Test obj nrvo for copy assignment" << std::endl; Test obj22; obj22 = getTestWithName(); std::cout << "--------------" << std::endl; std::cout << "++Test obj nrvo for move assignment" << std::endl; Test obj2222; obj2222 = std::move(getTestWithName()); std::cout << "==============================" << std::endl; // std::string s1 = "s1 string move semantics test", s2; //std::cout << "++before move s1\t" << s1 << std::endl; //s2 = std::move(s1); //std::cout << "++after move s1\t" << s1 << std::endl; //std::cout << "=============" << std::endl; return 0; } 测试结果: =============RVO============== ++Test obj rvo for copy construct construct a Test object -------------- ++Test obj rvo for move construct construct a Test object move construct a Test object destruct a Test object -------------- ++Test obj rvo for copy assignment construct a Test object construct a Test object move assignment a Test object destruct a Test object -------------- ++Test object rvo for move assignment construct a Test object construct a Test object move assignment a Test object destruct a Test object =============NRVO============== ++Test obj nrvo for copy construct construct a Test object -------------- ++Test obj nrvo for move construct construct a Test object move construct a Test object destruct a Test object -------------- ++Test obj nrvo for copy assignment construct a Test object construct a Test object move assignment a Test object destruct a Test object -------------- ++Test obj nrvo for move assignment construct a Test object construct a Test object move assignment a Test object destruct a Test object ============================== destruct a Test object destruct a Test object destruct a Test object destruct a Test object destruct a Test object destruct a Test object destruct a Test object destruct a Test object 由此可得出几个简单结论: 1.copy construct本身在RVO和NRVO两种情况下被优化了,如果再加上move反而画蛇添足。 2.加入了move assignment后,默认是调用move assignment而不是copy assignment,可以将move assignment注释后测试。 3.对于RVO和NRVO来说,construction的情况编译器优化得比较好了,加入move语义主要是对于assignment有比较大影响
通常在需要大量线程连接或者需要执行异步任务的时候,为了避免线程多次创建的开销,我们可以事先创建一定数量的线程,组成一个线程池。由threadpool统一管理线程的生命期以及任务的添加。 线程池通常由四部分构成 线程池本身结构作为管理器 任务队列 工作线程 往工作线程中添加任务的接口 下面是一个Linux下的简单线程池的实现与演示(为了测试方便有些地方直接将pthread_t转换成了int打印,另外一些地方使用了gettid): //gcc Feature Test Macros,为了syscall //可以参照http://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <sys/types.h> #include <sys/syscall.h> #include <unistd.h> //任务回调函数形式 typedef void *(*callback)(void *args); //任务结构,包含一个回调成员,参数成员,任务指针next组成一个任务队列 typedef struct _task task; struct _task{ callback cb; void *args; struct _task *next; }; //线程池成员 typedef struct _pool pool; struct _pool{ int thread_number; //工作线程数量限制 int task_queue_size; //当前任务队列中的数量 int max_queue_size; //最大任务数量 int running; pthread_t *pt; //保存工作线程pthread_t for join task *task_queue_head; //任务队列 pthread_mutex_t queue_mutex; //队列锁 pthread_cond_t queue_cond; //条件变量 }; //工作线程执行的函数 void *routine(void *args); //线程池的初始化 void pool_init(pool *p, int thread_number, int max_queue_size) { p->thread_number = thread_number; p->max_queue_size = max_queue_size; p->task_queue_size = 0; p->task_queue_head = NULL; p->pt = (pthread_t *)malloc(sizeof(pthread_t)*p->thread_number); if(!p->pt){ perror("malloc pthread_t array failed"); exit(EXIT_FAILURE); } pthread_mutex_init(&p->queue_mutex,NULL); pthread_cond_init(&p->queue_cond,NULL); for(int i = 0; i < p->thread_number; i++) { pthread_create (&(p->pt[i]), NULL, routine, (void *)p); } p->running = 1; } //线程池的清理 void pool_clean(pool *p) { if(!p->running) return; p->running = 0; //tell all threads we are exiting pthread_cond_broadcast(&p->queue_cond); //wait and join all threads for (int i = 0; i < p->thread_number; ++i) { pthread_join(p->pt[i],NULL); } free(p->pt); //free task queue or if needed we can persistent the remaining task task *temp; while((temp=p->task_queue_head)!=NULL){ p->task_queue_head = p->task_queue_head->next; free(temp); } pthread_mutex_destroy(&p->queue_mutex); pthread_cond_destroy(&p->queue_cond); free(p); p = NULL; } //内部使用 int _pool_add_task(pool *p, task *t) { int ret = 0; pthread_mutex_lock(&p->queue_mutex); if(p->task_queue_size>=p->max_queue_size){ pthread_mutex_unlock(&p->queue_mutex); //for max queue size error ret = 1; return ret; } task *temp = p->task_queue_head; if(temp!=NULL){ while(temp->next!=NULL){ temp = temp->next; } temp->next = t; }else{ p->task_queue_head = t; } p->task_queue_size++; pthread_mutex_unlock(&p->queue_mutex); return ret; } //添加任务接口 int pool_add_task(pool *p, callback cb, void *data) { int ret = 0; task *t = (task *)malloc(sizeof(task)); t->cb = cb; t->args = data; t->next = NULL; if((ret=_pool_add_task(p,t))>0){ fprintf(stderr,"add wroker failed,reaching max size of task queue\n"); return ret; } return ret; } //线程routine void *routine(void *args) { pool *p = (pool *)args; task *t; fprintf(stdout,"thread_id:%ld\n",syscall(SYS_gettid)); while(1){ //将加锁放在条件等待之前可以避免每次添加任务将其他线程白白唤醒 //而且能保证接受destroy broadcast退出时不会竞争 pthread_mutex_lock(&p->queue_mutex); //wait while(p->task_queue_size==0 && p->running){ pthread_cond_wait(&p->queue_cond,&p->queue_mutex); } //wake up because pool_destroy if(!p->running){ pthread_mutex_unlock(&p->queue_mutex); fprintf(stdout,"thread:%d will exit pool_destroy\n",(int)pthread_self()); pthread_exit(NULL); } //wake up to get a task t = p->task_queue_head; p->task_queue_head = p->task_queue_head->next; p->task_queue_size--; pthread_mutex_unlock(&p->queue_mutex); //when we do the task,release mutex for other threads t->cb(t->args); } pthread_exit(NULL); } //测试用的任务回调函数 void *callbacktest(void *args) { fprintf(stdout,"from thread:%d---passed parameter:%d\n",(int)pthread_self(),(int)(*(int *)(args))); } int main() { pool *p = (pool *)malloc(sizeof(pool)); if(p==NULL){ fprintf(stderr,"malloc pool failed\n"); } pool_init(p,4,10); int args[10]; for (int i=0;i<11;i++){ args[i] = i; } for (int i=0;i<11;i++){ pool_add_task(p,&callbacktest,&args[i]); } sleep(10); pool_clean(p); return 0; } 上面是一个简单的线程池,限制了线程的数量,任务队列的最大任务数量。我们还可以加一些其他的高级特性,比如在退出时将未完成的队列任务持久化,使用多个队列,给任务产生随机唯一id从而实现cancel,任务延迟等等,不过这些更多的是队列的特性,在一些成熟的产品如beanstalkd等中实现得挺不错了。
在上一篇Linux x86_64进程内存空间布局中谈了两个不同参数下的进程运行时内存空间宏观的分布。也许你会注意到这样一个细节,在每个进程的stack以上的地址中,有一段动态变化的映射地址段,比如下面这个进程,映射到vdso。 如果我们用ldd看相应的程序,会发现vdso在磁盘上没有对应的so文件。 不记得曾经在哪里看到大概这样一个问题: getpid,gettimeofday是不是系统调用? 其实这个问题的答案就和vDSO有关,杂x86_64和i386上,getpid是系统调用,而gettimeofday不是。 vDSO全称是virtual dynamic shared object,是一种内核将一些本身应该是系统调用的直接映射到用户空间,这样对于一些使用比较频繁的系统调用,直接在用户空间调用可以节省开销。如果想详细了解,可以参考这篇文档 下面我们用一段程序验证下: #include <stdio.h> #include <sys/time.h> #include <sys/syscall.h> #include <unistd.h> int main(int argc, char **argv) { struct timeval tv; int ret; if ((ret=gettimeofday(&tv, NULL))<0) { fprintf(stderr, "gettimeofday call failed\n"); }else{ fprintf(stdout, "seconds:%ld\n", (long int)tv.tv_sec); } fprintf(stdout, "pid:%d\n", (int)getpid()); fprintf(stdout, "thread id:%d\n", (int)syscall(SYS_gettid)); return 0; } 编译为可执行文件后,我们可以用strace来验证: strace -o temp ./vdso grep getpid temp grep gettimeofday temp
在开发扩展之前,最好了解下PHP内核的执行流程,PHP大概包括三个方面: SAPI Zend VM 扩展 Zend VM是PHP的虚拟机,与JVM类似,都是各自语言的编译/执行的核心。它们都会把各自的代码先编译为一种中间代码,PHP的通常叫opcode,Java通常叫bytecode,不同的是PHP的opcode直接被Zend VM的执行单元调用对应的C函数执行(PHP7加入了AST,会先生成AST,再生成opcode),不会显示保留下来(可以cache保留),而Java通常是生成class文件保留下来。而这一点可能也是PHP interpreter的名称的由来吧。其实相对严格的C/C++等编译型语言,PHP和Java更多的是结合了编译型和解释性的风格。 SAPI可以看作是Zend VM向外界提供编译/执行PHP代码服务的方式和规范。无论是作为cli/cgi/fastcgi/apache_mod与其他程序交互,还是嵌入其他语言中如C/C++等,都可以通过SAPI的规范实现。它的一个重要数据结构就是sapi_module_struct(main/SAPI.h line 217) 扩展部分可以看作是搭建在Zend VM和SAPI之上的库,为PHP开发人员提供性能和易用性上的保证。Java的各种包/Python的各种模块功能类似,不同的是PHP中为了性能是用C扩展来实现的,类似的在Java中可以通过JNI来实现,Python中如_socket和_select(多路复用)都不是原生Python实现。 生命周期 关于各种SAPI或者PHP本身的生命周期,可能会和其他组件如apache耦合,后续再细谈。关于PHP扩展的生命周期,这里借用一张图。流程应该是很容易明白的,关于这个过程,网上也有很多资料,不再赘述。我们开发扩展需要注意的几个地方也可以对应到图中的某些节点: 全局变量的定义,通常是zend_modulename_globals 模块的初始化,包括资源/类/常量/ini配置等模块级的初始化 请求的初始化,包括与单次请求相关的一些初始化 请求的结束,清理单次请求相关的数据/内存 模块的卸载,清理模块相关的数据/内存 基本上我们要做的就是按照上面的流程,实现相关的内置函数,定义自己的资源/全局变量/类/函数等。值得注意的地方是在在嵌入其他语言如Python或者被嵌入其他组件如apache时,要小心多进程多线程相关的问题。 PHP扩展结构 使用php-src/ext/ext_skel可以生成PHP扩展的框架 ./ext_skel --extname=myext [tan@tan ~/software/needbak/php-5.5.20/ext 12:24]$==> ls myext/ config.m4 config.w32 CREDITS EXPERIMENTAL myext.c myext.php php_myext.h tests 比较重要的文件是config.m4(当然还有源码),config.m4文件可以使用phpize命令生成configure文件,其中说明了我们是否开启模块,以及外部依赖的库。 //config.m4 //如果你的扩展依赖其他外部库 dnl PHP_ARG_WITH(myext, for myext support, dnl Make sure that the comment is aligned: dnl [ --with-myext Include myext support]) //扩展不依赖外部库 dnl PHP_ARG_ENABLE(myext, whether to enable myext support, dnl Make sure that the comment is aligned: dnl [ --enable-myext Enable myext support]) //寻找并包含头文件 if test "$PHP_MYEXT" != "no"; then dnl Write more examples of tests here... dnl # --with-myext -> check with-path dnl SEARCH_PATH="/usr/local /usr" # you might want to change this dnl SEARCH_FOR="/include/myext.h" # you most likely want to change this dnl if test -r $PHP_MYEXT/$SEARCH_FOR; then # path given as parameter dnl MYEXT_DIR=$PHP_MYEXT dnl else # search default path list dnl AC_MSG_CHECKING([for myext files in default path]) dnl for i in $SEARCH_PATH ; do dnl if test -r $i/$SEARCH_FOR; then dnl MYEXT_DIR=$i dnl AC_MSG_RESULT(found in $i) dnl fi dnl done dnl fi dnl dnl if test -z "$MYEXT_DIR"; then dnl AC_MSG_RESULT([not found]) dnl AC_MSG_ERROR([Please reinstall the myext distribution]) dnl fi dnl # --with-myext -> add include path dnl PHP_ADD_INCLUDE($MYEXT_DIR/include) //加载的lib位置 dnl # --with-myext -> check for lib and symbol presence dnl LIBNAME=myext # you may want to change this dnl LIBSYMBOL=myext # you most likely want to change this dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL, dnl [ dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $MYEXT_DIR/$PHP_LIBDIR, MYEXT_SHARED_LIBADD) dnl AC_DEFINE(HAVE_MYEXTLIB,1,[ ]) dnl ],[ dnl AC_MSG_ERROR([wrong myext lib version or lib not found]) dnl ],[ dnl -L$MYEXT_DIR/$PHP_LIBDIR -lm dnl ]) dnl dnl PHP_SUBST(MYEXT_SHARED_LIBADD) PHP_NEW_EXTENSION(myext, myext.c, $ext_shared) fi //php_myext.h #ifndef PHP_MYEXT_H #define PHP_MYEXT_H extern zend_module_entry myext_module_entry; #define phpext_myext_ptr &myext_module_entry //导出符号,在链接的时候有用 #ifdef PHP_WIN32 # define PHP_MYEXT_API __declspec(dllexport) #elif defined(__GNUC__) && __GNUC__ >= 4 # define PHP_MYEXT_API __attribute__ ((visibility("default"))) #else # define PHP_MYEXT_API #endif #ifdef ZTS #include "TSRM.h" #endif //几个核心函数的声明 PHP_MINIT_FUNCTION(myext); PHP_MSHUTDOWN_FUNCTION(myext); PHP_RINIT_FUNCTION(myext); PHP_RSHUTDOWN_FUNCTION(myext); PHP_MINFO_FUNCTION(myext); //自动生成的测试函数声明,我们自己定义的模块函数需要在此声明 PHP_FUNCTION(confirm_myext_compiled); //全局变量在这定义,展开后是zend_myext_globals结构体 ZEND_BEGIN_MODULE_GLOBALS(myext) long global_value; char *global_string; ZEND_END_MODULE_GLOBALS(myext) //线程安全与非线程安全下获取全局变量的方式 #ifdef ZTS #define MYEXT_G(v) TSRMG(myext_globals_id, zend_myext_globals *, v) #else #define MYEXT_G(v) (myext_globals.v) #endif #endif /* PHP_MYEXT_H */ //myext.c #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #include "php_ini.h" #include "ext/standard/info.h" #include "php_myext.h" //全局变量声明 ZEND_DECLARE_MODULE_GLOBALS(myext) /* True global resources - no need for thread safety here */ static int le_myext; //模块函数的导出 const zend_function_entry myext_functions[] = { PHP_FE(confirm_myext_compiled, NULL) /* For testing, remove later. */ PHP_FE_END /* Must be the last line in myext_functions[] */ }; //模块结构 zend_module_entry myext_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif "myext", myext_functions, PHP_MINIT(myext), PHP_MSHUTDOWN(myext), PHP_RINIT(myext), /* Replace with NULL if there's nothing to do at request start */ PHP_RSHUTDOWN(myext), /* Replace with NULL if there's nothing to do at request end */ PHP_MINFO(myext), #if ZEND_MODULE_API_NO >= 20010901 PHP_MYEXT_VERSION, #endif STANDARD_MODULE_PROPERTIES }; #ifdef COMPILE_DL_MYEXT ZEND_GET_MODULE(myext) #endif //ini配置文件的设置 PHP_INI_BEGIN() STD_PHP_INI_ENTRY("myext.global_value", "42", PHP_INI_ALL, OnUpdateLong, global_value, zend_myext_globals, myext_globals) STD_PHP_INI_ENTRY("myext.global_string", "foobar", PHP_INI_ALL, OnUpdateString, global_string, zend_myext_globals, myext_globals) PHP_INI_END() //初始化全局变量 static void php_myext_init_globals(zend_myext_globals *myext_globals) { myext_globals->global_value = 0; myext_globals->global_string = NULL; } //模块加载时的函数 PHP_MINIT_FUNCTION(myext) { /* If you have INI entries, uncomment these lines REGISTER_INI_ENTRIES(); */ return SUCCESS; } //模块卸载时函数 PHP_MSHUTDOWN_FUNCTION(myext) { /* uncomment this line if you have INI entries UNREGISTER_INI_ENTRIES(); */ return SUCCESS; } //请求初始化函数 PHP_RINIT_FUNCTION(myext) { return SUCCESS; } //请求关闭函数 PHP_RSHUTDOWN_FUNCTION(myext) { return SUCCESS; } //模块信息,phpinfo PHP_MINFO_FUNCTION(myext) { php_info_print_table_start(); php_info_print_table_header(2, "myext support", "enabled"); php_info_print_table_end(); /* Remove comments if you have entries in php.ini DISPLAY_INI_ENTRIES(); */ } //测试函数 PHP_FUNCTION(confirm_myext_compiled) { char *arg = NULL; int arg_len, len; char *strg; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) { return; } len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "myext", arg); RETURN_STRINGL(strg, len, 0); }
一、官方实例博客源码 官方blog实例,此处摘抄了main函数部分 def main(): tornado.options.parse_command_line() http_server=tornado.httpserver.HTTPServer(Application()) #listen()函数是核心,他做的事情有: # (1)调用netutil中的bind_socket,返回的是绑定的所有IP地址的 socket # (2)通过add_socket方法调用netutil中的add_accept_handler方法,将建立的连接的回调处理函数_handle_connection添加到ioloop中 # (3)_handle_connection将handle_stream包装,实质是生成iostream,调用self.handle_stream(),handle_stream在HTTPServer中重新定义了,实则用HTTPConnection处理stream,将HTTPConnection对象传入application的__call__方法中,__call__方法负责完成响应。 #ioloop的start就是循环判断添加的handler队列是否可以处理,若可以,则调用_handle_connection处理。 http_server.listen(options.port) tornado.ioloop.IOLoop.instance().start() if __name__ == "__main__": main() 如上红色部分注释就是一个基本的流程。下面进入详细分析 二、详细分解main() 前面的设置与handler实例暂时不看,直接从main()函数出发 def main(): ##解析命令行参数 tornado.options.parse_command_line() ##构造一个httpserver,其实大部分都是继承至tcpserver,注意参数 Application()是个对象,而且是个可调用的对象,它里面有个方法__call__起了核心作用 http_server=tornado.httpserver.HTTPServer(Application()) http_server.listen(options.port) ##构造事件循环,并执行触发事件的相应handler/注册的timeout事件/注册的callback等。 tornado.ioloop.IOLoop.instance().start() 三、http_server.listen(options.port)详解 def listen(self, port, address=""): ##调用netutil中的bind_socket,返回的是绑定的所有(IP,port)地址的socket sockets = bind_sockets(port, address=address) ##自身的add_sockets方法中调用了netutil中的add_accept_handler self.add_sockets(sockets) 看看add_sockets干了什么: def add_sockets(self, sockets): if self.io_loop is None: self.io_loop = IOLoop.current() for sock in sockets: self._sockets[sock.fileno()] = sock ##这里回调的是_handle_connection,是处理请求的核心,稍后还会回头看add_accept_handler(sock,self._handle_connection,io_loop=self.io_loop) def add_accept_handler(sock, callback, io_loop=None): if io_loop is None: io_loop = IOLoop.current() def accept_handler(fd, events): while True: try: connection, address = sock.accept() except socket.error as e: if e.args[0] == errno.ECONNABORTED: continue raise callback(connection, address) ##把callback,也就是_handle_connection这个回调的handler注册到ioloop的多路复用(select/poll/epoll等)之上 io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ) ##ioloop.add_handler函数: def add_handler(self, fd, handler, events): self._handlers[fd] = stack_context.wrap(handler) self._impl.register(fd, events | self.ERROR) ##之后就由ioloop.start内的循环poll发生的事件并回调相应的handler了 四、handle_connection响应请求开始 这个函数位于tcpserver中: def _handle_connection(self, connection, address): if self.ssl_options is not None: assert ssl, "Python 2.6+ and OpenSSL required for SSL" try: connection = ssl_wrap_socket(connection, self.ssl_options, server_side=True, do_handshake_on_connect=False) except ssl.SSLError as err: if err.args[0] == ssl.SSL_ERROR_EOF: return connection.close() else: raise except socket.error as err: if errno_from_exception(err) in (errno.ECONNABORTED, errno.EINVAL): return connection.close() else: raise try: if self.ssl_options is not None: stream = SSLIOStream(connection, io_loop=self.io_loop, max_buffer_size=self.max_buffer_size, read_chunk_size=self.read_chunk_size) else: stream = IOStream(connection, io_loop=self.io_loop, max_buffer_size=self.max_buffer_size, read_chunk_size=self.read_chunk_size) self.handle_stream(stream, address) except Exception: app_log.error("Error in connection callback", exc_info=True) 实例化了iostream对象,这个对象专门负责读写数据。然后是调用heepserver重写的handle_stream方法,将stream交给HTTPConnection处理,注意这里的request_callback是Application对象 def handle_stream(self, stream, address): HTTPConnection(stream, address, self.request_callback, self.no_keep_alive, self.xheaders, self.protocol) 之后就到HTTPConnection初始化部分,核心就是_on_headers方法与read_until def __init__(self, stream, address, request_callback, no_keep_alive=False, xheaders=False, protocol=None): self._header_callback = stack_context.wrap(self._on_headers) self.stream.set_close_callback(self._on_connection_close) ##read_until可以暂时简单看作将数据读给_on_headers方法 self.stream.read_until(b"\r\n\r\n",self._header_callback) ##self.request_callback(self._request),这是调用Application的__call__方法,传入request对象完成响应 def _on_headers(self, data): try: data = native_str(data.decode('latin1')) eol = data.find("\r\n") start_line = data[:eol] try: method, uri, version = start_line.split(" ") except ValueError: raise _BadRequestException("Malformed HTTP request line") if not version.startswith("HTTP/"): raise _BadRequestException("Malformed HTTP version in HTTP Request-Line") try: headers = httputil.HTTPHeaders.parse(data[eol:]) except ValueError: # Probably from split() if there was no ':' in the line raise _BadRequestException("Malformed HTTP headers") # HTTPRequest wants an IP, not a full socket address if self.address_family in (socket.AF_INET, socket.AF_INET6): remote_ip = self.address[0] else: # Unix (or other) socket; fake the remote address remote_ip = '0.0.0.0' self._request = HTTPRequest( connection=self, method=method, uri=uri, version=version, headers=headers, remote_ip=remote_ip, protocol=self.protocol) content_length = headers.get("Content-Length") if content_length: content_length = int(content_length) if content_length > self.stream.max_buffer_size: raise _BadRequestException("Content-Length too long") if headers.get("Expect") == "100-continue": self.stream.write(b"HTTP/1.1 100 (Continue)\r\n\r\n") self.stream.read_bytes(content_length, self._on_request_body) return self.request_callback(self._request) except _BadRequestException as e: gen_log.info("Malformed HTTP request from %s: %s", self.address[0], e) self.close() return 五、application的call完成最终相应: def __call__(self, request): """Called by HTTPServer to execute the request.""" transforms = [t(request) for t in self.transforms] handler = None args = [] kwargs = {} handlers = self._get_host_handlers(request) if not handlers: handler = RedirectHandler( self, request, url="http://" + self.default_host + "/") else: for spec in handlers: match = spec.regex.match(request.path) if match: handler = spec.handler_class(self, request, **spec.kwargs) if spec.regex.groups: # None-safe wrapper around url_ to handle # unmatched optional groups correctly def unquote(s): if s is None: return s return escape.url_(s, encoding=None, plus=False) # Pass matched groups to the handler. Since # match.groups() includes both named and unnamed groups, # we want to use either groups or groupdict but not both. # Note that args are passed as bytes so the handler can # decide what encoding to use. if spec.regex.groupindex: kwargs = dict( (str(k), unquote(v)) for (k, v) in match.groupdict().items()) else: args = [unquote(s) for s in match.groups()] break if not handler: handler = ErrorHandler(self, request, status_code=404) # In debug mode, re-compile templates and reload static files on every # request so you don't need to restart to see changes if self.settings.get("debug"): with RequestHandler._template_loader_lock: for loader in RequestHandler._template_loaders.values(): loader.reset() StaticFileHandler.reset() handler._execute(transforms, *args, **kwargs) return handler handler._execute(transforms, *args, **kwargs)定义在RequestHandler中 def _execute(self, transforms, *args, **kwargs): """Executes this request with the given output transforms.""" self._transforms = transforms try: if self.request.method not in self.SUPPORTED_METHODS: raise HTTPError(405) self.path_args = [self.decode_argument(arg) for arg in args] self.path_kwargs = dict((k, self.decode_argument(v, name=k)) for (k, v) in kwargs.items()) if self.request.method not in ("GET", "HEAD", "OPTIONS") and \ self.application.settings.get("xsrf_cookies"): self.check_xsrf_cookie() self._when_complete(self.prepare(), self._execute_method)//prepare是空的,没被重写 except Exception as e: self._handle_request_exception(e) 调用的_when_complete回调callback,也就是_execute_method def _when_complete(self, result, callback): try: if result is None: callback() elif isinstance(result, Future): if result.done(): if result.result() is not None: raise ValueError('Expected None, got %r' % result) callback() else: from tornado.ioloop import IOLoop IOLoop.current().add_future( result, functools.partial(self._when_complete, callback=callback)) else: raise ValueError("Expected Future or None, got %r" % result) except Exception as e: self._handle_request_exception(e) 之后是_execute_method def _execute_method(self): if not self._finished: method = getattr(self, self.request.method.lower()) ####当method被执行过后,就直接调用finish,否则将method加入ioloop self._when_complete(method(*self.path_args, **self.path_kwargs), self._execute_finish) def _execute_finish(self): if self._auto_finish and not self._finished: self.finish() def finish(self, chunk=None): """Finishes this response, ending the HTTP request.""" if self._finished: raise RuntimeError("finish() called twice. May be caused " "by using async operations without the " "@asynchronous decorator.") if chunk is not None: self.write(chunk) 六、总结 至此,整个IO流程完毕,中间还有许多非常值得深入挖掘的地方,比如ioloop/iostream,future,异步客户端,web框架…在网络编程模型方面,是一个很完整的单线程epoll-level trigger-nonblocking的reactor模型,在异步读写等方面的封装值得细看。
epoll多路复用非阻塞模型 epoll多路复用技术相比select和poll更加高效,相比select和poll每次都轮询O(n),epoll每次返回k个有事件发生的fd,所以是O(k)的复杂度,或者说O(1)。epoll分为水平触发(LT)和垂直触发(ET),这两种方式下对fd的读写是很不一样的,这也是epoll编程的难点,现在很多网络库都是优先提供epoll作为多路复用的,如libev/libevent/muduo/boost.asio,还有一些组件如beanstalkd/nginx… epoll_create(<#(int)__size#>) epoll_ctl(<#(int)__epfd#>, <#(int)__op#>, <#(int)__fd#>, <#(struct epoll_event*)__event#>)\ epoll_wait(<#(int)__epfd#>, <#(struct epoll_event*)__events#>, <#(int)__maxevents#>, <#(int)__timeout#>) //epoll-nonblocking LT #include <stdio.h> #include <stdlib.h> #include <sys/epoll.h> #include <netinet/in.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #define READ_BUF_SIZE 20*1024*1024 #define WRITE_BUF_SIZE 20*1024*1024 #define CHUNCK_SIZE 2*1024*1024 #define KEEP_ALIVE 0 #define MAX_EVENTS 2048 struct epoll_fd_state { char readbuf[READ_BUF_SIZE]; char writebuf[WRITE_BUF_SIZE]; ssize_t readlen; ssize_t write_pos; ssize_t write_upto; int writing; }; void run(); int main(int argc, char *argv[]) { run(); return 0; } struct epoll_fd_state *alloc_epoll_fd_state() { struct epoll_fd_state *p = (struct epoll_fd_state *)malloc(sizeof(struct epoll_fd_state)); if (!p){ perror("error alloc_epoll_fd_state"); return NULL; } p->readlen = p->write_upto = p->write_pos = p->writing = 0; return p; } void free_epoll_fd_state(struct epoll_fd_state *p) { free(p); p = NULL; } //handle read event int do_read(struct epoll_event ev, struct epoll_fd_state *state) { ssize_t result; char buf[CHUNCK_SIZE]; while(1){ result = recv(ev.data.fd, buf, sizeof(buf), 0); if (result<=0) break; int i; for (i = 0; i < result; ++i) { if (state->readlen < sizeof(state->readbuf)){ state->readbuf[state->readlen++] = buf[i]; } printf("%c",buf[i]); fflush(stdout); //read until '\n' /* * todo: handle the readbuffer for http * */ /*if (buf[i]=='\n'){ state->writing = 1; state->written_upto = state->readlen; pfd->events = POLLOUT;//register write event break; }*/ } } //change state to write state->writing = 1; state->write_upto = state->readlen; printf("readlen result:%d\n",(int)result); fflush(stdout); if (result==0) return 1; if (result<0){ if (errno== EAGAIN) return 0; else return -1; } return 0; } //handle write event int do_write(struct epoll_event ev, struct epoll_fd_state *state) { ssize_t result; while (state->write_pos < state->write_upto) { result = send(ev.data.fd, state->readbuf + state->write_pos, CHUNCK_SIZE, 0); if (result <= 0) break; state->write_pos += result; } if (state->write_pos == state->write_upto) state->write_pos = state->write_upto = state->readlen = 0; state->writing = 0; printf("writelen result:%d",(int)result); fflush(stdout); if (result == 0) return 1; if (result < 0) { if (errno == EAGAIN) return 0; else return -1; } return 0; } int create_server_socket() { int fd; int reuse = 1; if ((fd= socket(AF_INET, SOCK_STREAM, 0))<0){ perror("error create socket"); return fd; } if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse))<0){ perror("error setsockopt"); } return fd; } void set_socket_nonblocking(int fd) { if ((fd, F_SETFL, O_NONBLOCK)<0){ perror("error set nonblocking"); exit(EXIT_FAILURE); } } void do_epoll(int serverfd) { int epollfd = epoll_create(2048); if (epollfd<0){ perror("error epoll_create"); exit(EXIT_FAILURE); } struct epoll_event ev; ev.data.fd = serverfd; ev.events = EPOLLIN; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, serverfd, &ev)<0){ perror("error epoll_ctl"); exit(EXIT_FAILURE); } struct epoll_event events[MAX_EVENTS]; struct epoll_fd_state *fds_state[MAX_EVENTS]; int epoll_ret; int clientfd; //epoll loop while(1){ epoll_ret = epoll_wait(epollfd, events, MAX_EVENTS, 0); if (epoll_ret<0){ perror("error epoll_wait"); exit(EXIT_FAILURE); } int i,j; //check writing state for (j = 0; j < epoll_ret; ++j) { if (events[j].data.fd!=serverfd && fds_state[j]&&fds_state[j]->writing==1){ /*printf("write ready:%d",events[j].data.fd); fflush(stdout);*/ ev.data.fd = events[j].data.fd; ev.events = EPOLLOUT; if (epoll_ctl(epollfd, EPOLL_CTL_MOD, events[j].data.fd, &ev)<0){ perror("error epoll_ctl add epollout"); exit(EXIT_FAILURE); } } } //handle server and client sock events for (i = 0; i < epoll_ret; ++i) { int flag = 0; //server socket reay? if (events[i].data.fd==serverfd){ struct sockaddr_in client; socklen_t slen = sizeof(client); if ((events[i].events& EPOLLIN) == EPOLLIN){ clientfd = accept(events[i].data.fd, (struct sockaddr *)&client, &slen); if (clientfd<0){ perror("error accept"); exit(EXIT_FAILURE); } set_socket_nonblocking(clientfd); ev.data.fd = clientfd; ev.events = EPOLLIN; if(epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &ev)<0){ perror("error epoll_ctl client"); exit(EXIT_FAILURE); } struct epoll_fd_state *temp = alloc_epoll_fd_state(); if(!temp){ exit(EXIT_FAILURE); } fds_state[i] = temp; } }else{ if ((events[i].events& EPOLLIN)== EPOLLIN){ fprintf(stdout, "in do_read\n"); fflush(stdout); flag = do_read(events[i], fds_state[i]); printf("read flag:%d\n",flag); fflush(stdout); } if (flag==0&&((events[i].events==EPOLLOUT)== EPOLLOUT)){ fprintf(stdout, "in do_write\n"); fflush(stdout); flag = do_write(events[i], fds_state[i]); if (!KEEP_ALIVE){ free_epoll_fd_state(fds_state[i]); fds_state[i] = NULL; if(epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, events)<0){ perror("error epoll_ctl delete event"); exit(EXIT_FAILURE); } close(events[i].data.fd); } } } if (flag){ fprintf(stdout, "in error handle\n"); printf("read error flag:%d\n",flag); fflush(stdout); free_epoll_fd_state(fds_state[i]); fds_state[i] = NULL; close(events[i].data.fd); } } } } void run() { struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_addr.s_addr = 0; sin.sin_port = htons(8000); int serverfd = create_server_socket(); set_socket_nonblocking(serverfd); if (serverfd<0){ exit(EXIT_FAILURE); } if (bind(serverfd, (struct sockaddr *)&sin, sizeof(sin))<0){ perror("error bind"); exit(EXIT_FAILURE); } if (listen(serverfd, 20)<0){ perror("error listen"); exit(EXIT_FAILURE); } do_epoll(serverfd); }
关于Linux 32位内存下的内存空间布局,可以参考这篇博文Linux下C程序进程地址空间局关于源代码中各种数据类型/代码在elf格式文件以及进程空间中所处的段,在x86_64下和i386下是类似的,本文主要关注vm.legacy_va_layout以及kernel.randomize_va_space参数影响下的进程空间内存宏观布局。 情形一: vm_legacy_va_layout=1 kernel.randomize_va_space=0 此种情况下采用传统内存布局方式,不开启随机化 cat 程序的内存布局 可以看出: 代码段:0x400000–> 数据段 堆:向上增长 2aaaaaaab000–> 栈:7ffffffde000<–7ffffffff000 系统调用:ffffffffff600000-ffffffffff601000 你可以试一下其他程序,在kernel.randomize_va_space=0时堆起点是不变的 情形二: vm_legacy_va_layout=0 kernel.randomize_va_space=0 现在默认内存布局,不随机化 可以看出: 代码段:0x400000–> 数据段 堆:向下增长 <–7ffff7fff000 栈:7ffffffde000<–7ffffffff000 系统调用:ffffffffff600000-ffffffffff601000 情形三: vm_legacy_va_layout=0 kernel.randomize_va_space=2 //ubuntu 14.04默认值 使用现在默认布局,随机化 对比两次启动的cat程序,其内存布局堆的起点是变化的,这从一定程度上防止了缓冲区溢出攻击。 情形四: vm_legacy_va_layout=1 kernel.randomize_va_space=2 //ubuntu 14.04默认值 与情形三类似,不再赘述