Android11 DNS解析流程

Android11 DNS解析

1. DNS解析概念

​ DNS的全称是domain name system,即域名系统。主要目的是将域名解析为IP地址,域名是方便用户记忆,但网络传输中源目地址使用IP地址来进行标识的,所以Android中的网络应用程序在发起http请求之前必然要经历DNS解析过程。

2. Android11的DNS解析过程

2.1 Inet6AddressImpl.lookupHostByName

Android app不管用什么网络框架,dns解析都会走到Inet6AddressImpl.lookupHostByName方法,我们就以这个方法为入口开始看代码:

/**
 * Resolves a hostname to its IP addresses using a cache.
 *
 * @param host the hostname to resolve.
 * @param netId the network to perform resolution upon.
 * @return the IP addresses of the host.
 */
private static InetAddress[] lookupHostByName(String host, int netId)
        throws UnknownHostException {
    //参数host代表dns解析的域名,netId代表使用的网络,如wifi,数据网络等等都是可以自己选择的
    BlockGuard.getThreadPolicy().onNetwork();
    // 缓存中是否已经有了,这里的缓存只是应用进程的缓存(TTL为2s),并不是DNS的TTL缓存
        ...
    try {
        StructAddrinfo hints = new StructAddrinfo();
        //不指定协议族,返回包括 IPv4 和 IPv6 的所有地址
        hints.ai_flags = AI_ADDRCONFIG;
        hints.ai_family = AF_UNSPEC;
        //指定socket类型为TCP
        hints.ai_socktype = SOCK_STREAM;
        //真正进行DNS解析的方法
        InetAddress[] addresses = Libcore.os.android_getaddrinfo(host, hints, netId);
        ...
        return addresses;
    } catch (GaiException gaiException) {
          ...
        }
        // 常见的无网报错
        String detailMessage = "Unable to resolve host \"" + host + "\": " + Libcore.os.gai_strerror(gaiException.error);
        addressCache.putUnknownHost(host, netId, detailMessage);
        throw gaiException.rethrowAsUnknownHostException(detailMessage);
    }
}

2.2 BlockGuardOs.android_getaddrinfo

@Override public InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException {
    // With AI_NUMERICHOST flag set, the node must a numerical network address, therefore no
    // host address lookups will be performed. In this case, it is fine to perform on main
    // thread.
    boolean isNumericHost = (hints.ai_flags & AI_NUMERICHOST) != 0;
    if (!isNumericHost) {
        BlockGuard.getThreadPolicy().onNetwork();
    }
    return super.android_getaddrinfo(node, hints, netId);
}

2.3 Linux.android_getaddrinfo

public native InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException;

这里是一个native方法,对应的native层函数是libcore_io_Linux.Linux_android_getaddrinfo

2.4 libcore_io_Linux.Linux_android_getaddrinfo

static jobjectArray Linux_android_getaddrinfo(JNIEnv* env, jobject, jstring javaNode,
          jobject javaHints, jint netId) {
      ScopedUtfChars node(env, javaNode);
      if (node.c_str() == NULL) {
          return NULL;
      }
  
      static jfieldID flagsFid = env->GetFieldID(JniConstants::GetStructAddrinfoClass(env), "ai_flags", "I");
      static jfieldID familyFid = env->GetFieldID(JniConstants::GetStructAddrinfoClass(env), "ai_family", "I");
      static jfieldID socktypeFid = env->GetFieldID(JniConstants::GetStructAddrinfoClass(env), "ai_socktype", "I");
      static jfieldID protocolFid = env->GetFieldID(JniConstants::GetStructAddrinfoClass(env), "ai_protocol", "I");
  
      addrinfo hints;
      memset(&hints, 0, sizeof(hints));
      //使用java层传递的协议项,socket设置等构造addrinfo对象
      hints.ai_flags = env->GetIntField(javaHints, flagsFid);
      hints.ai_family = env->GetIntField(javaHints, familyFid);
      hints.ai_socktype = env->GetIntField(javaHints, socktypeFid);
      hints.ai_protocol = env->GetIntField(javaHints, protocolFid);
  
      addrinfo* addressList = NULL;
      errno = 0;
      //addressList接收最终的DNS解析结果,这里传的server name是null
      int rc = android_getaddrinfofornet(node.c_str(), NULL, &hints, netId, 0, &addressList);
      //省略后续构造java层对象返回的代码
      ...
      return result;
  }

android_getaddrinfofornet这个函数会调到getaddrinfo.c中:

2.5 getaddrinfo.android_getaddrinfofornet

getaddrinfo.c中经过一系列内部调用,对数据转换,封装之后最终调到android_getaddrinfo_proxy这个函数:

2.6 getaddrinfo.android_getaddrinfo_proxy

#if defined(__ANDROID__)
// Returns 0 on success, else returns on error.
static int
android_getaddrinfo_proxy(
    const char *hostname, const char *servname,
    const struct addrinfo *hints, struct addrinfo **res, unsigned netid)
{
   int success = 0;
   //省略一些判断
    ...
   //这是核心方法,这里会打开一个socket文件
   FILE* proxy = fdopen(__netdClientDispatch.dnsOpenProxy(), "r+");
   if (proxy == NULL) {
      return EAI_SYSTEM;
   }
   netid = __netdClientDispatch.netIdForResolv(netid);

   //向socket发送请求,其实就是向socket对端写入字符串
   if (fprintf(proxy, "getaddrinfo %s %s %d %d %d %d %u",
          hostname == NULL ? "^" : hostname,
          servname == NULL ? "^" : servname,
          hints == NULL ? -1 : hints->ai_flags,
          hints == NULL ? -1 : hints->ai_family,
          hints == NULL ? -1 : hints->ai_socktype,
          hints == NULL ? -1 : hints->ai_protocol,
          netid) < 0) {
      goto exit;
   }
   // literal NULL byte at end, required by FrameworkListener
   if (fputc(0, proxy) == EOF ||
       fflush(proxy) != 0) {
      goto exit;
   }

   char buf[4];
   // 将对端返回的消息放入buf
   if (fread(buf, 1, sizeof(buf), proxy) != sizeof(buf)) {
      goto exit;
   }

   int result_code = (int)strtol(buf, NULL, 10);
   // verify the code itself
   if (result_code != DnsProxyQueryResult) {
      fread(buf, 1, sizeof(buf), proxy);
      goto exit;
   }

   struct addrinfo* ai = NULL;
   struct addrinfo** nextres = res;
   //填充addrinfo数据,这是返回的DNS解析数据,代码省略了,不是重点
   ....
   return EAI_NODATA;
}
#endif

