字节跳动面试流程笔记

嵌入式软开 一面

日期: 2025 年 8 月 1 日

职位: 嵌入式软开


一、整体流程与时长

阶段 时长(分钟) 内容概览
一、简历 & 项目深挖 20 自我介绍 → 项目背景 → 细节追问
二、八股问答(概念题) 10 C++多态,Linux内存,RTOS线程,堆栈区别等
三、手写代码(算法题) 20 寻找下一个更大的数 (ACM)自己写所有

二、阶段一:简历 & 项目深挖(20 分钟)

  1. 自我介绍

    • 按照自己的实际情况答
  2. 工作经历的内容

    • 重点介绍一段工作的具体内容
    • 明确你的角色、职责范围、以及成功
  3. 技术细节追问

    • 相关实现细节,但是不是垂直方向,很多面试官不太懂,没有细节追问

三、阶段二:八股问答(概念题,10 分钟)

1.虚函数和纯虚函数的区别

纯虚函数必须在派生类实现

#include <iostream>

class Animal {
public:
    virtual void speak() {            // 虚函数
        std::cout << "Animal sound" << std::endl;
    }
    virtual void move() = 0;          // 纯虚函数
};

// 派生类必须实现 move()
class Dog : public Animal {
public:
    void speak() override {           // 重写虚函数
        std::cout << "Woof!" << std::endl;
    }
    void move() override {            // 实现纯虚函数
        std::cout << "Dog runs" << std::endl;
    }
};

int main() {
    // Animal a;    // 错误:Animal 是抽象类,包含纯虚函数 move()
    Dog d;
    Animal* p = &d;
    p->speak();  // 调用 Dog::speak() —— 多态
    p->move();   // 调用 Dog::move()
    return 0;
}

Tip:抽象类(abstract class)是一种包含纯虚函数(pure virtual function)的类,用来定义接口规范,不能被实例化,只能被继承并由派生类实现其所有纯虚函数后才能实例化。

2.堆和栈的定义

我是举例说明的,用的链表,当然用下面的更简单:

void foo() {
    int a = 10;               // 在栈上分配
    int* p = new int[1000];   // 在堆上分配
    // ...
    delete[] p;               // 释放堆内存
} // 函数返回时,a 自动出栈

3.函数定义的struct是在堆还是栈

(应该是这个问题,早上面试刚睡醒有点懵,不知道是不是这个,按这个回答的)

分配方式 示例语法 存储位置 管理
自动(栈) Point p; 编译器自动
动态(堆) new Point / malloc 程序员/运行时
静态(数据段) static Point s; 数据段(BSS/RO) 程序开始/结束

反思:当时问题一连串,有点多,也不知道回答对了没。

4.RTOS线程

这个我很久远了,记不清楚了,没有回答上来细节,以下是答案:



---

### RTOS 中的线程(Task)

1. **基本概念**

   * 在大多数 RTOS(如 FreeRTOS、ThreadX、μC/OS-II)里,线程(也常称为 Task)是系统调度的最小单位。
   * 每个线程拥有自己独立的**栈空间**、**程序计数器(PC)**、**寄存器上下文**和若干**线程局部变量**。

2. **调度机制**

   * **优先级调度**:RTOS 通常支持多级优先级,数值越高优先级越高。调度器总是选取就绪队列中最高优先级的线程运行。
   * **时间分片**(可选):同一优先级下可启用时间片 Round-Robin,使各线程轮流运行。
   * **抢占 vs 非抢占**:大多数 RTOS 都支持抢占式调度,即当更高优先级的线程变为就绪态时,立即中断当前线程并切换。

3. **确定性与开销**

   * **上下文切换时间短**:RTOS 内核极简,系统调用、调度开销通常在几十到几百微秒级。
   * **可预测性**:RTOS 保证最坏延迟边界(Worst-Case Latency),关键任务能在可控时间内获得 CPU。

4. **线程间通信和同步**

   * **消息队列(Message Queue)**、**信号量(Semaphore/Mutex)**、**事件标志(Event Flags)**、**邮箱(Mailbox)** 等。
   * 这些机制都设计得十分轻量,且大多都是**禁止中断时可用**、在中断上下文与线程上下文之间安全交互。

5. **栈管理**

   * 每个线程需分配固定大小的栈,栈大小由开发者在创建时指定。
   * RTOS 往往提供**栈水印检查**功能,方便在线检测栈溢出。

