面试真题 | 思特威[20241013]

@[toc]

1. 您在简历中提到的四个项目中,哪一个最复杂,能否详细描述一下您在该项目中的角色和遇到的技术挑战?

2. 在嵌入式系统中,您如何管理和优化内存使用?

在嵌入式系统中,管理和优化内存使用是至关重要的,因为嵌入式设备通常资源有限,尤其是内存资源。

管理与优化内存使用的方法

  1. 静态内存分配

    • 静态内存分配是指在编译时确定内存空间大小和生命周期的方法。
    • 程序员通常使用全局变量、静态变量或在栈上声明局部变量来实现静态内存分配。
    • 这种分配方式简单高效,无需运行时动态分配和释放,从而避免了内存碎片和泄漏问题。
  2. 动态内存分配

    • 动态内存分配允许程序在运行时根据需要申请和释放内存。
    • 在嵌入式开发中,通常使用标准库函数如malloc()calloc()realloc()来分配内存,使用free()来释放内存。
    • 动态内存分配提供了更大的灵活性,但也需要谨慎管理以防止内存泄漏、溢出和碎片化。
  3. 内存池管理

    • 内存池是一种预先分配一大块连续内存,然后按照固定大小划分为多个小块供程序按需分配使用的机制。
    • 内存池适用于频繁进行小对象分配和释放的场景,能有效减少内存碎片,提高内存分配速度,降低系统开销。
  4. 内存优化策略

    • 减少内存消耗:选择合适的数据结构和算法,避免不必要的内存冗余。例如,使用位图代替布尔数组存储大量标志位,合理设置缓冲区大小避免过度分配。
    • 避免内存泄漏:使用智能指针、RAII(Resource Acquisition Is Initialization)等技术自动管理内存生命周期,或者使用内存泄漏检测工具定期检查代码。
    • 预防内存溢出:对内存分配请求进行边界检查,确保不会超出可用内存范围。对于动态数组,可以考虑使用动态扩容策略(如倍增法)而不是一次性分配过大空间。
    • 缓解内存碎片:除了使用内存池外,还可以通过合并相邻的空闲内存块、采用最佳适配、首次适配等分配策略来减少碎片。

面试官追问及回答

面试官追问1:在嵌入式系统中,动态内存分配有哪些潜在的风险,您是如何应对这些风险的?

回答: 动态内存分配在嵌入式系统中可能带来的风险主要包括内存泄漏、内存溢出和内存碎片。为了应对这些风险,我采取以下措施:

  • 内存泄漏:定期使用内存泄漏检测工具检查代码,确保每次动态分配的内存都有相应的释放操作。同时,遵循良好的编程规范,如使用智能指针和RAII技术来自动管理内存生命周期。
  • 内存溢出:在动态分配内存时,进行严格的边界检查,确保分配的内存大小不会超过可用内存范围。对于动态数组等数据结构,采用动态扩容策略来避免一次性分配过大空间。
  • 内存碎片:使用内存池技术来减少内存碎片的产生。内存池通过预先分配固定大小的内存块并重复使用这些内存块来降低碎片化的风险。

面试官追问2:在嵌入式系统中,如何选择合适的内存分配策略以提高系统性能?

回答: 在嵌入式系统中选择合适的内存分配策略需要考虑多个因素,包括系统的实时性要求、内存资源的限制以及应用程序的具体需求。以下是我选择内存分配策略时的一些考虑:

  • 实时性要求:如果系统对实时性要求较高,我会优先考虑使用静态内存分配或内存池技术来减少动态内存分配带来的不确定性和延迟。
  • 内存资源限制:在内存资源受限的嵌入式系统中,我会尽量减少动态内存的使用,采用静态内存分配或固定大小的内存块来降低内存开销。
  • 应用程序需求:根据应用程序的具体需求来选择合适的内存分配策略。例如,如果应用程序需要频繁分配和释放小对象,我会考虑使用内存池技术来提高内存分配和释放的效率。

面试官追问3:在嵌入式系统中进行内存优化时,您通常会关注哪些方面的指标?