上述函数的,主要作用是打开一个socket描述符,然后向此socket写入一堆字符串,我们需要关注的是打开的是哪个socket?监听此socket的进程是哪个?对端如何处理写入的字符串?

首先来看__netdClientDispatch.dnsOpenProxy()是什么东西?

来看下面一段代码:

template <typename FunctionType>
static void netdClientInitFunction(void* handle, const char* symbol, FunctionType* function) {
    typedef void (*InitFunctionType)(FunctionType*);
    InitFunctionType initFunction = reinterpret_cast<InitFunctionType>(dlsym(handle, symbol));
    if (initFunction != nullptr) {
        initFunction(function);
    }
}

static void netdClientInitImpl() {
    // Prevent netd from looping back fwmarkd connections to itself. It would work, but it's
    // a deadlock hazard and unnecessary overhead for the resolver.
    if (getuid() == 0 && strcmp(basename(getprogname()), "netd") == 0) {
        async_safe_format_log(ANDROID_LOG_INFO, "netdClient",
                              "Skipping libnetd_client init since *we* are netd");
        return;
    }

    void* handle = dlopen("libnetd_client.so", RTLD_NOW);
    if (handle == nullptr) {
        // If the library is not available, it's not an error. We'll just use
        // default implementations of functions that it would've overridden.
        return;
    }

    netdClientInitFunction(handle, "netdClientInitAccept4", &__netdClientDispatch.accept4);
    netdClientInitFunction(handle, "netdClientInitConnect", &__netdClientDispatch.connect);
    netdClientInitFunction(handle, "netdClientInitSendmmsg", &__netdClientDispatch.sendmmsg);
    netdClientInitFunction(handle, "netdClientInitSendmsg", &__netdClientDispatch.sendmsg);
    netdClientInitFunction(handle, "netdClientInitSendto", &__netdClientDispatch.sendto);
    netdClientInitFunction(handle, "netdClientInitSocket", &__netdClientDispatch.socket);

    netdClientInitFunction(handle, "netdClientInitNetIdForResolv",
                           &__netdClientDispatch.netIdForResolv);
    netdClientInitFunction(handle, "netdClientInitDnsOpenProxy",
                           &__netdClientDispatch.dnsOpenProxy);
}

上面代码用到了dlopendlsym函数,dlopen打开的libnetd_client.so定义在netd进程中:

在这里插入图片描述

dlsym函数获取了libnetd_client.so中的netdClientInitDnsOpenProxy函数地址,其代码实现如下所示:

extern "C" void netdClientInitDnsOpenProxy(DnsOpenProxyType* function) {
    if (function) {
        *function = dns_open_proxy;
    }
}

&__netdClientDispatch.dnsOpenProxy地址被传入上述函数,作为函数指针指向了dns_open_proxy函数,所以最终我们知道了前面的fdopen函数打开的socket的是在dns_open_proxy函数中返回的

2.7 NetdClient.dns_open_proxy

int dns_open_proxy() {
    ....
    const auto socketFunc = libcSocket ? libcSocket : socket;
    int s = socketFunc(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (s == -1) {
        return -1;
    }
    const int one = 1;
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    
    static const struct sockaddr_un proxy_addr = {
            .sun_family = AF_UNIX,
            .sun_path = "/dev/socket/dnsproxyd",
    };

    const auto connectFunc = libcConnect ? libcConnect : connect;
    if (TEMP_FAILURE_RETRY(
                connectFunc(s, (const struct sockaddr*) &proxy_addr, sizeof(proxy_addr))) != 0) {
        // Store the errno for connect because we only care about why we can't connect to dnsproxyd
        int storedErrno = errno;
        close(s);
        errno = storedErrno;
        return -1;
    }

    return s;
}

上面代码一目了然,作为客户端打开/dev/socket/dnsproxyd并连接,回到getaddrinfo.android_getaddrinfo_proxy函数中,socket连接上之后,发送**“getaddrinfo %s %s %d %d %d %d %u”**字符串,流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MP8J1uYg-1686898811898)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230616144316150.png)]

接着需要知道监听/dev/socket/dnsproxyd的服务端是哪个进程?如何处理客户端的字符串?

这个socket的服务端在netd进程里面通过SocketListener监听的,所以DNS相关的请求字符串发给了netd:

在这里插入图片描述

接着看字符串的处理,服务端对这些客户端发送的字符处理方式是通过FrameworkListener注册的命令来处理的,getaddrinfo对应的是GetAddrInfoCmd,直接上代码:

2.8 DnsProxyListener::GetAddrInfoCmd::runCommand

DnsProxyListener::GetAddrInfoCmd::GetAddrInfoCmd() : FrameworkCommand("getaddrinfo") {}
int DnsProxyListener::GetAddrInfoCmd::runCommand(SocketClient *cli,
                                            int argc, char **argv) {
    //省略一些合法性判断
    ...
        
    
    addrinfo* hints = nullptr;
    int ai_flags = strtol(argv[3], nullptr, 10);
    int ai_family = strtol(argv[4], nullptr, 10);
    int ai_socktype = strtol(argv[5], nullptr, 10);
    int ai_protocol = strtol(argv[6], nullptr, 10);
    unsigned netId = strtoul(argv[7], nullptr, 10);
    const bool useLocalNameservers = checkAndClearUseLocalNameserversFlag(&netId);
    const uid_t uid = cli->getUid();
    const int pid = cli->getPid();
    
    android_net_context netcontext;
    //构造netcontext
    gResNetdCallbacks.get_network_context(netId, uid, &netcontext);
    
    if (ai_flags != -1 || ai_family != -1 ||
        ai_socktype != -1 || ai_protocol != -1) {
        //构造addrinfo
        hints = (addrinfo*) calloc(1, sizeof(addrinfo));
        hints->ai_flags = ai_flags;
        hints->ai_family = ai_family;
        hints->ai_socktype = ai_socktype;
        hints->ai_protocol = ai_protocol;
    }

    DnsProxyListener::GetAddrInfoHandler* handler =
            new DnsProxyListener::GetAddrInfoHandler(cli, name, service, hints, netcontext);
    tryThreadOrError(cli, handler);
    return 0;
}

