Android 12 init(6) Subcontext进程工作过程分析

news/2024/9/23 4:28:43/

文章托管在gitee上 Android Notes , 同步csdn
本文基于Android12 分析

概述

在init启动过程中,会启动一个subcontext进程,通常与init有着不一样的 secontext 以及 mount namespace。该进程用来接收来自init的命令,用来执行某些操作,这些操作是在 subcontext 的secontext 和 mount namespace 下进行。通过ps命令看看init及subcontext进程信息

# ps -AZ | grep init                                                                                                            
u:r:init:s0                    root             1     0 10904472  3956 do_epoll_wait       0 S init   # 这个是 init 进程
u:r:vendor_init:s0             root           166     1 10780496  1924 do_sys_poll         0 S init   # 这个是 subcontext 进程

Subcontext 进程启动与初始化

Subcontext 进程是在init启动第二阶段进行启动和初始化的,在SecondStageMain函数中调用InitializeSubcontext去完成相关操作。

SecondStageMain

int SecondStageMain(int argc, char** argv) {...const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap(); // 内置函数表Action::set_function_map(&function_map); // 设置 Action 需要的函数映射表...if (!SetupMountNamespaces()) { // 初始化 Mount NamespacesPLOG(FATAL) << "SetupMountNamespaces failed";}InitializeSubcontext(); // 初始化SubcontextActionManager& am = ActionManager::GetInstance(); // 创建action管理器ServiceList& sm = ServiceList::GetInstance(); // 创建服务管理列表LoadBootScripts(am, sm); // 加载并解析启动脚本...
}

SetupMountNamespaces

  • 如果apex可更新并且不是recovery模式,则会创建一个新的mount namespace,被记为default,而原始的则称为 bootstrap
  • 否则,default 和 bootstrap 的mount namespace是同一个
bool SetupMountNamespaces() {// Set the propagation type of / as shared so that any mounting event (e.g.// /data) is by default visible to all processes. When private mounting is// needed for /foo/bar, then we will make /foo/bar as a mount point (by// bind-mounting by to itself) and set the propagation type of the mount// point to private.if (!ChangeMount("/", MS_SHARED | MS_REC)) return false; // 将根 / 挂载为共享// /apex is a private mountpoint to give different sets of APEXes for// the bootstrap and default mount namespaces. The processes running with// the bootstrap namespace get APEXes from the read-only partition.if (!(ChangeMount("/apex", MS_PRIVATE))) return false;  // 将/apex 挂载为私有// /linkerconfig is a private mountpoint to give a different linker configuration// based on the mount namespace. Subdirectory will be bind-mounted based on current mount// namespaceif (!(ChangeMount("/linkerconfig", MS_PRIVATE))) return false; // 将/linkerconfig 挂载为私有...// 保存当前的mount namespace 为 bootstrapbootstrap_ns_fd.reset(OpenMountNamespace());bootstrap_ns_id = GetMountNamespaceId();// When APEXes are updatable (e.g. not-flattened), we create separate mount// namespaces for processes that are started before and after the APEX is// activated by apexd. In the namespace for pre-apexd processes, small// number of essential APEXes (e.g. com.android.runtime) are activated.// In the namespace for post-apexd processes, all APEXes are activated.bool success = true;if (IsApexUpdatable() && !IsRecoveryMode()) { // 如果apex可更新并且不是recovery模式// Creating a new namespace by cloning, saving, and switching back to// the original namespace.if (unshare(CLONE_NEWNS) == -1) { // 创建新的mount namespacePLOG(ERROR) << "Cannot create mount namespace";return false;}// 将新的 mount namespace 保存为 defaultdefault_ns_fd.reset(OpenMountNamespace());default_ns_id = GetMountNamespaceId();if (setns(bootstrap_ns_fd.get(), CLONE_NEWNS) == -1) { // 将init重新设置回 bootstrapPLOG(ERROR) << "Cannot switch back to bootstrap mount namespace";return false;}} else { // 否则 default 和 bootstrap 同 mount namespace// Otherwise, default == bootstrapdefault_ns_fd.reset(OpenMountNamespace());default_ns_id = GetMountNamespaceId();}
#ifdef ACTIVATE_FLATTENED_APEXsuccess &= ActivateFlattenedApexesIfPossible();
#endifLOG(INFO) << "SetupMountNamespaces done";return success;
}