回答: 在嵌入式系统中进行内存优化时,我通常会关注以下几个方面的指标:

  • 内存使用率:通过监控内存使用率来了解系统的内存消耗情况,确保系统不会因内存不足而崩溃。
  • 内存碎片率:通过计算内存碎片率来评估内存分配的效率和系统性能。较高的内存碎片率可能导致内存分配失败或系统性能下降。
  • 内存访问延迟:通过测量内存访问延迟来评估系统的实时性能。较低的内存访问延迟可以提高系统的响应速度和数据处理能力。
  • 内存泄漏率:通过定期检测内存泄漏率来确保系统的稳定性和可靠性。较高的内存泄漏率可能导致系统内存逐渐耗尽,最终导致系统崩溃。

3. 您能否解释一下C++中的指针和引用在内存管理和使用上的主要区别?

指针与引用的基本概念

  • 指针:指针是C++中一种特殊的变量类型,它存储的是另一个变量的内存地址。通过指针,我们可以间接地访问和操作内存中的数据。指针具有自己的内存空间,用于存储地址值。

  • 引用:引用是C++中的另一种复合类型,它是某个已存在变量的别名。引用在创建时就必须被初始化,并且一旦被初始化后,就不能再改变引用的对象。引用本身不占用额外的内存空间,它只是现有变量的一个别名。

内存管理和使用上的主要区别

  1. 内存分配与释放

    • 指针:指针可以动态地分配和释放内存(通过newdelete操作符)。这使得指针在需要时能够灵活地管理内存资源,但同时也带来了内存泄漏和悬挂指针等风险。
    • 引用:引用则不能用于动态内存分配和释放。它总是指向一个已经存在的对象,因此不存在内存泄漏的问题。但是,这也意味着引用不能用于创建新的对象或数组。
  2. 空值处理

    • 指针:指针可以被初始化为nullptr(或NULL,在C++11之前),表示它不指向任何对象。这使得指针能够用于表示空值或无效状态。
    • 引用:引用在创建时必须被初始化,且不能为空。因此,引用不能用于表示空值或无效状态。如果尝试将引用初始化为空值,将导致编译错误。
  3. 操作灵活性

    • 指针:指针可以进行各种算术运算(如加减运算、指针比较等),这使得指针能够用于更复杂的内存管理和数据结构操作。但是,这种灵活性也增加了出错的风险。
    • 引用:引用则相对简单和直观。它们不能进行算术运算,只能用于访问和修改所引用的对象。这使得引用更加安全和易于使用。
  4. 生命周期

    • 指针:指针的生命周期独立于它所指向的对象。即使对象被销毁,指针仍然可以存在并指向该对象的内存地址(这被称为悬挂指针)。因此,在使用指针时需要特别小心以避免悬挂指针问题。
    • 引用:引用的生命周期与其所引用的对象相同。当对象被销毁时,引用也会自动失效。这使得引用更加安全和易于管理。

面试官追问及回答

面试官追问1:在嵌入式系统中,使用指针和引用时需要注意哪些特殊问题?

回答:在嵌入式系统中,内存资源通常非常有限。因此,在使用指针和引用时需要注意以下几点:

  • 避免不必要的动态内存分配和释放,以减少内存碎片和泄漏的风险。
  • 谨慎处理悬挂指针问题,确保在对象被销毁后不再使用指向该对象的指针。
  • 在使用引用时,要确保所引用的对象在引用的生命周期内始终有效。

面试官追问2:能否给出一个具体的例子来说明指针和引用在内存管理上的区别?

回答:当然可以。以下是一个简单的例子来说明指针和引用在内存管理上的区别:

#include <iostream>

void usePointer() {
    int* ptr = new int(10); // 动态分配内存
    std::cout << "Pointer value: " << *ptr << std::endl;
    delete ptr; // 手动释放内存
}

void useReference(int& ref) {
    std::cout << "Reference value: " << ref << std::endl;
}

int main() {
    int value = 20;
    usePointer(); // 使用指针动态分配和释放内存
    useReference(value); // 使用引用访问现有变量
    return 0;
}

在这个例子中,usePointer函数使用指针动态地分配了一个整数,并在使用完毕后手动释放了内存。而useReference函数则使用引用来访问一个已经存在的整数变量。这个例子展示了指针和引用在内存管理上的主要区别:指针可以动态地分配和释放内存,而引用则只能访问现有变量。

面试官追问3:在C++中,有没有一种方式可以像引用一样安全地使用指针,同时又能保留指针的灵活性?