2.9 DnsProxyListener::GetAddrInfoHandler::run

void DnsProxyListener::GetAddrInfoHandler::run() {
    LOG(DEBUG) << "GetAddrInfoHandler::run: {" << mNetContext.app_netid << " "
               << mNetContext.app_mark << " " << mNetContext.dns_netid << " "
               << mNetContext.dns_mark << " " << mNetContext.uid << " " << mNetContext.flags << "}";

     
    addrinfo* result = nullptr;
    Stopwatch s;
    maybeFixupNetContext(&mNetContext, mClient->getPid());
    const uid_t uid = mClient->getUid();
    int32_t rv = 0;
    NetworkDnsEventReported event;
    initDnsEvent(&event, mNetContext);
    if (queryLimiter.start(uid)) {
        if (evaluate_domain_name(mNetContext, mHost)) {
            //核心函数,result用于接收最后的结果
            rv = resolv_getaddrinfo(mHost, mService, mHints, &mNetContext, &result,
                                    &event);
        } else {
            rv = EAI_SYSTEM;
        }
        queryLimiter.finish(uid);
    } else {
        ...
    }
    //IPv4,IPv6地址转换,当 DNS 查询请求中的查询类型为 AAAA(IPv6 地址查询)且查询结果为空时
    //使用IPv4进行查询
    doDns64Synthesis(&rv, &result, &event);
    const int32_t latencyUs = saturate_cast<int32_t>(s.timeTakenUs());
    event.set_latency_micros(latencyUs);
    event.set_event_type(EVENT_GETADDRINFO);
    event.set_hints_ai_flags((mHints ? mHints->ai_flags : 0));

    bool success = true;
    if (rv) {
        // getaddrinfo failed
        success = !mClient->sendBinaryMsg(ResponseCode::DnsProxyOperationFailed, &rv, sizeof(rv));
    } else {
        //DNS解析成功
        success = !mClient->sendCode(ResponseCode::DnsProxyQueryResult);
        addrinfo* ai = result;
        while (ai && success) {
            //DNS解析数据返回给客户端
            success = sendBE32(mClient, 1) && sendaddrinfo(mClient, ai);
            ai = ai->ai_next;
        }
        success = success && sendBE32(mClient, 0);
    }
    ...

}

上面重点函数resolv_getaddrinfo,DNS解析核心函数:

2.10 getaddrinfo.resolv_getaddrinfo

int resolv_getaddrinfo(const char* _Nonnull hostname, const char* servname, const addrinfo* hints,
                       const android_net_context* _Nonnull netcontext, addrinfo** _Nonnull res,
                       NetworkDnsEventReported* _Nonnull event) {
    //省略一些判断
    ...

    addrinfo ai = hints ? *hints : addrinfo{};
    addrinfo sentinel = {};
    addrinfo* cur = &sentinel;
    //explore_options里面定义了一些条件,必须匹配才能进行解析
    for (const Explore& ex : explore_options) {
        
        if (ai.ai_family != ex.e_af) continue;

        if (!MATCH(ai.ai_socktype, ex.e_socktype, WILD_SOCKTYPE(ex))) continue;

        if (!MATCH(ai.ai_protocol, ex.e_protocol, WILD_PROTOCOL(ex))) continue;

        addrinfo tmp = ai;
        if (tmp.ai_socktype == ANY && ex.e_socktype != ANY) tmp.ai_socktype = ex.e_socktype;
        if (tmp.ai_protocol == ANY && ex.e_protocol != ANY) tmp.ai_protocol = ex.e_protocol;
        //核心函数
        error = explore_fqdn(&tmp, hostname, servname, &cur->ai_next, netcontext, event);

        while (cur->ai_next) cur = cur->ai_next;
    }

    // Propagate the last error from explore_fqdn(), but only when *all* attempts failed.
    if ((*res = sentinel.ai_next)) return 0;

    // TODO: consider removing freeaddrinfo.
    freeaddrinfo(sentinel.ai_next);
    *res = nullptr;
    return (error == 0) ? EAI_FAIL : error;
}

2.11 getaddrinfo.explore_fqdn

// FQDN hostname, DNS lookup
static int explore_fqdn(const addrinfo* pai, const char* hostname, const char* servname,
                        addrinfo** res, const android_net_context* netcontext,
                        NetworkDnsEventReported* event) {
    

    addrinfo* result = nullptr;
    int error = 0;

    // If the servname does not match socktype/protocol, return error code.
    if ((error = get_portmatch(pai, servname))) return error;

    if (!files_getaddrinfo(netcontext->dns_netid, hostname, pai, &result)) {
        ALOGE("djtang....files_getaddrinfo == false.");
        error = dns_getaddrinfo(hostname, pai, netcontext, &result, event);
    }
    if (error) {
        freeaddrinfo(result);
        return error;
    }

    for (addrinfo* cur = result; cur; cur = cur->ai_next) {
        // canonname should be filled already
        if ((error = get_port(cur, servname, 0))) {
            freeaddrinfo(result);
            return error;
        }
    }
    *res = result;
    return 0;
}

上面代码首先会去缓存文件/system/etc/hosts去查看是否已经有域名对应IP地址了,如果没有,再调dns_getaddrinfo函数获取,测试了一下dns解析成功之后也不会将解析结果写入/system/etc/hosts,关于files_getaddrinfo函数查缓存的细节就不展开了,不是重点,继续分析dns_getaddrinfo

2.12 getaddrinfo.dns_getaddrinfo

static int dns_getaddrinfo(const char* name, const addrinfo* pai,
                           const android_net_context* netcontext, addrinfo** rv,
                           NetworkDnsEventReported* event) {
    //IPv6,IPv4的解析结果
    res_target q = {};
    res_target q2 = {};
    //省略一堆ipv4,ipv6判断
    ...

    ResState res;
    //将netcontext的值赋给res
    res_init(&res, netcontext, event);

    int he;
    //进行dns解析
    if (res_searchN(name, &q, &res, &he) < 0) {
        //dns解析失败
        return herrnoToAiErrno(he);
    }
    
    addrinfo sentinel = {};
    addrinfo* cur = &sentinel;
    //获取解析结果
    addrinfo* ai = getanswer(q.answer, q.n, q.name, q.qtype, pai, &he);
    if (ai) {
        cur->ai_next = ai;
        while (cur && cur->ai_next) cur = cur->ai_next;
    }
    if (q.next) {
        //获取解析结果
        ai = getanswer(q2.answer, q2.n, q2.name, q2.qtype, pai, &he);
        if (ai) cur->ai_next = ai;
    }
    if (sentinel.ai_next == NULL) {
        // Note that getanswer() doesn't set the pair NETDB_INTERNAL and errno.
        // See also herrnoToAiErrno().
        return herrnoToAiErrno(he);
    }

    _rfc6724_sort(&sentinel, netcontext->app_mark, netcontext->uid);

    *rv = sentinel.ai_next;
    return 0;
}