5.mmap 在linux

不太会,没做准备,下面是答案:

后期再看。

在 Linux 中,`mmap` 是一个非常重要的系统调用(和库函数封装),它可以将一个文件或者匿名内存区域映射到进程的虚拟地址空间,从而用内存指针的方式直接读写文件或共享内存,而不必再通过 `read`/`write` 系统调用。它的原型定义在 `<sys/mman.h>` 中:

```c
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
int   munmap(void *addr, size_t length);
```

---

### 一、参数详解

| 参数       | 含义                                                                                                  |
| -------- | --------------------------------------------------------------------------------------------------- |
| `addr`   | 建议的映射起始地址,通常设为 `NULL` 让内核自动选择。                                                                      |
| `length` | 要映射的字节长度,**必须**按页(通常 4 KiB)对齐。                                                                      |
| `prot`   | 访问权限,按位或:<br>`PROT_READ` 读 / `PROT_WRITE` 写 / `PROT_EXEC` 可执行 / `PROT_NONE` 无权限                     |
| `flags`  | 映射类型和选项,包括:<br>`MAP_SHARED`(写入同步回文件)<br>`MAP_PRIVATE`(写时拷贝)<br>`MAP_ANONYMOUS`(匿名映射,不关联文件,`fd` 忽略)等 |
| `fd`     | 要映射的文件描述符;若 `flags` 包含 `MAP_ANONYMOUS`,则须设为 `-1`。                                                   |
| `offset` | 文件偏移量,映射从文件的哪个字节开始;必须是页大小的整数倍。                                                                      |

调用成功时返回映射区的起始地址;失败时返回 `(void*)-1`,并将 `errno` 设为错误码。

---

### 二、常用场景

1. **文件 I/O 加速**
   直接将文件内容映射到内存,随机读写就像操作数组,避免多次系统调用和内核缓冲区拷贝。

2. **进程间共享内存(IPC)**
   两个进程都 `mmap` 同一文件(或使用 `MAP_ANONYMOUS|MAP_SHARED` 加上 `fork`),即可在用户空间共享数据。

3. **申请大块匿名内存**
   `mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)` 相当于 `malloc`,但更易控制对齐、权限,也可配合 `mprotect` 动态调整权限。

4. **内存映射设备**
   通过 `/dev/mem` 或特定设备驱动的 `mmap` 接口,将外设寄存器空间映射到用户态。

---

### 三、示例代码

```c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <file>\n", argv[0]);
        exit(1);
    }
    const char *path = argv[1];
    int fd = open(path, O_RDWR);
    if (fd < 0) { perror("open"); exit(1); }

    struct stat st;
    if (fstat(fd, &st) < 0) { perror("fstat"); exit(1); }

    size_t len = st.st_size;
    // 1. 文件共享读写映射
    char *data = mmap(NULL, len,
                      PROT_READ | PROT_WRITE,
                      MAP_SHARED,
                      fd, 0);
    if (data == MAP_FAILED) { perror("mmap"); exit(1); }

    // 2. 修改文件内容(内存修改自动写回)
    for (size_t i = 0; i < len; i++) {
        if (data[i] == 'a') data[i] = 'A';
    }

    // 3. 取消映射并关闭
    if (munmap(data, len) < 0) { perror("munmap"); exit(1); }
    close(fd);
    return 0;
}
```

---

### 四、常见注意事项

* **页对齐**:`length` 和 `offset` 必须是系统页面大小(`getpagesize()`)的整数倍,否则会被向下取整。
* **写时拷贝**:`MAP_PRIVATE` 下对映射区的写操作不会回写文件,而是创建私有的物理页。
* **同步持久化**:`MAP_SHARED` 写后可用 `msync` 强制刷回磁盘;不调用 `msync` 时写入仍可能延迟。
* **权限变更**:可用 `mprotect` 在运行时变更映射区的 `PROT_*`。
* **资源释放**:使用完必须调用 `munmap`,否则映射区会一直占用虚拟地址和内核资源。
* **错误检查**:映射失败时需检查 `errno`,如 `ENOMEM`(内存不足)、`EACCES`(权限不足)等。

---

### 五、内核实现概要