回答:在C++中,智能指针(如std::unique_ptrstd::shared_ptr)提供了一种像引用一样安全地使用指针的方式,同时保留了指针的灵活性。智能指针是C++11及更高版本中引入的一种类模板,它们能够自动管理内存资源,避免内存泄漏和悬挂指针等问题。通过使用智能指针,我们可以更安全地动态分配和释放内存,同时享受指针带来的灵活性。

4. 在您的项目中,您是如何使用static关键字的,它对程序性能有何影响?

在嵌入式移动通信项目中,static关键字的使用非常关键,它主要用于控制变量的作用域、生命周期以及函数或变量的链接属性。

使用static关键字的方式

  1. 控制变量作用域

    • 在函数内部使用static关键字声明的变量,其生命周期贯穿整个程序运行期间,但作用域仅限于该函数内部。这种变量被称为静态局部变量。它常用于需要在多次函数调用间保持状态的场景,如计数器或缓存数据。
  2. 限制链接属性

    • 在文件作用域(全局变量或函数)前使用static关键字,可以限制这些变量或函数的作用范围仅限于定义它们的源文件。这有助于避免命名冲突,并提高代码的封装性。
  3. 实现静态存储类

    • 在C语言中,static还可以用于修饰函数内部的静态数组或结构体,这些静态存储类的数据在程序运行期间只分配一次内存,并在函数调用结束后保持其值不变,直到下次函数调用或程序结束。

对程序性能的影响

  1. 内存使用

    • 静态变量在程序运行期间只分配一次内存,并在整个程序生命周期内保持其值。这减少了动态内存分配和释放的开销,但也可能导致内存占用增加,特别是在静态变量占用大量内存时。
  2. 访问速度

    • 静态变量的访问通常比动态分配的内存更快,因为它们位于程序的静态数据区,访问路径相对固定。然而,这种性能提升在大多数情况下并不显著,且依赖于具体的编译器和硬件平台。
  3. 代码优化

    • 编译器可以对静态变量进行更多的优化,因为它们的作用域和生命周期是已知的。这包括但不限于常量折叠、循环展开和死代码消除等优化技术。
  4. 线程安全性

    • 在多线程环境中,非静态局部变量是线程局部的,而静态变量(包括全局静态变量和函数内部的静态变量)是共享的。因此,使用静态变量时需要特别注意线程安全问题,避免数据竞争和死锁等问题。

面试官追问及回答

面试官追问1:在您的项目中,您是如何处理静态变量的线程安全问题的?

回答:在处理静态变量的线程安全问题时,我通常会采用以下几种策略:

  • 使用互斥锁(mutex)来保护对静态变量的访问,确保同一时间只有一个线程可以访问该变量。
  • 使用原子操作(atomic operations)来更新静态变量的值,这通常适用于简单的计数器或标志位等场景。
  • 将静态变量封装在类中,并通过类的成员函数来访问和修改这些变量。在成员函数内部,可以使用上述的互斥锁或原子操作来确保线程安全。

面试官追问2:在嵌入式系统中,内存资源通常非常有限。您是如何在使用静态变量时平衡内存使用和性能之间的关系的?

回答:在嵌入式系统中,内存资源确实非常有限。为了平衡内存使用和性能之间的关系,我通常会采取以下策略:

  • 仔细分析程序的需求,确保只使用必要的静态变量,并避免不必要的内存浪费。
  • 对于需要频繁访问的静态变量,我会尽量将它们放在缓存友好的位置,以减少缓存未命中的开销。
  • 在可能的情况下,我会使用动态内存分配来替代静态变量,但会谨慎管理内存的生命周期和释放时机,以避免内存泄漏和碎片问题。
  • 最后,我会通过代码分析和性能测试来评估静态变量对内存使用和性能的影响,并根据测试结果进行调整和优化。

面试官追问3:在您的项目中,您是否遇到过由于静态变量导致的难以调试的问题?您是如何解决的?

回答:确实,在项目中我遇到过由于静态变量导致的难以调试的问题。这些问题通常发生在多线程环境中,由于静态变量的共享性,导致数据竞争和死锁等问题难以追踪。为了解决这个问题,我通常会采取以下策略:

  • 在开发过程中,我会尽量避免使用全局静态变量,而是将它们封装在类中或使用局部变量来替代。
  • 对于必须使用静态变量的场景,我会在代码中添加详细的日志和断言来监控静态变量的状态变化,以便在出现问题时能够快速定位。
  • 此外,我还会使用静态分析工具来检查代码中的潜在问题,如内存泄漏、数据竞争等。这些工具可以帮助我发现并修复一些难以手动发现的错误。