2.13 getaddrinfo.res_searchN

static int res_searchN(const char* name, res_target* target, res_state res, int* herrno) {
    const char* cp;
    HEADER* hp;
    //定义"."的数量
    uint32_t dots;
    int ret, saved_herrno;
    int got_nodata = 0, got_servfail = 0, tried_as_is = 0;

 

    hp = (HEADER*)(void*)target->answer.data();

    errno = 0;
    *herrno = HOST_NOT_FOUND; /* default, if we never query */
    dots = 0;
    //这段循环的意思是查看name域名中的"."的数量
    for (cp = name; *cp; cp++) dots += (*cp == '.');
    const bool trailing_dot = (cp > name && *--cp == '.') ? true : false;

    
    saved_herrno = -1;
    //res->ndots在前面初始化为1,当dns请求的域名中的"."数量大于等于1时
    if (dots >= res->ndots) {
        //dns解析
        ret = res_querydomainN(name, NULL, target, res, herrno);
        if (ret > 0) return (ret);
        saved_herrno = *herrno;
        tried_as_is++;
    }
    ...
    }

上面代码主要对DNS要解析的域名中的"."进行计算,目的是在处理 DNS 域名时解析域名中有多少个子域名,以方便后面进行分割和处理。

2.14 getaddrinfo.res_querydomainN

static int res_querydomainN(const char* name, const char* domain, res_target* target, res_state res,
                            int* herrno) {
    char nbuf[MAXDNAME];
    const char* longname = nbuf;
    size_t n, d;
    assert(name != NULL);

    if (domain == NULL) {
        // Check for trailing '.'; copy without '.' if present.
        n = strlen(name);
        if (n + 1 > sizeof(nbuf)) {
            *herrno = NO_RECOVERY;
            return -1;
        }
        if (n > 0 && name[--n] == '.') {
            strncpy(nbuf, name, n);
            nbuf[n] = '\0';
        } else
            longname = name;
    } else {
        n = strlen(name);
        d = strlen(domain);
        if (n + 1 + d + 1 > sizeof(nbuf)) {
            *herrno = NO_RECOVERY;
            return -1;
        }
        snprintf(nbuf, sizeof(nbuf), "%s.%s", name, domain);
    }
    ALOGE("djtang...res_querydomainN...name:%s,domain:%s,longname:%s",name,domain,longname);
    return res_queryN_wrapper(longname, target, res, herrno);
}

上面代码会对传入的主机名(name)和域名(domain)进行处理,其中域名可以为 NULL,如果域名为 NULL,就直接使用主机名作为完整域名。如果域名不为 NULL,则将主机名和域名拼接成完整的域名。对于主机名,如果最后有一个点".",则将其删除,以避免后续拼接出问题,然后使用 snprintf() 函数将主机名和域名拼接成完整域名,并将结果存储到 nbuf 缓冲区中,如下是日志打印结果,供参考:

在这里插入图片描述

2.15 getaddrinfo.res_queryN_wrapper

static int res_queryN_wrapper(const char* name, res_target* target, res_state res, int* herrno) {
    const bool parallel_lookup =
            android::net::Experiments::getInstance()->getFlag("parallel_lookup", 0);
    if (parallel_lookup) return res_queryN_parallel(name, target, res, herrno);

    return res_queryN(name, target, res, herrno);
}

2.16 getaddrinfo.res_queryN

static int res_queryN(const char* name, res_target* target, res_state res, int* herrno) {
    uint8_t buf[MAXPACKET];
    int n;
    struct res_target* t;
    int rcode;
    int ancount;

    assert(name != NULL);
    /* XXX: target may be NULL??? */

    rcode = NOERROR;
    ancount = 0;

    for (t = target; t; t = t->next) {
        HEADER* hp = (HEADER*)(void*)t->answer.data();
        bool retried = false;
    again:
        hp->rcode = NOERROR; /* default */

        /* make it easier... */
        int cl = t->qclass;
        int type = t->qtype;
        const int anslen = t->answer.size();

        LOG(DEBUG) << __func__ << ": (" << cl << ", " << type << ")";
        //DNS 查询报文的组装函数,可以将指定类别和类型的 DNS 查询报文组装为二进制格式,并存储到指定缓冲区中,
        //首先对 DNS 查询报文的头部进行初始化,包括设置报文 ID、报文类型、查询类型等字段。然后根据操作类型 op,
        //分别组装并添加报文的查询部分、附加部分和回答部分
        //此函数比较复杂,不必关注
        n = res_nmkquery(QUERY, name, cl, type, /*data=*/nullptr, /*datalen=*/0, buf, sizeof(buf),
                         res->netcontext_flags);
         
        ...
        //发送请求
        n = res_nsend(res, buf, n, t->answer.data(), anslen, &rcode, 0);
        
        ...
        
    }
    ...
    return ancount;
}

上面函数会对dns查询报文进行组装,然后通过res_nsend发送请求报文:

2.17 res_send.res_nsend

这个函数很长,很复杂,作用于 DNS 查询,此函数接收一个 DNS 查询请求并返回相应的 DNS 响应,优先通过查询缓存来优化查询速度,如果查询在缓存中找到,则会在不执行实际查询的情况下返回缓存中的响应。如果启用了 DNS-over-TLS,则使用 DNS-over-TLS 与 DNS 服务器进行通信。否则使用 UDP 或 TCP 协议与 DNS 服务器进行通信,如果在所有 DNS 服务器上都无法获得响应,则该函数将返回错误。

