一、Android 中的 Linux 系统调用 Android 基于 Linux 内核,所有底层操作最终都通过系统调用(syscall)完成。NDK 开发中可以直接使用 POSIX API,这些 API 内部封装了系统调用:
类别
系统调用
函数封装
AOSP 使用场景
文件 I/O
open/read/write/close
fopen/fread/fwrite/fclose
文件读写
进程管理
fork/execve/waitpid
fork/exec/wait
Zygote 进程孵化
内存管理
mmap/munmap/brk
malloc/free(内部调用)
Binder 内存映射、匿名共享内存
线程同步
futex/clone
pthread_create/pthread_mutex_lock
所有线程操作
网络 I/O
socket/bind/connect/send/recv
getaddrinfo/connect
网络通信
设备控制
ioctl
OS 封装函数
HAL 层与硬件通信
文件监控
inotify_add_watch
FileObserver(Java API)
文件变化监听
Android 的系统调用表在 bionic/libc/kernel/uapi/asm-generic/unistd.h 中定义。32 位 ARM 架构使用 __NR_ 前缀(如 __NR_openat),通过 svc #0 指令触发;64 位 ARM 使用 svc #0 但系统调用号表不同。
1.1 系统调用的底层细节 从C函数调用到内核执行,系统调用经历了以下步骤:
用户态 C 函数 (open) → libc 封装函数 (将 fd/路径/标志放入寄存器) → svc #0 (ARM64) / syscall (x86) 触发同步异常 → 内核异常向量表 → el0_sync 处理 → 根据系统调用号从 sys_call_table 查找内核函数 → 执行内核中的 sys_open 函数 → 复制用户空间参数 (copy_from_user) → 执行实际的文件打开操作 → 返回结果 → 恢复用户态寄存器上下文 → 用户态继续执行
在 Android Bionic 中,系统调用封装位于 bionic/libc/arch-arm64/syscalls/。每个系统调用都是一个汇编函数,将参数移到正确的寄存器(x0-x5),将系统调用号放入 x8,然后执行 svc #0。
二、文件 I/O:系统调用 vs C 库 Android NDK 中,文件 I/O 有两种方式:
2.1 无缓冲 I/O(系统调用) #include <fcntl.h> #include <unistd.h> int fd = open("/sdcard/test.txt" , O_RDWR | O_CREAT | O_TRUNC, 0644 );if (fd < 0 ) { perror("open failed" ); return ; } const char *data = "Hello from native code\n" ;ssize_t written = write(fd, data, strlen (data));if (written < 0 ) { perror("write failed" ); } lseek(fd, 0 , SEEK_SET); char buffer[128 ];ssize_t nread = read(fd, buffer, sizeof (buffer) - 1 );if (nread > 0 ) { buffer[nread] = '\0' ; } close(fd);
open、read、write、close 是系统调用,每次调用都涉及用户态到内核态的切换,开销较大。
2.2 缓冲 I/O(C 标准库) #include <stdio.h> FILE *fp = fopen("/sdcard/test.txt" , "w+" ); if (fp == NULL ) { perror("fopen failed" ); return ; } fprintf (fp, "Line %d\n" , 1 );fputs ("Hello from native\n" , fp);fflush(fp); rewind(fp); char line[256 ];while (fgets(line, sizeof (line), fp) != NULL ) { printf ("Read: %s" , line); } fclose(fp);
FILE* 系列函数在用户态维护了一个缓冲区(默认 8KB),减少了系统调用的次数。但当需要精确的控制(如实时性要求高的场景、需要对文件描述符进行 poll/epoll/select 操作)时,应该使用无缓冲 I/O。
2.3 实用技巧:mmap 文件映射 #include <sys/mman.h> #include <sys/stat.h> int fd = open("/sdcard/large_file.bin" , O_RDONLY);struct stat st ;fstat(fd, &st); void *mapped = mmap(NULL , st.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );if (mapped == MAP_FAILED) { perror("mmap failed" ); close(fd); return ; } process_data((uint8_t *)mapped, st.st_size); munmap(mapped, st.st_size); close(fd);
Android 的 ART 运行时加载 DEX/OAT 文件时使用 mmap 将其直接映射到进程空间,从而实现高效的代码和数据访问。Binder 驱动的数据传输也基于 mmap(一次性映射 1MB-16KB 的内核缓冲区,后续数据传输无需额外拷贝)。
源码路径:frameworks/native/libs/binder/ProcessState.cpp → mmap(NULL, BINDER_VM_SIZE, ...)
2.4 mmap 的深入:MAP_SHARED vs MAP_PRIVATE vs MAP_ANONYMOUS void *priv = mmap(NULL , file_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0 ); void *shared = mmap(NULL , 4096 , PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ); void *anon = mmap(NULL , 4096 , PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1 , 0 );
三、进程管理:Zygote 模型 Android 应用进程不是通过传统 fork+exec 创建的,而是通过 Zygote 进程 的 fork 机制孵化,这极大地加速了应用启动。
init 进程 → Zygote 进程 (app_process / system/bin/app_process64) 预加载 Framework 类、JNI 库、通用资源 → fork() → 应用进程 1 (com.example.app1) → fork() → 应用进程 2 (com.example.app2) → fork() → 应用进程 3 (com.example.app3)
Zygote 的精妙之处在于 Linux fork 的 Copy-on-Write(CoW)特性:
fork() 后,子进程与父进程共享物理内存页(只读)。
只有当子进程尝试写入某一页时,内核才会为该页创建一份副本。
因此,Zygote 预加载的 Framework 代码和资源在所有应用进程之间共享,大大减少了整体内存占用。
在 NDK 中,fork() 可以直接使用,但 Android 对其有限制:
#include <unistd.h> #include <sys/wait.h> pid_t pid = fork();if (pid == 0 ) { execl("/system/bin/logcat" , "logcat" , "-d" , NULL ); _exit(0 ); } else if (pid > 0 ) { int status; waitpid(pid, &status, 0 ); if (WIFEXITED(status)) { printf ("Child exited with %d\n" , WEXITSTATUS(status)); } } else { perror("fork failed" ); }
fork() 后子进程中的注意事项:
只有调用 fork() 的线程被复制到子进程。其它线程消失(可能导致持有的锁无法释放,造成死锁)。
子进程必须使用 _exit() 而不是 exit(),因为 exit() 会调用 atexit 注册的清理函数。
子进程不能安全地使用父进程的 JNIEnv(需要通过 AttachCurrentThread 重新获取)。
3.1 posix_spawn: fork + exec 的安全替代 fork 在多线程程序中是不安全的。posix_spawn 是更安全的替代方案:
#include <spawn.h> pid_t child_pid;char *argv[] = { "/system/bin/logcat" , "-d" , NULL };extern char **environ;int result = posix_spawn(&child_pid, "/system/bin/logcat" , NULL , NULL , argv, environ); if (result == 0 ) { int status; waitpid(child_pid, &status, 0 ); }
posix_spawn 的优势:直接在内核中创建新进程并加载新程序,避免了 fork 后到 exec 之间可能出现的竞争条件。
四、信号处理 信号(Signal)是 Linux 中进程间异步通知的机制。Android 的 tombstone(崩溃日志)就是由 debuggerd 捕获 crash 信号后生成的。
#include <signal.h> #include <string.h> void crash_handler (int sig, siginfo_t *info, void *ucontext) { const char *sig_name = "UNKNOWN" ; switch (sig) { case SIGSEGV: sig_name = "SIGSEGV" ; break ; case SIGABRT: sig_name = "SIGABRT" ; break ; case SIGFPE: sig_name = "SIGFPE" ; break ; case SIGILL: sig_name = "SIGILL" ; break ; } write(STDERR_FILENO, "Caught signal: " , 15 ); write(STDERR_FILENO, sig_name, strlen (sig_name)); write(STDERR_FILENO, "\n" , 1 ); signal(sig, SIG_DFL); raise(sig); } void install_crash_handler () { struct sigaction sa ; memset (&sa, 0 , sizeof (sa)); sa.sa_sigaction = crash_handler; sa.sa_flags = SA_SIGINFO | SA_ONSTACK; sigaction(SIGSEGV, &sa, NULL ); sigaction(SIGABRT, &sa, NULL ); sigaction(SIGFPE, &sa, NULL ); }
Android 的 debuggerd 守护进程(源码路径:system/core/debuggerd/)通过 ptrace attach 到 crash 的进程,读取寄存器、调用栈、内存映射等信息,生成 tombstone 文件(保存在 /data/tombstones/)。NDK 开发中分析 native crash 的首要工具就是 tombstone 和 ndk-stack。
信号处理器的限制:
只能使用异步信号安全的函数(man 7 signal-safety)。
不能调用 malloc/free、printf/fprintf、pthread_mutex_lock 等。
安全的函数主要是:write、read、open、close、_exit、signal、raise。
推荐使用 SA_ONSTACK 标志,为信号处理器分配独立栈空间(避免栈溢出时信号处理器无法执行)。
4.1 signalfd:将信号变成文件描述符 传统的信号处理(signal handler)是异步的,不便于在事件循环中统一处理。Linux 的 signalfd 能将信号转换为一个文件描述符,纳入 epoll 等 I/O 复用框架:
#include <sys/signalfd.h> #include <signal.h> #include <unistd.h> sigset_t mask;sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); sigprocmask(SIG_BLOCK, &mask, NULL ); int sfd = signalfd(-1 , &mask, SFD_NONBLOCK | SFD_CLOEXEC);struct epoll_event ev ;ev.events = EPOLLIN; ev.data.fd = sfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev); struct signalfd_siginfo si ;ssize_t n = read(sfd, &si, sizeof (si));if (n == sizeof (si)) { printf ("Received signal: %d\n" , si.ssi_signo); }
五、epoll:高效 I/O 多路复用 epoll 是 Linux 特有的 I/O 多路复用机制,Android 的 MessageQueue 和网络框架底层都依赖它(Java 层的 Looper → Native 层的 Looper::pollInner → epoll_wait):
#include <sys/epoll.h> #define MAX_EVENTS 64 int epfd = epoll_create1(EPOLL_CLOEXEC); if (epfd < 0 ) { perror("epoll_create1" ); return ; } struct epoll_event ev ;ev.events = EPOLLIN | EPOLLET; ev.data.fd = server_fd; if (epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev) < 0 ) { perror("epoll_ctl" ); return ; } struct epoll_event events [MAX_EVENTS ];while (1 ) { int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1 ); if (nfds < 0 ) { if (errno == EINTR) continue ; perror("epoll_wait" ); break ; } for (int i = 0 ; i < nfds; i++) { if (events[i].events & EPOLLIN) { int fd = events[i].data.fd; } else if (events[i].events & (EPOLLERR | EPOLLHUP)) { epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL ); close(events[i].data.fd); } } } close(epfd);
epoll 的两种触发模式:
水平触发(Level Triggered) :只要 fd 有可读数据,每次 epoll_wait 都会返回该事件。类似 poll,编程简单。
边缘触发(Edge Triggered) :仅在 fd 状态从不可读变为可读时通知一次。需要循环 read 直到返回 EAGAIN,编程复杂但性能更好(减少重复通知)。
Android 的 android::Looper(Native 层)在 system/core/libutils/Looper.cpp 中,使用 epoll 监控文件描述符事件,同时支持定时器(通过 timerfd_create + epoll 实现)。
5.1 timerfd 和 eventfd #include <sys/timerfd.h> int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);struct itimerspec its ;its.it_value.tv_sec = 1 ; its.it_value.tv_nsec = 0 ; its.it_interval.tv_sec = 0 ; its.it_interval.tv_nsec = 500 * 1000 * 1000 ; timerfd_settime(tfd, 0 , &its, NULL ); #include <sys/eventfd.h> int efd = eventfd(0 , EFD_NONBLOCK | EFD_CLOEXEC);uint64_t val = 1 ;write(efd, &val, sizeof (val)); read(efd, &val, sizeof (val));
六、Unix Domain Socket Unix Domain Socket 是同一台设备上进程间通信(IPC)的高效方式。相比 TCP/IP,它不需要经过网络协议栈,性能更高(延迟更低,吞吐更大)。
#include <sys/socket.h> #include <sys/un.h> int server_fd = socket(AF_UNIX, SOCK_STREAM, 0 );struct sockaddr_un addr ;memset (&addr, 0 , sizeof (addr));addr.sun_family = AF_UNIX; strncpy (addr.sun_path, "/data/local/tmp/my_socket" , sizeof (addr.sun_path) - 1 );unlink(addr.sun_path); bind(server_fd, (struct sockaddr *)&addr, sizeof (addr)); listen(server_fd, 5 ); int client_fd = accept(server_fd, NULL , NULL );char buffer[1024 ];ssize_t n = recv(client_fd, buffer, sizeof (buffer), 0 );send(client_fd, "OK" , 2 , 0 ); close(client_fd); close(server_fd); unlink(addr.sun_path);
Android 系统服务的 IPC 通常使用 Binder,但在一些性能敏感的场景(如 SurfaceFlinger 与 App 的 BufferQueue 通信、mediaserver 的音频数据传输)会使用 Unix Domain Socket 或匿名共享内存(ashmem)。
Android 的 installd 守护进程(包管理器后台服务)通过 Unix Domain Socket 接收 pm 命令:
installd → /dev/socket/installd (Unix Domain Socket) → install / uninstall / dexopt 等操作
6.1 SCM_RIGHTS:通过 Unix Socket 传递文件描述符 struct msghdr msg = {0 };struct iovec iov [1];char data[1 ] = {0 };int fd_to_send = open("/path/to/file" , O_RDONLY);union { struct cmsghdr hdr ; char buf[CMSG_SPACE(sizeof (int ))]; } cmsg; msg.msg_control = cmsg.buf; msg.msg_controllen = sizeof (cmsg.buf); msg.msg_iov = iov; msg.msg_iovlen = 1 ; iov[0 ].iov_base = data; iov[0 ].iov_len = sizeof (data); struct cmsghdr *cmsgp = CMSG_FIRSTHDR(&msg);cmsgp->cmsg_level = SOL_SOCKET; cmsgp->cmsg_type = SCM_RIGHTS; cmsgp->cmsg_len = CMSG_LEN(sizeof (int )); memcpy (CMSG_DATA(cmsgp), &fd_to_send, sizeof (int ));sendmsg(socket_fd, &msg, 0 ); struct msghdr rmsg ;recvmsg(socket_fd, &rmsg, 0 ); int received_fd;memcpy (&received_fd, CMSG_DATA(CMSG_FIRSTHDR(&rmsg)), sizeof (int ));
七、匿名共享内存(ashmem) Android 特有的 ashmem 驱动提供了进程间的匿名共享内存:
#include <sys/mman.h> #include <linux/ashmem.h> #include <sys/ioctl.h> int fd = open("/dev/ashmem" , O_RDWR);if (fd < 0 ) { perror("open /dev/ashmem" ); return ; } ioctl(fd, ASHMEM_SET_NAME, "MySharedBuffer" ); ioctl(fd, ASHMEM_SET_SIZE, 4096 ); void *shared_mem = mmap(NULL , 4096 , PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 );munmap(shared_mem, 4096 ); close(fd);
Android 的 MemoryFile(Java API)和 IMemory(Binder 接口)底层都是对 ashmem 的封装。ashmem 的特性是:内核通过引用计数跟踪共享内存的引用者,当所有引用者释放后自动回收内存。
7.1 System V IPC vs POSIX IPC Linux 提供了两套 IPC 机制:
机制
System V
POSIX
说明
共享内存
shmget/shmat/shmdt
shm_open/mmap
POSIX 更易用
信号量
semget/semop
sem_open/sem_wait
POSIX 支持命名和匿名
消息队列
msgsnd/msgrcv
mq_send/mq_receive
POSIX 支持优先级
Android Bionic 全面支持 POSIX IPC,但对 System V IPC 的支持不完整(部分函数为 stub)。在 Android NDK 开发中,优先使用 POSIX IPC。
#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> int shm_fd = shm_open("/my_shm" , O_CREAT | O_RDWR, 0600 );ftruncate(shm_fd, 4096 ); void *shm = mmap(NULL , 4096 , PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0 );munmap(shm, 4096 ); close(shm_fd); shm_unlink("/my_shm" );
八、CMake 构建配置 现代 Android NDK 项目使用 CMake 作为构建系统:
cmake_minimum_required (VERSION 3.18 .1 )project ("nativeutils" )find_library (log-lib log) find_library (z-lib z) add_library (nativeutils SHARED src/main/cpp/jni_main.cpp src/main/cpp/crypto/aes_encrypt.cpp src/main/cpp/ipc/socket_server.cpp src/main/cpp/ipc/socket_client.cpp ) target_include_directories (nativeutils PRIVATE src/main/cpp/include ${CMAKE_CURRENT_SOURCE_DIR} /third_party/openssl/include ) target_link_libraries (nativeutils ${log-lib} ${z-lib} android openssl ) target_compile_options (nativeutils PRIVATE -Wall -Werror -O2 -DANDROID )
app 模块的 build.gradle 中配置:
android { defaultConfig { ndk { abiFilters 'arm64-v8a' , 'armeabi-v7a' , 'x86_64' } } externalNativeBuild { cmake { path "CMakeLists.txt" version "3.18.1" } } }
九、面试常问题目 Q1: mmap 和 read/write 的区别?什么场景下用 mmap?
read/write 需要将数据从内核缓冲区拷贝到用户空间(一次数据拷贝)。mmap 将文件映射到进程的虚拟地址空间,通过缺页中断按需加载数据,不需要显式的 read/write 调用。mmap 适合:(1) 大文件的随机访问(只需访问其中一部分);(2) 多个进程共享同一文件的数据(MAP_SHARED);(3) 零拷贝的数据传输(如 Binder)。read/write 适合小文件、顺序读写。Android 的 DEX/OAT 加载使用 mmap。
Q2: epoll 的水平触发和边缘触发有什么区别?
水平触发(LT):只要 fd 仍有未处理的数据,每次 epoll_wait 都会返回该 fd 的事件。编程简单,与 select/poll 行为一致。边缘触发(ET):只在 fd 状态变化时(如从不可读变为可读)通知一次。必须循环读取直到返回 EAGAIN,否则可能丢失事件。ET 模式的优点是可以避免重复通知,减少 epoll_wait 的调用次数,适合高并发场景。Android Native Looper 默认使用 LT 模式。
Q3: Zygote fork 为什么比直接创建进程快?
Zygote 进程在启动时预加载了 Framework 类、JNI 库、系统资源(主题、字体等)。fork 使用 Copy-on-Write 机制,子进程共享这些预加载的资源,不需要重新加载。直接创建进程(fork + exec)需要一个全新的内存空间初始化过程,要重新执行所有加载步骤。Zygote 模式下,应用启动只需 fork(复制页表,约几毫秒),然后在新进程中初始化自己的少量组件。
Q4: Unix Domain Socket 和 TCP/IP Socket 的区别?在 Android 中如何选择?
Unix Domain Socket 用于同一台设备上的进程间通信,不需要经过 IP 层和 TCP 协议栈,数据在内核中直接传输(不经过网络设备),延迟更低、吞吐更高。TCP Socket 用于网络通信,可跨设备。在 Android 中,同一设备上的 IPC 首选 Binder(有权限管理、引用计数等高级特性),但当需要流式数据传输(如音频流)或需要支持非 Java 进程时,Unix Domain Socket 是合适的补充方案。
Q5: timerfd 和 eventfd 相比传统的定时器和条件变量有什么优势?
timerfd 和 eventfd 将定时器和事件通知统一为文件描述符,可以无缝纳入 epoll/select 等 I/O 多路复用框架中。这意味着一个线程可以用同一个 epoll_wait 同时等待:socket 数据到达 + 定时器超时 + 事件通知,避免了传统的 poll(timeout) + 定时器回调的复杂性。Android 的 Native Looper 正是使用 timerfd 来支持定时消息。eventfd 可用于替代条件变量,在单生产者单消费者场景下,eventfd 的性能优于条件变量(因为在内核中实现,无需用户态的 mutex 保护)。
参考源码路径:
Android Looper(Native):system/core/libutils/Looper.cpp
Zygote 进程:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
Zygote fork 流程:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
Binder mmap:frameworks/native/libs/binder/ProcessState.cpp
debuggerd:system/core/debuggerd/
installd:frameworks/native/cmds/installd/
ashmem 驱动:kernel/common/drivers/staging/android/ashmem.c
Bionic epoll:bionic/libc/bionic/epoll_create.cpp