5. 您如何理解堆和栈的区别,以及它们在嵌入式系统中的应用?

在嵌入式移动通信领域,堆和栈是两种重要的内存管理机制,它们在数据结构、内存分配方式、存储内容以及访问方式等方面存在显著差异。

堆和栈的区别

  1. 数据结构

    • 栈(Stack):是一种后进先出(LIFO,Last In First Out)的线性数据结构。在栈中,数据的存取方式是“先进后出”,即最后插入的元素最先被移除。
    • 堆(Heap):是一种树形结构,用于动态分配内存。在堆中,数据的存取方式没有固定的规律,可以随时任意读写。
  2. 内存分配方式

    • 栈:栈内存由系统自动分配和管理,通常是在编译阶段就确定了分配给栈的内存空间。栈内存的释放也是由系统自动完成的,当函数执行完毕或局部变量超出作用域时,栈内存会自动释放。
    • 堆:堆内存由程序员手动分配和释放。在程序运行期间,程序员可以向操作系统请求动态分配一段空间,并在使用完之后再手动释放这段空间。
  3. 存储内容

    • 栈:通常用于存储函数的局部变量、函数参数、返回地址等。栈内存是线程私有的,每个线程都有自己的栈空间。
    • 堆:通常用于存储程序中动态分配的对象,如通过newmalloc等函数分配的空间。堆内存是所有线程共有的,可以被多个线程共享。
  4. 访问方式

    • 栈:栈是一种后进先出的数据结构,只有栈顶的元素可以被访问和操作。栈的访问速度通常较快,因为栈内存是连续的,可以直接通过指针操作。
    • 堆:堆中的元素可以通过指针或引用来访问。由于堆内存是不连续的,访问速度可能相对较慢,且需要额外的指针操作。
  5. 内存分配效率

    • 栈:栈的内存分配效率较高,因为栈内存是连续的,且由系统自动管理。栈内存分配和释放的速度通常较快。
    • 堆:堆的内存分配效率相对较低,因为堆内存是不连续的,且需要程序员手动管理。堆内存的分配和释放可能涉及复杂的算法和数据结构。

在嵌入式系统中的应用

  1. 栈的应用

    • 栈在嵌入式系统中主要用于存储函数的局部变量、函数参数和返回地址等。由于栈内存是线程私有的,因此它非常适合用于存储线程特有的数据。
    • 栈还可以用于实现函数调用、表达式求值、括号匹配和递归算法等功能。这些功能在嵌入式系统的程序设计和实现中非常重要。
  2. 堆的应用

    • 堆在嵌入式系统中主要用于动态分配内存。当程序需要存储大量的数据或对象时,可以使用堆来动态申请内存空间。
    • 堆还可以用于实现优先队列和堆排序等算法。这些算法在嵌入式系统的数据处理和排序任务中非常有用。

面试官追问及回答

面试官追问1:在嵌入式系统中,堆和栈的内存分配和释放需要注意哪些问题?

回答:在嵌入式系统中,堆和栈的内存分配和释放需要注意以下问题:

  • 对于栈内存,由于它是自动分配和释放的,因此需要注意避免栈溢出的问题。当函数递归调用过深或局部变量过多时,可能会导致栈溢出,从而引发系统崩溃。
  • 对于堆内存,由于它是手动分配和释放的,因此需要注意避免内存泄漏和内存碎片的问题。内存泄漏会导致系统内存资源耗尽,而内存碎片会降低内存利用效率。为了解决这个问题,可以使用智能指针、内存池等技术来管理堆内存。

面试官追问2:在嵌入式系统中,如何选择合适的内存分配策略来优化程序性能?