int res_nsend(res_state statp, const uint8_t* buf, int buflen, uint8_t* ans, int anssiz, int* rcode,
              uint32_t flags, std::chrono::milliseconds sleepTimeMs) {
    
    /*
    将dns请求打印出来,大概长这样:
    # Query packet
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29827
    ;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
    ;; QUERY SECTION:
    ;;    www.google.com, type = A, class = IN
    */
    res_pquery(buf, buflen);

    int anslen = 0;
    Stopwatch cacheStopwatch;
    //查询缓存
    ResolvCacheStatus cache_status =
            resolv_cache_lookup(statp->netid, buf, buflen, ans, anssiz, &anslen, flags);
    const int32_t cacheLatencyUs = saturate_cast<int32_t>(cacheStopwatch.timeTakenUs());
    if (cache_status == RESOLV_CACHE_FOUND) {
        HEADER* hp = (HEADER*)(void*)ans;
        *rcode = hp->rcode;
        DnsQueryEvent* dnsQueryEvent = addDnsQueryEvent(statp->event);
        dnsQueryEvent->set_latency_micros(cacheLatencyUs);
        dnsQueryEvent->set_cache_hit(static_cast<CacheStatus>(cache_status));
        dnsQueryEvent->set_type(getQueryType(buf, buflen));
        return anslen;
    } else if (cache_status != RESOLV_CACHE_UNSUPPORTED) {

        resolv_populate_res_for_net(statp);
    }
    if (statp->nameserverCount() == 0) {

        _resolv_cache_query_failed(statp->netid, buf, buflen, flags);

        // TODO: Remove errno once callers stop using it
        errno = ESRCH;
        return -ESRCH;
    }

    // If parallel_lookup is enabled, it might be required to wait some time to avoid
    // gateways drop packets if queries are sent too close together
    if (sleepTimeMs != 0ms) {
        std::this_thread::sleep_for(sleepTimeMs);
    }
    // DoT
    if (!(statp->netcontext_flags & NET_CONTEXT_FLAG_USE_LOCAL_NAMESERVERS)) {
        bool fallback = false;
        //DNS-over-TLS查询
        int resplen = res_tls_send(statp, Slice(const_cast<uint8_t*>(buf), buflen),
                                   Slice(ans, anssiz), rcode, &fallback);
        if (resplen > 0) {
            LOG(DEBUG) << __func__ << ": got answer from DoT";
            res_pquery(ans, resplen);
            if (cache_status == RESOLV_CACHE_NOTFOUND) {
                resolv_cache_add(statp->netid, buf, buflen, ans, resplen);
            }
            return resplen;
        }
        if (!fallback) {
            _resolv_cache_query_failed(statp->netid, buf, buflen, flags);
            return -ETIMEDOUT;
        }
    }

    res_stats stats[MAXNS]{};
    res_params params;
    int revision_id = resolv_cache_get_resolver_stats(statp->netid, &params, stats, statp->nsaddrs);
    if (revision_id < 0) {
        // TODO: Remove errno once callers stop using it
        errno = ESRCH;
        return -ESRCH;
    }
    bool usable_servers[MAXNS];
    int usableServersCount = android_net_res_stats_get_usable_servers(
            &params, stats, statp->nameserverCount(), usable_servers);

    if ((flags & ANDROID_RESOLV_NO_RETRY) && usableServersCount > 1) {
        auto hp = reinterpret_cast<const HEADER*>(buf);

        // Select a random server based on the query id
        int selectedServer = (hp->id % usableServersCount) + 1;
        res_set_usable_server(selectedServer, statp->nameserverCount(), usable_servers);
    }

    // Send request, RETRY times, or until successful.
    int retryTimes = (flags & ANDROID_RESOLV_NO_RETRY) ? 1 : params.retry_count;
    int useTcp = buflen > PACKETSZ;
    int gotsomewhere = 0;
    // Use an impossible error code as default value
    int terrno = ETIME;

    for (int attempt = 0; attempt < retryTimes; ++attempt) {
        for (size_t ns = 0; ns < statp->nsaddrs.size(); ++ns) {
            if (!usable_servers[ns]) continue;

            *rcode = RCODE_INTERNAL_ERROR;

            // Get server addr
            const IPSockAddr& serverSockAddr = statp->nsaddrs[ns];
            LOG(DEBUG) << __func__ << ": Querying server (# " << ns + 1
                       << ") address = " << serverSockAddr.toString();

            ::android::net::Protocol query_proto = useTcp ? PROTO_TCP : PROTO_UDP;
            time_t query_time = 0;
            int delay = 0;
            bool fallbackTCP = false;
            const bool shouldRecordStats = (attempt == 0);
            int resplen;
            Stopwatch queryStopwatch;
            int retry_count_for_event = 0;
            size_t actualNs = ns;
            // Use an impossible error code as default value
            terrno = ETIME;
            if (useTcp) {
                // TCP查询
                attempt = retryTimes;
                resplen = send_vc(statp, &params, buf, buflen, ans, anssiz, &terrno, ns,
                                  &query_time, rcode, &delay);

                if (buflen <= PACKETSZ && resplen <= 0 &&
                    statp->tc_mode == aidl::android::net::IDnsResolver::TC_MODE_UDP_TCP) {
                    // reset to UDP for next query on next DNS server if resolver is currently doing
                    // TCP fallback retry and current server does not support TCP connectin
                    useTcp = false;
                }
                LOG(INFO) << __func__ << ": used send_vc " << resplen << " terrno: " << terrno;
            } else {
                // UDP查询
                resplen = send_dg(statp, &params, buf, buflen, ans, anssiz, &terrno, &actualNs,
                                  &useTcp, &gotsomewhere, &query_time, rcode, &delay);
                fallbackTCP = useTcp ? true : false;
                retry_count_for_event = attempt;
                LOG(INFO) << __func__ << ": used send_dg " << resplen << " terrno: " << terrno;
            }

            const IPSockAddr& receivedServerAddr = statp->nsaddrs[actualNs];
            DnsQueryEvent* dnsQueryEvent = addDnsQueryEvent(statp->event);
            dnsQueryEvent->set_cache_hit(static_cast<CacheStatus>(cache_status));
            // When |retryTimes| > 1, we cannot actually know the correct latency value if we
            // received the answer from the previous server. So temporarily set the latency as -1 if
            // that condition happened.
            // TODO: make the latency value accurate.
            dnsQueryEvent->set_latency_micros(
                    (actualNs == ns) ? saturate_cast<int32_t>(queryStopwatch.timeTakenUs()) : -1);
            dnsQueryEvent->set_dns_server_index(actualNs);
            dnsQueryEvent->set_ip_version(ipFamilyToIPVersion(receivedServerAddr.family()));
            dnsQueryEvent->set_retry_times(retry_count_for_event);
            dnsQueryEvent->set_rcode(static_cast<NsRcode>(*rcode));
            dnsQueryEvent->set_protocol(query_proto);
            dnsQueryEvent->set_type(getQueryType(buf, buflen));
            dnsQueryEvent->set_linux_errno(static_cast<LinuxErrno>(terrno));

            // Only record stats the first time we try a query. This ensures that
            // queries that deterministically fail (e.g., a name that always returns
            // SERVFAIL or times out) do not unduly affect the stats.
            if (shouldRecordStats) {
                // (b/151166599): This is a workaround to prevent that DnsResolver calculates the
                // reliability of DNS servers from being broken when network restricted mode is
                // enabled.
                // TODO: Introduce the new server selection instead of skipping stats recording.
                if (!isNetworkRestricted(terrno)) {
                    res_sample sample;
                    res_stats_set_sample(&sample, query_time, *rcode, delay);
                    // KeepListening UDP mechanism is incompatible with usable_servers of legacy
                    // stats, so keep the old logic for now.
                    // TODO: Replace usable_servers of legacy stats with new one.
                    resolv_cache_add_resolver_stats_sample(
                            statp->netid, revision_id, serverSockAddr, sample, params.max_samples);
                }
                resolv_stats_add(statp->netid, receivedServerAddr, dnsQueryEvent);
            }

            if (resplen == 0) continue;
            if (fallbackTCP) {
                ns--;
                continue;
            }
            if (resplen < 0) {
                _resolv_cache_query_failed(statp->netid, buf, buflen, flags);
                statp->closeSockets();
                return -terrno;
            };

            /*
            将dns响应打印出来,大概长这样:
            # Response packet
            ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29827
            ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
            ;; QUERY SECTION:
            ;;    www.google.com, type = A, class = IN

            ;; ANSWER SECTION:
            ;; www.google.com.        2m19s IN A  172.217.160.100
            */
            res_pquery(ans, (resplen > anssiz) ? anssiz : resplen);

            if (cache_status == RESOLV_CACHE_NOTFOUND) {
                resolv_cache_add(statp->netid, buf, buflen, ans, resplen);
            }
            statp->closeSockets();
            return (resplen);
        }  // for each ns
    }  // for each retry
    statp->closeSockets();
    terrno = useTcp ? terrno : gotsomewhere ? ETIMEDOUT : ECONNREFUSED;
    // TODO: Remove errno once callers stop using it
    errno = useTcp ? terrno
                   : gotsomewhere ? ETIMEDOUT /* no answer obtained */
                                  : ECONNREFUSED /* no nameservers found */;

    _resolv_cache_query_failed(statp->netid, buf, buflen, flags);
    return -terrno;
}