* **VMA(虚拟内存区域)**:进程的 `mm_struct` 维护一个红黑树(或链表)管理所有 `mmap` 区间,每个区间是一个 `struct vm_area_struct`。
* **按需分页**:对映射区首次访问时触发缺页中断,内核查找对应 VMA、调用 `file->mmap` 或匿名页分配,建立物理页映射。
* **卸载映射**:`munmap` 时拆除页表项,回收 VMA 结构,并视情况写回或丢弃脏页。

---

**总结**:

* `mmap` 在用户态提供了高效的“内存—文件”或“进程间”共享映射机制,
* 正确使用可以大幅提升大文件 I/O 或 IPC 的性能,
* 也需要注意对齐、同步和资源管理。


四、阶段三:手写代码(算法题,20 分钟)

题目描述 给定一个正整数 n,找出由它的各位数字重新排列后,比 n 大的最小整数。如果不存在这样的排列,就返回 –1。

示例

输入 输出 说明
1243 1324 下一个比 1243 大的最小数
1234 1243 自然升序后的下一个
4321 –1 已经是降序,无法更大
115 151 注意有重复数字

解题思路:经典“下一个排列”算法

要在 O(n) 时间内就地(原地)找到“下一个”字典序排列,核心分为四步:

  1. 从右向左找到第一个升序拐点 找最大的索引i,使得 a[i] < a[i+1]

    • 如果不存在这样的 i,说明整个序列是非增(降序)的,已经是最大排列 → 返回 –1。
  2. 在后缀中找到比 a[i] 大的最小元素 从数组尾部向左找到第一个 j,满足 a[j] > a[i]

    • 这样交换后能保证新数比原先大,但尽可能小。
  3. 交换 a[i]a[j] 保证前缀增大最少量。

  4. i+1 至末尾的后缀反转 原本后缀是降序的,反转后变升序,得到最小的尾部排列。

详细步骤举例

1243 为例:

原数列:1 2 4 3
步骤1:从右找拐点 a[i]<a[i+1]
       i 从 2(值 4)开始:
       a[2]=4 ≥ a[3]=3 → i-- 变 1(值 2)
       a[1]=2 < a[2]=4 → 拐点 i=1

步骤2:在后缀 [2..3] (4,3) 找第一个 > a[1]=2 的元素
       从尾部开始 j=3:a[3]=3 > 2 → j=3

步骤3:交换 a[1] 与 a[3]
       1 3 4 2

步骤4:反转后缀 i+1..末尾 → 位置 2..3:
       后缀原 [4,2] → 反转为 [2,4]
最终:1 3 2 4 → 1324

C++ 实现

#include <iostream>
#include <string>
#include <algorithm>
#include <climits>
using namespace std;

int nextGreaterNumber(int n) {
    string s = to_string(n);
    int i = s.size() - 2;  // 从倒数第二个字符开始

    // 1. 找拐点
    while (i >= 0 && s[i] >= s[i+1]) --i;
    if (i < 0) return -1;  // 已经是最大排列

    // 2. 找后缀中第一个比 s[i] 大的
    int j = s.size() - 1;
    while (s[j] <= s[i]) --j;

    // 3. 交换
    swap(s[i], s[j]);

    // 4. 反转后缀
    reverse(s.begin() + i + 1, s.end());

    // 转回整数并防溢出
    long long m = stoll(s);
    return (m > INT_MAX ? -1 : (int)m);
}

int main() {
    int n;
    // 从标准输入读取一个正整数
    while (cin >> n) {
        int ans = nextGreaterNumber(n);
        if (ans == -1) {
            cout << "-1";
        } else {
            cout << ans;
        }
        cout << "\n";
    }
    return 0;
}

复杂度分析

边界与注意事项

  1. 无更大排列:当整个序列为降序时,返回 –1。
  2. 重复数字:算法同样适用,交换和反转逻辑不变。
  3. 整数溢出:转换回整数后,要判断是否超出 32 位限制,否则返回 –1。
  4. 返回值类型:题目可按需求返回整数、字符串或原地修改数组。

五、结束与问答环节

我问了为什么这是秋招但是发实习生的岗位面试邀请,面试官也不知道XD。。。


六、准备建议

最好就是放松,也不用着急,因上努力,果上随缘,放平心态,面试越多越熟练。


本记录由张子健整理,用于面试复盘与自我提升,欢迎交流改进意见。