回答:在嵌入式系统中,选择合适的内存分配策略来优化程序性能需要考虑以下因素:

  • 程序的需求:根据程序的需求选择合适的内存分配策略。如果程序需要存储大量的数据或对象,可以考虑使用堆内存来动态分配空间;如果程序对内存访问速度要求较高,可以考虑使用栈内存来存储局部变量和函数参数等。
  • 系统的资源:根据系统的资源情况选择合适的内存分配策略。如果系统内存资源有限,需要谨慎使用堆内存,避免内存泄漏和内存碎片的问题;如果系统内存资源充足,可以考虑使用更灵活的内存分配策略来满足程序的需求。
  • 程序的实时性:如果程序对实时性要求较高,需要确保内存分配和释放的时间开销较小。在这种情况下,可以考虑使用静态内存分配或内存池等技术来减少内存分配和释放的时间开销。

面试官追问3:在嵌入式系统中,如何检测和处理内存泄漏问题?

回答:在嵌入式系统中,检测和处理内存泄漏问题可以采取以下措施:

  • 使用内存泄漏检测工具:可以使用一些专门的内存泄漏检测工具来检测程序中的内存泄漏问题。这些工具通常会在程序运行时监控内存的使用情况,并报告内存泄漏的位置和大小等信息。
  • 编写代码时注意内存管理:在编写代码时,需要注意内存的管理问题。避免使用未初始化的指针、重复释放内存、忘记释放内存等常见的内存管理错误。同时,可以使用智能指针等数据结构来自动管理内存的生命周期。
  • 定期进行代码审查和测试:定期进行代码审查和测试是发现和解决内存泄漏问题的重要手段。通过代码审查和测试,可以发现潜在的内存泄漏问题,并及时进行修复和优化。

6. 您在项目中使用过哪些无线通信协议,例如蓝牙或Wi-Fi,能否分享一些实际的开发经验?

在嵌入式移动通信项目中,我使用过多种无线通信协议,其中蓝牙和Wi-Fi是最为常见的两种。

蓝牙开发经验

项目背景: 在一个智能家居项目中,我负责开发一个蓝牙智能灯泡控制器。该控制器需要与手机APP进行通信,以实现灯泡的开关、亮度调节等功能。

技术选型: 考虑到蓝牙的低功耗和广泛普及性,我们选择了蓝牙4.2作为通信协议。蓝牙4.2不仅支持传统的经典蓝牙(SPP、HID等),还引入了低功耗蓝牙(BLE),非常适合智能家居等需要长时间待机和频繁通信的应用场景。

开发过程

  1. 硬件设计:选择了支持蓝牙4.2的MCU,并设计了相应的电路板和天线布局。
  2. 软件开发:使用蓝牙协议栈(如TI的CC254x系列蓝牙协议栈)进行开发。首先,配置了蓝牙设备的名称、服务UUID等基本信息。然后,实现了蓝牙设备的广告、扫描、连接、数据传输等功能。在数据传输部分,我们定义了自定义的蓝牙特性(Characteristic),用于传输灯泡的控制指令和状态信息。
  3. 调试与优化:在开发过程中,遇到了蓝牙连接不稳定、数据传输延迟等问题。通过调整蓝牙广告的间隔、优化数据传输的速率和格式、增加错误重传机制等方式,最终解决了这些问题。

经验总结: 蓝牙开发需要注意硬件和软件的协同设计,特别是天线布局和蓝牙协议栈的配置。此外,蓝牙通信的可靠性和稳定性也是开发过程中需要重点关注的问题。

Wi-Fi开发经验

项目背景: 在一个智能安防项目中,我负责开发一个Wi-Fi摄像头。该摄像头需要将拍摄的视频实时传输到云端服务器进行存储和分析。

技术选型: 考虑到Wi-Fi的高速率和广泛覆盖性,我们选择了Wi-Fi作为通信协议。同时,为了降低开发难度和提高系统的稳定性,我们选择了成熟的Wi-Fi模块(如ESP8266)进行开发。

开发过程

  1. 硬件设计:选择了支持Wi-Fi的MCU或Wi-Fi模块,并设计了相应的电路板和天线布局。同时,设计了摄像头的摄像头接口、电源电路等。
  2. 软件开发:使用Wi-Fi模块的SDK进行开发。首先,配置了Wi-Fi模块的SSID、密码等网络参数。然后,实现了Wi-Fi的连接、TCP/IP通信等功能。在视频传输部分,我们使用了RTP(实时传输协议)进行视频流的封装和传输。
  3. 调试与优化:在开发过程中,遇到了Wi-Fi连接速度慢、视频传输卡顿等问题。通过调整Wi-Fi的连接参数、优化视频流的编码和传输速率、增加网络重连机制等方式,最终解决了这些问题。