上面代码大概分为三块,1. 查询缓存,2. 使用DNS-over-TLS查询,3. 使用TCP或者UDP查询,我们这儿只关注第三点,由于DNS解析走的UDP协议,只需关注send_dg函数:

2.18 res_send.send_dg

static int send_dg(res_state statp, res_params* params, const uint8_t* buf, int buflen,
                   uint8_t* ans, int anssiz, int* terrno, size_t* ns, int* v_circuit,
                   int* gotsomewhere, time_t* at, int* rcode, int* delay) {
  
   
   
    ...
      
    if (statp->nssocks[*ns] == -1) {
        //创建socket
        statp->nssocks[*ns].reset(socket(nsap->sa_family, SOCK_DGRAM | SOCK_CLOEXEC, 0));
        if (statp->nssocks[*ns] < 0) {
            *terrno = errno;
            PLOG(DEBUG) << __func__ << ": socket(dg): ";
            switch (errno) {
                case EPROTONOSUPPORT:
                case EPFNOSUPPORT:
                case EAFNOSUPPORT:
                    return (0);
                default:
                    return (-1);
            }
        }

        const uid_t uid = statp->enforce_dns_uid ? AID_DNS : statp->uid;
        resolv_tag_socket(statp->nssocks[*ns], uid, statp->pid);
        if (statp->_mark != MARK_UNSET) {
            if (setsockopt(statp->nssocks[*ns], SOL_SOCKET, SO_MARK, &(statp->_mark),
                           sizeof(statp->_mark)) < 0) {
                *terrno = errno;
                statp->closeSockets();
                return -1;
            }
        }
        // Use a "connected" datagram socket to receive an ECONNREFUSED error
        // on the next socket operation when the server responds with an
        // ICMP port-unreachable error. This way we can detect the absence of
        // a nameserver without timing out.
        if (random_bind(statp->nssocks[*ns], nsap->sa_family) < 0) {
            *terrno = errno;
            dump_error("bind(dg)", nsap, nsaplen);
            statp->closeSockets();
            return (0);
        }
        //连接到socket,ip地址和端口信息在nsap中,ip地址就是DNS服务器地址,对wifi来说是网关地址,对移动网络来说
        //是网络创建时指定的地址
        if (connect(statp->nssocks[*ns], nsap, (socklen_t)nsaplen) < 0) {
            *terrno = errno;
            dump_error("connect(dg)", nsap, nsaplen);
            statp->closeSockets();
            return (0);
        }
        LOG(DEBUG) << __func__ << ": new DG socket";
    }
    //向DNS服务器发送查询报文
    if (send(statp->nssocks[*ns], (const char*)buf, (size_t)buflen, 0) != buflen) {
        *terrno = errno;
        PLOG(DEBUG) << __func__ << ": send: ";
        statp->closeSockets();
        
        return 0;
    }
    //超时时间
    timespec timeout = get_timeout(statp, params, *ns);
    timespec start_time = evNowTime();
    timespec finish = evAddTime(start_time, timeout);
    for (;;) {
        // 等待回复
        auto result = udpRetryingPollWrapper(statp, *ns, &finish);

        if (!result.has_value()) {
            const bool isTimeout = (result.error().code() == ETIMEDOUT);
            *rcode = (isTimeout) ? RCODE_TIMEOUT : *rcode;
            *terrno = (isTimeout) ? ETIMEDOUT : errno;
            *gotsomewhere = (isTimeout) ? 1 : *gotsomewhere;
            // Leave the UDP sockets open on timeout so we can keep listening for
            // a late response from this server while retrying on the next server.
            if (!isTimeout) statp->closeSockets();
            LOG(DEBUG) << __func__ << ": " << (isTimeout) ? "timeout" : "poll";
            return 0;
        }
        //是否重试
        bool needRetry = false;
        for (int fd : result.value()) {
            needRetry = false;
            sockaddr_storage from;
            socklen_t fromlen = sizeof(from);
            //接收响应消息
            int resplen =
                    recvfrom(fd, (char*)ans, (size_t)anssiz, 0, (sockaddr*)(void*)&from, &fromlen);
             
            if (resplen <= 0) {
                *terrno = errno;
                PLOG(DEBUG) << __func__ << ": recvfrom: ";
                continue;
            }
            *gotsomewhere = 1;
            if (resplen < HFIXEDSZ) {
                // Undersized message.
                LOG(DEBUG) << __func__ << ": undersized: " << resplen;
                *terrno = EMSGSIZE;
                continue;
            }

            int receivedFromNs = *ns;
            if (needRetry =
                        ignoreInvalidAnswer(statp, from, buf, buflen, ans, anssiz, &receivedFromNs);
                needRetry) {
                res_pquery(ans, (resplen > anssiz) ? anssiz : resplen);
                continue;
            }

            HEADER* anhp = (HEADER*)(void*)ans;
            if (anhp->rcode == FORMERR && (statp->netcontext_flags & NET_CONTEXT_FLAG_USE_EDNS)) {
                //  Do not retry if the server do not understand EDNS0.
                //  The case has to be captured here, as FORMERR packet do not
                //  carry query section, hence res_queriesmatch() returns 0.
                LOG(DEBUG) << __func__ << ": server rejected query with EDNS0:";
                res_pquery(ans, (resplen > anssiz) ? anssiz : resplen);
                // record the error
                statp->_flags |= RES_F_EDNS0ERR;
                *terrno = EREMOTEIO;
                continue;
            }

            timespec done = evNowTime();
            *delay = res_stats_calculate_rtt(&done, &start_time);
            if (anhp->rcode == SERVFAIL || anhp->rcode == NOTIMP || anhp->rcode == REFUSED) {
                LOG(DEBUG) << __func__ << ": server rejected query:";
                res_pquery(ans, (resplen > anssiz) ? anssiz : resplen);
                *rcode = anhp->rcode;
                continue;
            }
            if (anhp->tc) {
                // To get the rest of answer,
                // use TCP with same server.
                LOG(DEBUG) << __func__ << ": truncated answer";
                *terrno = E2BIG;
                *v_circuit = 1;
                return 1;
            }
            // All is well, or the error is fatal. Signal that the
            // next nameserver ought not be tried.

            *rcode = anhp->rcode;
            *ns = receivedFromNs;
            *terrno = 0;
            return resplen;
        }
        if (!needRetry) return 0;
    }
}