关于 mount namespace 可参考文章 浅谈Linux Namespace机制(一)

InitializeSubcontext

创建subcontext进程,用于执行init交给其的一些任务(比如后面执行的某些Command),通过socket与init进行通信

  • 在Android P 开始才创建 subcontext
  • 使用的secontext是 kVendorContext, 即 u:r:vendor_init:s0
/// @system/core/init/subcontext.cpp
void InitializeSubcontext() {if (SelinuxGetVendorAndroidVersion() >= __ANDROID_API_P__) { // android s api 31 > android p api 28subcontext.reset( // 创建Subcontext, path_prefixes为"/vendor", "/odm"。kVendorContext u:r:vendor_init:s0new Subcontext(std::vector<std::string>{"/vendor", "/odm"}, kVendorContext));}
}/// @system/core/init/subcontext.h
Subcontext(std::vector<std::string> path_prefixes, std::string context, bool host = false): path_prefixes_(std::move(path_prefixes)), context_(std::move(context)), pid_(0) {if (!host) { /// host默认为falseFork(); // 创建subcontext进程}
}

Subcontext::Fork

  • 创建Socket pair,用于subcontext与init进行通信
  • fork() 创建子进程 subcontext
  • subcontext通过setexeccon设置安全上下文,通过setns 设置为默认mount space
  • 通过execv执行/system/bin/init 进入subcontext业务逻辑
/// @system/core/init/subcontext.cpp
void Subcontext::Fork() {unique_fd subcontext_socket;// 创建 socket对,用于与init进行通信if (!Socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, &socket_, &subcontext_socket)) {LOG(FATAL) << "Could not create socket pair to communicate to subcontext";return;}auto result = fork(); // 创建子进程 subcontextif (result == -1) {LOG(FATAL) << "Could not fork subcontext";} else if (result == 0) { // subcontext 进程socket_.reset(); // 关闭init的 fd// We explicitly do not use O_CLOEXEC here, such that we can reference this FD by number// in the subcontext process after we exec.int child_fd = dup(subcontext_socket);  // NOLINT(android-cloexec-dup)if (child_fd < 0) {PLOG(FATAL) << "Could not dup child_fd";}// We don't switch contexts if we're running the unit tests.  We don't use std::optional,// since we still need a real context string to pass to the builtin functions.if (context_ != kTestContext) {if (setexeccon(context_.c_str()) < 0) { // 设置安全上下文 Set exec security context for the next execvePLOG(FATAL) << "Could not set execcon for '" << context_ << "'";}}
#if defined(__ANDROID__)// subcontext init runs in "default" mount namespace// so that it can access /apex/*if (auto result = SwitchToMountNamespaceIfNeeded(NS_DEFAULT); !result.ok()) {// 切换到 default mount namespaceLOG(FATAL) << "Could not switch to \"default\" mount namespace: " << result.error();}
#endifauto init_path = GetExecutablePath(); // /system/bin/initauto child_fd_string = std::to_string(child_fd);const char* args[] = {init_path.c_str(), "subcontext", context_.c_str(), // 注意此处传入的参数 subcontextchild_fd_string.c_str(), nullptr};execv(init_path.data(), const_cast<char**>(args)); // execv 执行initPLOG(FATAL) << "Could not execv subcontext init";} else { // init 进程subcontext_socket.reset();  // 关闭subcontext的 fdpid_ = result;LOG(INFO) << "Forked subcontext for '" << context_ << "' with pid " << pid_;}
}

SwitchToMountNamespaceIfNeeded

/// @system/core/init/mount_namespace.cpp
Result<void> SwitchToMountNamespaceIfNeeded(MountNamespace target_mount_namespace) {if (IsRecoveryMode() || !IsApexUpdatable()) {// we don't have multiple namespaces in recovery mode or if apex is not updatablereturn {};}const auto& ns_id = target_mount_namespace == NS_BOOTSTRAP ? bootstrap_ns_id : default_ns_id;const auto& ns_fd = target_mount_namespace == NS_BOOTSTRAP ? bootstrap_ns_fd : default_ns_fd;const auto& ns_name = target_mount_namespace == NS_BOOTSTRAP ? "bootstrap" : "default";// 读取link /proc/self/ns/mnt// lrwxrwxrwx 1 root root 0 2022-12-16 15:30 /proc/164/ns/mnt -> mnt:[4026532713]if (ns_id != GetMountNamespaceId() && ns_fd.get() != -1) {if (setns(ns_fd.get(), CLONE_NEWNS) == -1) { // 调用 setns 设置mount namespacereturn ErrnoError() << "Failed to switch to " << ns_name << " mount namespace.";}}return {};
}

subcontext入口在init main方法中,通过参数可以知道启动的是哪个,对于subcontext则会调用SubcontextMain函数

/// @system/core/init/main.cpp
int main(int argc, char** argv) {...if (argc > 1) {if (!strcmp(argv[1], "subcontext")) { // 此处。 subcontext 子进程入口android::base::InitLogging(argv, &android::base::KernelLogger);const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();return SubcontextMain(argc, argv, &function_map);  // 调用 SubcontextMain}if (!strcmp(argv[1], "selinux_setup")) {// selinux初始化阶段return SetupSelinux(argv);}if (!strcmp(argv[1], "second_stage")) {// 启动第二阶段return SecondStageMain(argc, argv);}}return FirstStageMain(argc, argv); // 启动第一阶段
}

SubcontextMain

/// @system/core/init/subcontext.cpp
int SubcontextMain(int argc, char** argv, const BuiltinFunctionMap* function_map) {if (argc < 4) LOG(FATAL) << "Fewer than 4 args specified to subcontext (" << argc << ")";auto context = std::string(argv[2]); // 默认是 kVendorContext,u:r:vendor_init:s0auto init_fd = std::atoi(argv[3]); // socket fdSelabelInitialize(); // selable 初始化// 设置关机命令lambdatrigger_shutdown = [](const std::string& command) { shutdown_command = command; };// 创建 SubcontextProcess 对象auto subcontext_process = SubcontextProcess(function_map, context, init_fd);// Restore prio before main loopsetpriority(PRIO_PROCESS, 0, 0);subcontext_process.MainLoop(); // 调用其 MainLoop 函数return 0;
}

SubcontextProcess::MainLoop

进入主循环,等待init消息并处理相关请求。

  • poll等待读事件的发生,通常是init发送相关命令请求
  • ReadMessage(init_fd_) 读取来自 init 的消息
  • subcontext_command.ParseFromString 解析init消息
  • 根据Command类别执行不同的操作
    • kExecuteCommand 执行指定的命令
    • kExpandArgsCommand 展开给定的参数。
  • SendMessage回复init执行结果
/// @system/core/init/subcontext.cpp
void SubcontextProcess::MainLoop() {pollfd ufd[1];ufd[0].events = POLLIN;ufd[0].fd = init_fd_; // init socket fd,监听相关事件while (true) {ufd[0].revents = 0;int nr = TEMP_FAILURE_RETRY(poll(ufd, arraysize(ufd), -1)); // poll等待事件发生if (nr == 0) continue;if (nr < 0) {PLOG(FATAL) << "poll() of subcontext socket failed, continuing";}auto init_message = ReadMessage(init_fd_); // 读取来自 init 的消息if (!init_message.ok()) {if (init_message.error().code() == 0) {// If the init file descriptor was closed, let's exit quietly. If// this was accidental, init will restart us. If init died, this// avoids calling abort(3) unnecessarily.return;}LOG(FATAL) << "Could not read message from init: " << init_message.error();}auto subcontext_command = SubcontextCommand();if (!subcontext_command.ParseFromString(*init_message)) { // 解析init消息LOG(FATAL) << "Unable to parse message from init";}auto reply = SubcontextReply();switch (subcontext_command.command_case()) {case SubcontextCommand::kExecuteCommand: { // 执行命令RunCommand(subcontext_command.execute_command(), &reply);break;}case SubcontextCommand::kExpandArgsCommand: { // 展开参数ExpandArgs(subcontext_command.expand_args_command(), &reply);break;}default:LOG(FATAL) << "Unknown message type from init: "<< subcontext_command.command_case();}if (!shutdown_command.empty()) { // shutdown命令不为空,回写shutdown消息reply.set_trigger_shutdown(shutdown_command);shutdown_command.clear();}// 发送回复消息到 initif (auto result = SendMessage(init_fd_, reply); !result.ok()) {LOG(FATAL) << "Failed to send message to init: " << result.error();}}
}

ReadMessage

从socket 中读取消息, 存储到 string

/// @system/core/init/proto_utils.h
inline Result<std::string> ReadMessage(int socket) {char buffer[kBufferSize] = {};auto result = TEMP_FAILURE_RETRY(recv(socket, buffer, sizeof(buffer), 0));if (result == 0) {return Error();} else if (result < 0) {return ErrnoError();}return std::string(buffer, result);
}

SendMessage

将消息序列化为string,然后通过socket发送

template <typename T>
Result<void> SendMessage(int socket, const T& message) {std::string message_string;if (!message.SerializeToString(&message_string)) {return Error() << "Unable to serialize message";}if (message_string.size() > kBufferSize) {return Error() << "Serialized message too long to send";}if (auto result =TEMP_FAILURE_RETRY(send(socket, message_string.c_str(), message_string.size(), 0));result != static_cast<long>(message_string.size())) {return ErrnoError() << "send() failed to send message contents";}return {};
}

SubcontextCommand、SubcontextReply

上面两个用来接收命令,发送反馈的类,在源码的实现是通过proto2。通过SubcontextCommand定义可知,它目前支持两种命令,execute_command和expand_args_command。

/// @system/core/init/subcontext.proto
syntax = "proto2";
option optimize_for = LITE_RUNTIME;message SubcontextCommand {message ExecuteCommand { repeated string args = 1; }message ExpandArgsCommand { repeated string args = 1; }oneof command {ExecuteCommand execute_command = 1;ExpandArgsCommand expand_args_command = 2;}
}message SubcontextReply {message Failure {optional string error_string = 1;optional int32 error_errno = 2;}message ExpandArgsReply { repeated string expanded_args = 1; }oneof reply {bool success = 1;Failure failure = 2;ExpandArgsReply expand_args_reply = 3;}optional string trigger_shutdown = 4;
}

关于proto2 请参考 快来看看Google出品的Protocol Buffer

init发送命令

init(2) rc脚本解析和事件执行流程 中,分析过Command的解析与执行。通常,每一个Command都在内置函数Map有对应的一行,如下所示。第四列true/false表示是否在subcontext中执行。

/// @system/core/init/builtins.cpp
static const BuiltinFunctionMap builtin_functions =
// 函数名, MapValue{ min_args,max_args,BuiltinFunctionMapValue{ run_in_subcontext, BuiltinFunction } }{"copy",                    {2,     2,    {true,   do_copy}}},{"copy_per_line",           {2,     2,    {true,   do_copy_per_line}}},{"domainname",              {1,     1,    {true,   do_domainname}}},{"enable",                  {1,     1,    {false,  do_enable}}},{"exec",                    {1,     kMax, {false,  do_exec}}},{"exec_background",         {1,     kMax, {false,  do_exec_background}}},{"exec_start",              {1,     1,    {false,  do_exec_start}}},

以copy命令为例,看看它的实现。当执行该命令时,会调用Command::InvokeFunc

Command::InvokeFunc

  • 当指定了不为空的subcontext
    • 若execute_in_subcontext_为true,则会直接调用subcontext->Execute
    • 否则会先subcontext->ExpandArgs通过subcontext进程对args进行膨胀处理(主要是将 ${prop_name} 解析成对应的属性值),然后在调用RunBuiltinFunction
  • 未指定subcontext则直接调用RunBuiltinFunction
/// @system/core/init/action.cpp
Result<void> Command::InvokeFunc(Subcontext* subcontext) const {if (subcontext) { // 指定了subcontextif (execute_in_subcontext_) { // 如果指定在 subcontext 下执行return subcontext->Execute(args_);}auto expanded_args = subcontext->ExpandArgs(args_);if (!expanded_args.ok()) { // 膨胀处理失败,则结束处理return expanded_args.error();}return RunBuiltinFunction(func_, *expanded_args, subcontext->context());}return RunBuiltinFunction(func_, args_, kInitContext);
}

copy指定了在subcontext执行,因此会调用subcontext->Execute

Subcontext::Execute

Result<void> Subcontext::Execute(const std::vector<std::string>& args) {auto subcontext_command = SubcontextCommand();std::copy( // 填充参数到命令。 注意此处的mutable_execute_command,表示使用的消息是 execute_commandargs.begin(), args.end(),RepeatedPtrFieldBackInserter(subcontext_command.mutable_execute_command()->mutable_args()));auto subcontext_reply = TransmitMessage(subcontext_command);// 传递消息并接收回复if (!subcontext_reply.ok()) {return subcontext_reply.error();}if (subcontext_reply->reply_case() == SubcontextReply::kFailure) { // 处理失败auto& failure = subcontext_reply->failure();return ResultError(failure.error_string(), failure.error_errno());}if (subcontext_reply->reply_case() != SubcontextReply::kSuccess) {return Error() << "Unexpected message type from subcontext: "<< subcontext_reply->reply_case();}return {};
}

Subcontext::TransmitMessage

Result<SubcontextReply> Subcontext::TransmitMessage(const SubcontextCommand& subcontext_command) {if (auto result = SendMessage(socket_, subcontext_command); !result.ok()) { // 发送消息,和上面一样 通过socket发消息Restart(); // 发送失败则重启 subcontext 进程return ErrnoError() << "Failed to send message to subcontext";}auto subcontext_message = ReadMessage(socket_); // 接收处理结果if (!subcontext_message.ok()) {Restart();return Error() << "Failed to receive result from subcontext: " << subcontext_message.error();}auto subcontext_reply = SubcontextReply{};if (!subcontext_reply.ParseFromString(*subcontext_message)) { // 解析结果Restart();return Error() << "Unable to parse message from subcontext";}if (subcontext_reply.has_trigger_shutdown()) { // 若包含关机信息则触发执行trigger_shutdown(subcontext_reply.trigger_shutdown());}return subcontext_reply;
}

从上面分析可知,init发送了一个类型为execute_command的消息到 subcontext,在SubcontextProcess::MainLoop中收到请求后,根据类型执行相关动作,即调用RunCommand

// MainLoop ...
switch (subcontext_command.command_case()) {case SubcontextCommand::kExecuteCommand: { // 执行命令RunCommand(subcontext_command.execute_command(), &reply);break;}...
}
...
// 发送回复消息到 init
if (auto result = SendMessage(init_fd_, reply); !result.ok()) {LOG(FATAL) << "Failed to send message to init: " << result.error();
}

SubcontextProcess::RunCommand

void SubcontextProcess::RunCommand(const SubcontextCommand::ExecuteCommand& execute_command,SubcontextReply* reply) const {// Need to use ArraySplice instead of this code.auto args = std::vector<std::string>();for (const auto& string : execute_command.args()) {args.emplace_back(string);}auto map_result = function_map_->Find(args); // 从函数Map寻找对应的函数Result<void> result;if (!map_result.ok()) {result = Error() << "Cannot find command: " << map_result.error();} else {result = RunBuiltinFunction(map_result->function, args, context_); // 执行内置函数}if (result.ok()) {reply->set_success(true);} else {auto* failure = reply->mutable_failure();failure->set_error_string(result.error().message());failure->set_error_errno(result.error().code());}
}

RunBuiltinFunction

/// @system/core/init/action.cpp
Result<void> RunBuiltinFunction(const BuiltinFunction& function,const std::vector<std::string>& args, const std::string& context) {auto builtin_arguments = BuiltinArguments(context); // 函数参数builtin_arguments.args.resize(args.size());builtin_arguments.args[0] = args[0];for (std::size_t i = 1; i < args.size(); ++i) {auto expanded_arg = ExpandProps(args[i]); // 展开属性参数if (!expanded_arg.ok()) {return expanded_arg.error();}builtin_arguments.args[i] = std::move(*expanded_arg);}// 调用函数,copy 对应的函数是 do_copyreturn function(builtin_arguments);
}

ExpandProps

将属性参数展开为对应的值

  • 属性的形式可以是 $x.y 或者 ${x.y}。但 $x.y 这种形式从R开始不再支持
  • 连续两个$被解析为$。{}内部的内嵌属性不再解析,如 ${foo.${bar}}
  • 可以指定默认值,当属性值为空时返回。如 ${x.y:-default} , default即默认值
/// @system/core/init/util.cpp
Result<std::string> ExpandProps(const std::string& src) {const char* src_ptr = src.c_str();std::string dst;/* - variables can either be $x.y or ${x.y}, in case they are only part*   of the string.* - will accept $$ as a literal $.* - no nested property expansion, i.e. ${foo.${bar}} is not supported,*   bad things will happen* - ${x.y:-default} will return default value if property empty.*/while (*src_ptr) {const char* c;c = strchr(src_ptr, '$'); // 寻找字符串中的 $if (!c) { // 不包含$则不需要解析dst.append(src_ptr);return dst;}dst.append(src_ptr, c);c++;if (*c == '$') { // 检测到连续两个$$,则识别为$dst.push_back(*(c++));src_ptr = c;continue;} else if (*c == '\0') { // 后面紧跟结束符则返回return dst;}std::string prop_name;std::string def_val;if (*c == '{') { // 如果后面有{ ,则需要识别为 ${x.y} 这种c++;const char* end = strchr(c, '}');if (!end) { // { } 要成对匹配// failed to find closing brace, abort.return Error() << "unexpected end of string in '" << src << "', looking for }";}prop_name = std::string(c, end);c = end + 1;size_t def = prop_name.find(":-"); // 解析默认值。:-后面是默认值,前面是keyif (def < prop_name.size()) {def_val = prop_name.substr(def + 2);prop_name = prop_name.substr(0, def);}} else { // 否则$后面都是key。prop_name = c;if (SelinuxGetVendorAndroidVersion() >= __ANDROID_API_R__) { // 大于 R 版本不再支持这种语法return Error() << "using deprecated syntax for specifying property '" << c<< "', use ${name} instead";} else {LOG(ERROR) << "using deprecated syntax for specifying property '" << c<< "', use ${name} instead";}c += prop_name.size();}if (prop_name.empty()) {return Error() << "invalid zero-length property name in '" << src << "'";}// 获取属性值std::string prop_val = android::base::GetProperty(prop_name, "");if (prop_val.empty()) { // 没有获取属性值if (def_val.empty()) { // 没有指定默认值return Error() << "property '" << prop_name << "' doesn't exist while expanding '"<< src << "'";}prop_val = def_val; // 使用默认值}dst.append(prop_val);src_ptr = c;}return dst;
}

Command是copy情景下,function(builtin_arguments) 调用的函数是 do_copy

do_copy

/// @system/core/init/builtins.cpp
static Result<void> do_copy(const BuiltinArguments& args) {auto file_contents = ReadFile(args[1]);if (!file_contents.ok()) {return Error() << "Could not read input file '" << args[1] << "': " << file_contents.error();}if (auto result = WriteFile(args[2], *file_contents); !result.ok()) {return Error() << "Could not write to output file '" << args[2] << "': " << result.error();}return {};
}

subcontext在执行完命令后,会通过SendMessage向init发送回复信息。

总结

  • init启动过程第二阶段启动subcontext进程,创建socket pair用来与subcontext进程通信
  • subcontext进程初始化,设置 secontext 及 mount namespace
  • subcontext在MainLoop中通过poll等待init的消息
  • init执行某些命令时,通过socket向subcontext发送消息,让其执行某些操作
  • subcontext收到消息后,根据消息类型执行相关操作,并回写执行结果

http://www.ppmy.cn/news/4531.html

相关文章

基于 Spring Cloud 的微服务脚手架

基于 Spring Cloud 的微服务脚手架 作者&#xff1a; Grey 原文地址&#xff1a; 博客园&#xff1a;基于 Spring Cloud 的微服务脚手架 CSDN&#xff1a;基于 Spring Cloud 的微服务脚手架 本文主要介绍了基于 Spring Cloud Finchley 和 Spring Boot 2.0.x 版本的微服务脚…

皮带撕裂检测系统 yolo深度学习模型

皮带撕裂检测系统通过Python基于YOLOv7网络机器学习架构模型&#xff0c;对现场皮带撕裂实时分析检测。我们使用YOLO(你只看一次)算法进行对象检测。YOLO是一个聪明的卷积神经网络(CNN)&#xff0c;用于实时进行目标检测。该算法将单个神经网络应用于完整的图像&#xff0c;然后…

【华为OD机试真题2023 JAVA】寻找符合要求的最长子串

华为OD机试真题,2023年度机试题库全覆盖,刷题指南点这里 寻找符合要求的最长子串 知识点双指针 时间限制:1s 空间限制:256MB 限定语言:不限 题目描述: 给定一个字符串 s ,找出这样一个子串: 1)该子串中的任意一个字符最多出现2次; 2)该子串不包含指定某个字符; 请…

Promise学习

01_准备_函数对象VS实例对象.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>准备_函数对象 VS 实例对象</title> </head> <body> <script>/*函数对象 VS 实例对象1. 函…

jdk11新特性——更简化的编译运行程序

目录一、概述二、注意事项三、注意事项1——代码示例3.1、示例13.2、示例23.3、示例3四、注意事项2——代码示例4.1、示例14.2、示例2一、概述 JEP 330 : 增强java启动器支持运行单个java源代码文件的程序。 在我们的认知里面&#xff0c;要运行一个 Java 源代码必须先编译&am…

基于Java毕业设计新疆旅游专列订票系统源码+系统+mysql+lw文档+部署软件

基于Java毕业设计新疆旅游专列订票系统源码系统mysqllw文档部署软件 基于Java毕业设计新疆旅游专列订票系统源码系统mysqllw文档部署软件本源码技术栈&#xff1a; 项目架构&#xff1a;B/S架构 开发语言&#xff1a;Java语言 开发软件&#xff1a;idea eclipse 前端技术&a…

能够让你装逼的10个Python小技巧

列表推导式 你有一个list&#xff1a; bag [1, 2, 3, 4, 5] 现在你想让所有元素翻倍&#xff0c;让它看起来是这个样子&#xff1a; [2, 4, 6, 8, 10] 大多初学者&#xff0c;根据之前语言的经验会大概这样来做 bag [1, 2, 3, 4, 5] for i in range(len(bag)): bag[i] ba…

STM32 10个工程实战前言

从今年2022年元旦开通博客到现在基本接近一年了&#xff0c;真的会感到感觉时间飞逝&#xff0c;尤其当你全身心地投入一件工作上时&#xff0c;在FPGA基础篇和FPGA 20个经理例程篇后&#xff0c;又准备了STM32基础篇和STM32 10个工程实战篇&#xff0c;前两者即将收尾&#xf…