经验总结: Wi-Fi开发需要注意网络参数的配置和网络通信的稳定性。此外,由于Wi-Fi通信的速率较高,因此需要注意数据的实时性和完整性。在视频传输等应用场景中,还需要考虑视频流的编码和传输协议的选择。

面试官追问及回答

面试官追问1:在蓝牙和Wi-Fi开发中,您是如何处理低功耗和高效能之间的平衡的?

回答: 在蓝牙开发中,我们选择了低功耗蓝牙(BLE)作为通信协议,并通过优化广告间隔、数据传输速率和格式等方式来降低功耗。同时,我们还在软件层面实现了智能休眠和唤醒机制,以进一步降低设备的待机功耗。在Wi-Fi开发中,我们选择了低功耗的Wi-Fi模块,并通过优化网络连接参数、减少不必要的网络通信等方式来降低功耗。此外,我们还通过硬件层面的电源管理电路来确保设备在高效能运行的同时保持低功耗。

面试官追问2:在蓝牙和Wi-Fi通信中,您是如何处理数据安全和隐私保护的?

回答: 在蓝牙和Wi-Fi通信中,我们采用了多种安全措施来确保数据的安全和隐私保护。首先,我们使用了加密的通信协议(如AES加密)来传输敏感数据。其次,我们实现了设备认证和授权机制,确保只有合法的设备才能接入网络并进行通信。此外,我们还通过定期更新固件和协议栈来修复已知的安全漏洞,并加强了对潜在安全威胁的监控和防范。

面试官追问3:在蓝牙和Wi-Fi开发中,您遇到过哪些最具有挑战性的技术难题,并如何解决的?

回答: 在蓝牙开发中,我们遇到的一个最具有挑战性的技术难题是蓝牙连接的不稳定性。为了解决这个问题,我们进行了大量的测试和调试工作,包括调整蓝牙广告的间隔、优化数据传输的速率和格式、增加错误重传机制等。最终,我们成功地提高了蓝牙连接的稳定性和可靠性。在Wi-Fi开发中,我们遇到的一个最具有挑战性的技术难题是视频传输的卡顿问题。为了解决这个问题,我们优化了视频流的编码和传输速率,并增加了网络重连机制。同时,我们还对Wi-Fi模块进行了深入的性能调优和参数配置工作。最终,我们成功地解决了视频传输卡顿的问题,并提高了系统的整体性能和稳定性。

7. 在Python编程中,with关键字的主要作用是什么?

在Python编程中,with关键字的主要作用是用于简化资源管理的代码,特别是在处理需要显式关闭或释放的资源时,如文件、线程锁、数据库连接等。通过使用with语句,可以确保这些资源在使用完毕后能够被正确地关闭或释放,即使在发生异常的情况下也能保证这一点。

with关键字的作用机制

with语句的基本语法如下:

with expression as variable:
    do_something(variable)

其中,expression是一个上下文管理器(context manager)对象,它必须实现两个特殊方法:__enter__()__exit__()。当with语句开始执行时,__enter__()方法被调用,并且它的返回值(如果有的话)会被赋值给as子句中的variable。当with语句块执行完毕后,无论是正常结束还是由于异常而结束,__exit__()方法都会被调用。

with关键字的好处

  1. 简化代码:避免了手动关闭或释放资源的繁琐代码,使代码更加简洁易读。
  2. 异常安全:即使在with语句块中发生异常,资源也能被正确关闭或释放,避免了资源泄露等问题。
  3. 增强可读性:通过明确的上下文管理,使得代码的逻辑更加清晰,易于理解和维护。

示例

以下是一个使用with语句处理文件的示例:

with open('example.txt', 'r') as f

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

ARM/Linux嵌入式真题 文章被收录于专栏

让实战与真题助你offer满天飞!!! 每周更新!!! 励志做最全ARM/Linux嵌入式面试必考必会的题库。 励志讲清每一个知识点,找到每个问题最好的答案。 让你学懂,掌握,融会贯通。 因为技术知识工作中也会用到,所以踏实学习哦!!!

全部评论

相关推荐

点赞 7 评论
分享
牛客网
牛客企业服务