DNS解析的大致流程到这儿就结束了,这儿我们只看了大致的函数调用流程,其中的大量细节都没具体看,例如缓存机制,网络选择机制等,大致流程图如下:

在这里插入图片描述

总结一下整个过程吧,整个流程分为如下几个步骤:

  1. 应用程序使用网络框架(如okhttp)发送网络请求时,首先会通过java提供的Inet6AddressImpl.lookupHostByName方法获取域名对应的IP地址。
  2. Inet6AddressImpl.lookupHostByName核心是向"/dev/socket/dnsproxyd"这个socket写入一段字符串"getaddrinfo %s %s %d %d %d %d %u",这段字符串包含方法名+参数。
  3. netd进程的DnsProxyListener通过FrameworkListener监听了**“/dev/socket/dnsproxyd”**,接收到客户端发送的字符串调用对应函数GetAddrInfoCmd::runCommand
  4. 接着就是一系列调用,各种合法判断条件,缓存机制最终其实是创建,连接socket,IP和端口就是DNS解析的地址,wifi一般是路由器地址,移动网络一般是自行配置的地址如114.114.114.114或者8.8.8.8等等。

请求和响应的报文经过解析长这样子:

37 # Query packet
38 resolv : ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29827
39 resolv : ;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
40 resolv : ;; QUERY SECTION:
41 resolv : ;; www.google.com, type = A, class = IN
42 resolv :
43 resolv : Hex dump:
44 resolv : 7483010000010000000000000377777706676f6f676c6503636f6d0000010001
45
46 # Response packet
47 resolv : ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29827
48 resolv : ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
49 resolv : ;; QUERY SECTION:
50 resolv : ;; www.google.com, type = A, class = IN
51 resolv :
52 resolv : ;; ANSWER SECTION:
53 resolv : ;; www.google.com. 2m19s IN A 172.217.160.100
54 resolv :
55 resolv : Hex dump:
56 resolv : 7483818000010001000000000377777706676f6f676c6503636f6d0000010001
57 resolv : c00c000100010000008b0004acd9a064

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/30416.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python(request)爬虫有多强大?

requests是Python中的一种HTTP客户端库&#xff0c;用于发送HTTP请求并获取服务器响应。使用requests库可以轻松地进行常见的HTTP操作&#xff0c;如GET、POST、PUT、DELETE等&#xff0c;支持HTTPS和HTTP连接以及摘要验证、基本认证等身份验证方式。 因此&#xff0c;Python的…

阿里云创建ALB_Ingress

阿里云参考文档 1、编写alb AlbConfig创建文档 rootbiking-pre-middleware:~/alb# cat ingress-alb.yaml apiVersion: alibabacloud.com/v1 kind: AlbConfig metadata: #alb名称name: ingress-alb spec:config:name: ingress-albaddressType: InternetzoneMappings:# 两个交换…

ESP32学习之定时器和PWM

一.定时器代码如下&#xff1a; #include <Arduino.h>hw_timer_t *timer NULL; int interruptCounter 0;// 函数名称&#xff1a;onTimer() // 函数功能&#xff1a;中断服务的功能&#xff0c;它必须是一个返回void&#xff08;空&#xff09;且没有输入参数的函数 //…

Redis 哨兵模式

哨兵模式 自动选举 Redis 主服务器&#xff08;当主服务器宕机时&#xff09; 在 Redis2.8 之前&#xff0c;采用手动配置主从机的形式&#xff08;会导致一段时间服务不可用&#xff09; Redis2.8 之后&#xff0c;Redis正是提供了 Sentinel&#xff08;哨兵&#xff09;来解…

逆向Android开发工程,抓包!抓包!学习哪里?

抓包是什么&#xff1f; 在Android逆向工程中&#xff0c;抓包是一项重要的技术&#xff0c;用于获取手机应用程序与服务器之间的通信数据。通过抓包&#xff0c;可以分析应用程序的网络请求&#xff0c;获取请求的URL、参数、响应数据等信息&#xff0c;对应用程序的行为进行…

Python编程入门基础及高级技能、Web开发、数据分析和机器学习与人工智能

文章目录 入门基础安装 Python 环境&#xff0c;选择一个 IDE&#xff0c;如 PyCharm、VSCode等。学习基本语法&#xff1a;变量、数据类型、条件语句、循环语句、函数、异常处理等。熟悉标准库&#xff1a;常用模块、内置函数等。学习基本的面向对象编程&#xff08;OOP&#…

Rust in Action笔记 第六章 内存

Option<T>类型在Rust中使用了空指针优化&#xff08;null pointer optimization&#xff09;来保证该类型在编译后的二进制文件中占用0个字节。None变量是通过一个空指针null pointer来表示&#xff1b;内存地址、指针、引用的区别&#xff0c;内存地址是指在内存中的一个…

【kubernetes】Etcd集群部署与验证

前言:二进制部署kubernetes集群在企业应用中扮演着非常重要的角色。无论是集群升级,还是证书设置有效期都非常方便,也是从事云原生相关工作从入门到精通不得不迈过的坎。通过本系列文章,你将从虚拟机准备开始,到使用二进制方式从零到一搭建起安全稳定的高可用kubernetes集…

基于OpenMV的自动驾驶智能小车模拟系统

一、项目简介 基于机器视觉模块OpenMV采集车道、红绿灯、交通标志等模拟路况信息&#xff0c;实现一辆能车道保持、红绿灯识别、交通标志识别、安全避障以及远程WiFi控制的多功能无人驾驶小车。 赛道规格&#xff1a; 1、编程所需软件&#xff1a; OpenMV&#xff1a;使用Op…

Python自动化测试框架:Pytest和Unittest的区别

pytest和unittest是Python中常用的两种测试框架&#xff0c;它们都可以用来编写和执行测试用例&#xff0c;但两者在很多方面都有所不同。本文将从不同的角度来论述这些区别&#xff0c;以帮助大家更好地理解pytest和unittest。 1. 原理 pytest是基于Python的assert语句和Pytho…

Git第二章、多人协作

一、多人协作一 目前&#xff0c;我们所完成的工作如下&#xff1a; • 基本完成 Git 的所有本地库的相关操作&#xff0c;git基本操作&#xff0c;分支理解&#xff0c;版本回退&#xff0c;冲突解决等等 • 申请码云账号&#xff0c;将远端信息clone到本地&#xff0c;以及推…

一种基于目标的可解释的自动驾驶预测和规划策略

摘要&#xff1a; 本文介绍了一种通过理性逆向规划进行目标识别和多模态轨迹预测的方法。通过将目标识别与MCTS 计划相结合&#xff0c;为自车生成优化计划。 最近炒得比较火的影子模式实际就是在通过数据收集的方式不断模拟自动驾驶系统按照人类驾驶习惯实现人之间的交互过程…

Qt 中动态加载窗口(C++)

在编程中&#xff0c;我经常会遇见要根据用户触发按钮&#xff0c;动态生成窗口的情况。在此有两种方法可以动态生成窗口&#xff1a;一&#xff1a;直接在槽函数中调用窗口类。二&#xff1a;将 **.ui 添加到资源文件&#xff0c;通过 QUiLoader 加载。 现将两种方法介绍如下…

linuxOPS系统服务_Linux下用户管理

用户概念以及基本作用 **用户&#xff1a;**指的是Linux操作系统中用于管理系统或者服务的人 一问&#xff1a;管理系统到底在管理什么&#xff1f; 答&#xff1a;Linux下一切皆文件&#xff0c;所以用户管理的是相应的文件 二问&#xff1a;如何管理文件呢&#xff1f; …

【新星计划回顾】第六篇学习计划-通过自定义函数和存储过程模拟MD5数据

&#x1f3c6;&#x1f3c6;时间过的真快&#xff0c;这是导师回顾新星计划学习的第六篇文章&#xff01; 最近这段时间非常忙&#xff0c;虽然导师首次参与新星计划活动已经在4月16日圆满结束&#xff0c;早想腾出时间来好好整理活动期间分享的知识点。 &#x1f3c6;&#x1…

Unsupervised Learning(无监督学习)

目录 Introduction Clustering&#xff08;聚类&#xff09; Dimension Reduction&#xff08;降维&#xff09; PCA&#xff08;Principle component analysis&#xff0c;主成分分析&#xff09; Word Embedding&#xff08;词嵌入&#xff09; Matrix Factorization(矩…

适合嵌入式开发的GUI(嵌入式学习)

嵌入式开发的GUI如何选择&#xff1f; 常见的嵌入式GUI开发方法轻量级GUI库优缺点 基于Web技术优缺点 Qt框架优缺点 原生开发优缺点 嵌入式系统的限制 常见的嵌入式GUI开发方法 嵌入式开发中的GUI&#xff08;图形用户界面&#xff09;是指在嵌入式系统中实现图形化的用户界面…

Linux权限管理(超详解哦)

Linux权限 引言文件访问者的分类文件类型与访问权限文件类型访问权限 文件权限值的表示方法修改权限的指令chmod修改文件权限通过角色/-/权限来修改通过三个八进制数修改 chown修改所有者chgrp修改所属组umask修改或查看文件权限掩码文件创建时的权限 目录的权限粘滞位 总结 引…

驱动模块和printk函数

目录 1. 驱动模板 1.1. 在源码工程路径下创建.c文件 1.2. 编写驱动模板 1.3. 将模板放到ubuntu上 1.4. 书写Makefile 1.5. 编译和安装 2. printk 2.1. Source Insight查找命令 2.2. printk讲解 2.2.1. 分析函数 2.2.2. 编写代码 2.3. 拓展 2.3.1. 关于printk函数测…

ESP32开发环境搭建Windows VSCode集成Espressif IDF插件ESP32_IDF_V5.0开发编译环境搭建

一、安装ESP32-IDF库 下载网址&#xff1a;https://dl.espressif.com/dl/esp-idf/ 打开上面的网页&#xff0c;选择单击页面中 ESP32-IDF v5.0.2 - Offine Installer&#xff0c;5.0.2是当前最新版本&#xff0c;如果没有ESP32-IDF v5.0.2 - Offine Installer&#xff0c;说明…