Linux C 常用函数速查 链接到标题

以下总结 Linux 环境下编程常用函数的原型、用法与注意事项。每个函数注明头文件。


遇到忘了的函数,可以做以下操作 链接到标题

#install man-pages
man -k <大概的名字> # 你如果连个大概名字也没印象,那最好上网搜搜了.

容易犯的错误 链接到标题

这些是我(和我见过的很多人)反复踩坑的点,放在最前面提醒自己。

malloc 后忘记 free(跨函数) 链接到标题

// bad — 函数内分配,调用方忘记释放
char *make_message(const char *name) {
    char *buf = malloc(256);
    snprintf(buf, 256, "hello, %s", name);
    return buf;
}
// ... 100 行之后,调用方直接用完就丢了,泄漏。

痛点:分配和释放不在同一个函数里,写的时候想着"回头再 free",回头就忘了。代码一长,谁分配谁释放就理不清。

解决习惯

  1. 名字上约定: new_xxx() / create_xxx() 表示返回需调用方释放的指针;dup_xxx() 同理。
  2. 就地配对: 能在一个函数内 mallocfree 就不要跨函数。
  3. goto cleanup 模式: 单函数内多分配场景,统一跳到 cleanup: 标签释放,避免每个 return 前漏掉一个。
int do_stuff(int fd) {
    char *buf = NULL;
    int   ret = -1;

    buf = malloc(4096);
    if (!buf) goto cleanup;

    ret = 0;  // success
cleanup:
    free(buf);
    return ret;
}
  1. 用工具查: valgrind --leak-check=full ./a.out,养成跑完程序随手 valgrind 的习惯。

read() 返回值不检查或检查不完整 链接到标题

// bad 1 — 完全不检查
read(fd, buf, sizeof(buf));

// bad 2 — 只判断 == -1,忽略 0 和短读
int n = read(fd, buf, 1024);
if (n == -1) { perror("read"); return; }
// ← n == 0 或 n < 1024 时静默通过,丢掉数据

read() 三种返回值必须全部处理

返回值 含义 不能忽略
> 0 读到 n 字节,可能小于请求量(短读) 必须循环读到预期长度或 EOF
== 0 对端关闭 / 文件结尾 必须关闭 fd 或退出循环
== -1 出错,errno 有具体原因 EINTR 可重试,其他需处理

正确写法 — 循环读满指定长度

ssize_t read_full(int fd, void *buf, size_t count) {
    size_t  left = count;
    while (left > 0) {
        ssize_t n = read(fd, buf, left);
        if (n < 0) {
            if (errno == EINTR) continue;  // 被信号打断,重试
            return -1;                      // 真错
        }
        if (n == 0) break;                  // EOF,不可继续
        left -= n;
        buf  += n;
    }
    return count - left;  // 实际读到的字节数
}

核心原则read() 返回多少就读到多少,不要假设一次循环就拿到预期长度。write() 同理——返回 n < count 时需偏移继续写。

额外提醒

  • 网络编程中 recv() / send() 也有同样问题,别只判断 -1。
  • EAGAIN/EWOULDBLOCK(非阻塞模式下暂无数据)不算错误,可 continue 或用 epoll/poll 等下次就绪。

命令行选项 链接到标题

getopt — 解析命令行选项 链接到标题

#include <unistd.h>

int getopt(int argc, char *const argv[], const char *optstring);

extern char *optarg;
extern int optind, opterr, optopt;

功能:解析 -x 短选项,支持带参数 -x val。循环调用直到返回 -1

注意:每次调用修改 optind;解析完所有选项后 optind 指向第一个非选项参数的位置。多线程不安全。


错误处理 链接到标题

strerror — 错误码转字符串 链接到标题

#include <string.h>

char *strerror(int errnum);

功能:将 errno 值转为可读的错误字符串。

注意:返回的指针是静态 buffer,非线程安全。多线程用 strerror_r()。不要修改返回的字符串。


进程管理 链接到标题

fork — 创建子进程 链接到标题

#include <unistd.h>

pid_t fork(void);

功能:创建子进程,子进程是父进程的写时复制副本。父进程中返回子进程 PID,子进程中返回 0,失败返回 -1。

注意fork 后父子并发执行,调度顺序不确定。fd 默认共享(偏移量也共享)——注意 FD_CLOEXECfork 在大型进程中开销大(页表复制),高并发场景可考虑 vforkclone。子进程只复制调用线程,其他线程不复制。

exec 系列 — 替换进程映像 链接到标题

#include <unistd.h>

int execl(const char *path, const char *arg, ..., (char *) NULL);
int execlp(const char *file, const char *arg, ..., (char *) NULL);
int execle(const char *path, const char *arg, ..., (char *) NULL, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

功能:用新程序替换当前进程。成功不返回,失败返回 -1。

命名规则

后缀 含义
l (list) 参数以变参形式给出,NULL 结尾
v (vector) 参数以 char *argv[] 数组给出
p (path) PATH 中搜索可执行文件(不包含 / 时)
e (environment) 传递自定义环境变量数组,代替 environ

注意execFD_CLOEXEC 标记的 fd 会被关闭,其余的保留。execlpexecvp 如果 file 不含 /,会遍历 PATH 查找,找不到才失败。

wait / waitpid — 等待子进程状态改变 链接到标题

#include <sys/wait.h>

pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);

功能:父进程等待子进程状态变化(终止、停止、恢复)。wait 等任意子进程,waitpid 可指定 pid。

waitpid 的 pid 取值

含义
< -1 等待进程组 ID 为 -pid 的任意子进程
-1 等待任意子进程(等同于 wait
0 等待与本进程同进程组的任意子进程
> 0 等待指定 PID 的子进程

常用 optionsWNOHANG(非阻塞)、WUNTRACED(也返回已停止的子进程)、WCONTINUED(也返回被 SIGCONT 恢复的子进程)。

状态检查宏(传入 wstatus):

含义
WIFEXITED(status) 正常退出 → 用 WEXITSTATUS(status) 取退出码
WIFSIGNALED(status) 被信号杀死 → 用 WTERMSIG(status) 取信号
WIFSTOPPED(status) 被信号暂停 → 用 WSTOPSIG(status) 取信号
WIFCONTINUED(status) SIGCONT 恢复执行

注意必须 wait() 回收子进程,否则产生僵尸进程。SIGCHLD 默认忽略,子进程仍产生僵尸(除非显式设为 SIG_IGNSA_NOCLDWAIT)。如果 SIGCHLD 设为 SIG_IGN,子进程自动回收不产生僵尸。


进程组、会话与终端 链接到标题

进程组 链接到标题

  • 一个或多个进程的集合,由 setpgid() 设置。getpgid(pid) / getpgrp() 获取。
  • shell 为每条管道的各个进程建立进程组。前台进程组接收终端输入和信号(Ctrl+C → SIGINT 发往前台进程组全体)。
  • kill(-pgid, sig) 向整个进程组发信号(负号表示进程组)。

会话 链接到标题

  • setsid() 创建新会话:调用进程成为 session leader 和新进程组的 leader,脱离控制终端。
  • daemon 启动的标准第一步fork → 父进程退出 → 子进程 setsid()
  • 一个会话最多一个控制终端。session leader 打开终端设备时终端成为控制终端。

子进程 链接到标题

  • fork 后父子并发执行,调度顺序不保证。父子关系影响:
    • 子进程退出 → SIGCHLD 发往父进程。
    • 父进程先退出 → 子进程被 init(PID 1)收养(ppid 变为 1)。
    • 父进程不 wait → 子进程变为僵尸(ps 显示 Z 状态),PCB 不释放。
  • 僵尸进程占 PCB 但不占内存/CPU,大量僵尸会耗尽 PID(因为内核需保留 exit_code 等信息)。解决:wait()SIGCHLD 设为 SIG_IGN

信号 链接到标题

signal — 注册信号处理函数(已过时) 链接到标题

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

功能:为 signum 设置处理函数。handler 可以是 SIG_IGN(忽略)、SIG_DFL(默认)或自定义函数指针。

注意不要用 signal(),用 sigaction()signal 在不同 UNIX 实现中行为不一致(SysV 一次性注册 vs BSD 持久注册)。Linux 上 signal 等同于 BSD 语义,但缺乏对信号屏蔽字的控制。

sigaction — 可靠的信号处理 链接到标题

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
    void (*sa_handler)(int);     // SIG_DFL, SIG_IGN 或 handler
    void (*sa_sigaction)(int, siginfo_t *, void *);  // sa_flags 含 SA_SIGINFO 时用
    sigset_t sa_mask;            // handler 执行期间额外屏蔽的信号集
    int sa_flags;                // SA_RESTART, SA_NOCLDSTOP, SA_SIGINFO 等
    void (*sa_restorer)(void);   // 不用填
};

功能:精确控制信号处理行为,取代 signal

关键 flags

flag 含义
SA_RESTART 被该信号中断的慢速系统调用自动重启
SA_SIGINFO 使用 sa_sigaction(三参数 handler,可获得发送者 PID、UID 等信息)
SA_NOCLDWAIT 子进程终止时不产生僵尸(等价于 SIG_IGNSIGCHLD 的效果)
SA_NODEFER handler 执行期间不自动屏蔽本信号(默认会屏蔽)

注意sa_mask 在 handler 执行期间临时加入进程屏蔽字,handler 返回后自动还原。SIGKILLSIGSTOP 不能自定义处理。

kill — 发送信号 链接到标题

#include <signal.h>

int kill(pid_t pid, int sig);

功能:向进程发送信号。pid 语义和 waitpid 相同(>0 指定进程,0 进程组广播,-1 所有进程,<-1 指定进程组)。sig=0 时不发信号,只做权限检查(检测进程是否存在)。

注意kill 不是"杀死",只是发信号。SIGTERM/SIGKILL 才是终止信号。普通用户只能向自己的进程发信号。

alarm — 定时发送 SIGALRM 链接到标题

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

功能seconds 秒后向本进程发 SIGALRM。重复调用会重置计时,返回上次剩余秒数。alarm(0) 取消定时。

注意只能设一个定时器alarmsleep/setitimer 可能相互干扰——它们共享同一内核定时器。不能用于高精度需求(精度 1 秒)。

setitimer — 更精确的间隔定时器 链接到标题

#include <sys/time.h>

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

struct itimerval {
    struct timeval it_interval;  // 周期触发间隔(0 表示单次触发)
    struct timeval it_value;     // 首次触发时间
};

功能:比 alarm 更灵活的定时器,支持微秒精度和周期触发。

which 取值

含义 触发信号
ITIMER_REAL 真实时间 SIGALRM
ITIMER_VIRTUAL 用户态 CPU 时间 SIGVTALRM
ITIMER_PROF 用户+内核态 CPU 时间 SIGPROF

注意ITIMER_VIRTUALITIMER_PROF 用于性能剖析(profiling)。ITIMER_REALalarm 共享 SIGALRM,同时只能用一个。微秒精度受系统时钟粒度限制(通常 4ms / 250Hz)。

sigprocmask — 检查/修改信号屏蔽字 链接到标题

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

功能:修改或获取当前线程的信号屏蔽字(blocked)。被屏蔽的信号不会递送,解除屏蔽后递送(信号不丢失)。

how 取值

含义
SIG_BLOCK set 中的信号加入屏蔽字(并集)
SIG_UNBLOCK set 中的信号从屏蔽字移除
SIG_SETMASK 将屏蔽字设为 set

注意oldset 可传 NULLSIGKILLSIGSTOP 不能被阻塞。子进程通过 fork 继承父进程屏蔽字,exec 后屏蔽字保留(但 handler 重置为默认)。

sigset 操作函数 — 操作信号集 链接到标题

#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);

功能sigset_t 不可跨平台直接用位运算操作,必须用以上函数。sigemptyset 清空,sigfillset 填满所有信号。

注意:信号集使用前必须sigemptysetsigfillset 初始化,否则行为未定义。sigismember 返回 1 表示在集合中,0 表示不在,-1 出错。


慢速系统调用与信号 链接到标题

慢速系统调用指可能永久阻塞的调用:read(pipe/tty/网络)、writeopen(FIFO)、pausewaitacceptconnect 等。

核心问题:慢速系统调用被信号中断后返回 -1errno = EINTR。必须显式处理:

// 典型写法
do {
    n = read(fd, buf, len);
} while (n == -1 && errno == EINTR);

SA_RESTART:设置后,被信号中断的慢速系统调用由内核自动重启,handler 中不需要手写重试。但 selectpollsleepconnect 即使设置了 SA_RESTART 也不会自动重启——这是 POSIX 规定。


线程 链接到标题

线程概念 链接到标题

线程是进程中独立的执行流,同进程的线程共享地址空间、fd 表、信号处理设置、cwd 等。Linux 上线程由 clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | ...) 实现,内核用 task_struct 表示每个线程,tgid 相同的 task 属于同一进程。

线程间通信直接通过共享内存即可,无需 IPC。每个线程有独立的栈、errno、信号屏蔽字。

pthread_create — 创建线程 链接到标题

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);

功能:创建新线程执行 start_routine(arg)。成功返回 0。

注意:编译必须加 -pthread(同时链接 libpthread 并定义 _REENTRANT 等宏)。新线程的调度顺序不保证。attrNULL 用默认属性(joinable + 默认栈大小)。start_routine 通过 return 或 pthread_exit 返回值。

pthread_exit — 线程退出 链接到标题

#include <pthread.h>

void pthread_exit(void *retval);

功能:终止调用线程,retval 作为返回值传给 pthread_join。不会影响进程中的其他线程。

注意:线程函数直接 return 等价于 pthread_exit(返回值)。不要在线程函数中 exit()——那会终止整个进程。不要返回指向线程栈的指针(线程退出后栈被回收)。

pthread_join — 等待线程终止并回收 链接到标题

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

功能:阻塞等待指定线程结束,获取其返回值。类似于进程的 waitpid

注意必须 joindetach 每个线程,否则资源泄漏(类似僵尸进程)。线程必须是 joinable 的(默认),已 detach 的线程不能 join。多个线程不能 join 同一个线程。

pthread_self — 获取本线程 ID 链接到标题

#include <pthread.h>

pthread_t pthread_self(void);

功能:返回调用线程的 ID,类似 getpid() 之于进程。

注意pthread_t 是不透明类型,不要== 比较——用 pthread_equal()。不要手动解析 pthread_t 的值。

pthread_detach — 分离线程 链接到标题

#include <pthread.h>

int pthread_detach(pthread_t thread);

功能:将线程标记为 detached 状态。线程退出时内核自动回收其资源,无需 pthread_join 回收。

注意:已 detach 的线程不能再 join。pthread_detach(pthread_self()) 是常见写法让线程"自生自灭"。join 和 detach 必须二选一,否则资源泄漏。

pthread_cancel — 取消线程 链接到标题

#include <pthread.h>

int pthread_cancel(pthread_t thread);

功能:向指定线程发送取消请求。目标线程在到达取消点(cancellation point)时才终止。默认取消类型为 deferred(延迟取消)。

常见取消点readwritesleeppthread_cond_waitpthread_joinaccept 等阻塞函数。

注意:纯计算循环无取消点,需手动 pthread_testcancel() 插入取消点。pthread_setcancelstate 可关闭取消(PTHREAD_CANCEL_DISABLE),pthread_setcanceltype 可设为异步取消(PTHREAD_CANCEL_ASYNCHRONOUS——危险,不推荐)。取消线程时应用 pthread_cleanup_push/pop 注册清理函数以释放锁和内存。

pthread_attr — 线程属性 链接到标题

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

功能pthread_create 的第二个参数控制线程属性。先用 pthread_attr_init 初始化,设置后用 pthread_attr_destroy 销毁。

detachstate 取值PTHREAD_CREATE_JOINABLE(默认)或 PTHREAD_CREATE_DETACHED(创建即分离,无需再调 pthread_detach)。

注意pthread_attr_destroy 必须与 init 配对,否则内存泄漏。attr 还可设置栈大小(pthread_attr_setstacksize)、栈地址(pthread_attr_setstack)、调度策略(pthread_attr_setschedpolicy)等。


互斥锁 (mutex) 链接到标题

pthread_mutex_init / lock / unlock / trylock / destroy 链接到标题

#include <pthread.h>

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;  // 静态初始化

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

功能:互斥锁保证同一时刻只有一个线程进入临界区。lock 阻塞等待,trylock 立即返回(成功 0,失败 EBUSY)。

注意

  • PTHREAD_MUTEX_INITIALIZER 静态初始化等价于 pthread_mutex_init(&m, NULL),无需 destroy。
  • pthread_mutex_init 动态初始化的锁用完后需 pthread_mutex_destroy
  • 必须 unlock 配对 lock,同一线程不能重复 lock(默认非递归锁)。
  • attr 可设锁类型:PTHREAD_MUTEX_ERRORCHECK(检查死锁)、PTHREAD_MUTEX_RECURSIVE(递归锁)。

读写锁 (rwlock) 链接到标题

#include <pthread.h>

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

功能:允许多个读者并发访问,写者独占。rdlock 可多个线程同时持有,wrlock 写者独占。unlock 统一释放(无论读锁写锁)。

注意:写者优先还是读者优先由实现决定(可能导致写者饥饿)。大量写操作的场景读写锁不如互斥锁——读写锁内部开销更大。tryrdlock/trywrlock 不阻塞,失败返回 EBUSY


条件变量 (cond) 链接到标题

#include <pthread.h>

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
                           const struct timespec *abstime);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_destroy(pthread_cond_t *cond);

功能:条件变量用于线程间的事件通知。wait 原子操作:解锁 mutex → 睡眠等待 → 被唤醒后重新加锁signal 唤醒一个等待者,broadcast 唤醒全部。

注意

  • pthread_cond_wait 调用前必须先 lock mutex,返回后 mutex 已被重新 lock。
  • 等待条件必须用 while 而非 if——防止虚假唤醒(spurious wakeup)。
  • pthread_cond_timedwaitabstime绝对时间gettimeofday + offset 转 timespec),不是相对秒数。
  • signal 后要不要立即 unlock?先 signal 再 unlock 更高效(被唤醒线程不需再次睡眠)。

POSIX 信号量 (sem) 链接到标题

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_destroy(sem_t *sem);

功能sem_wait(P 操作)对信号量减 1,若值为 0 则阻塞;sem_post(V 操作)加 1。pshared=0 表示线程间共享,pshared≠0 表示进程间共享(需放在共享内存中)。

注意:链接需加 -pthread。信号量和条件变量的关键区别:条件变量需要 mutex 配合,信号量自带计数;条件变量适合通知模式,信号量适合资源计数(如连接池)。sem_wait 可能被信号中断。


线程综合示例:生产者-消费者 链接到标题

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define BUF_SIZE 5
int buffer[BUF_SIZE];
int count = 0, in = 0, out = 0;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  not_full  = PTHREAD_COND_INITIALIZER;
pthread_cond_t  not_empty = PTHREAD_COND_INITIALIZER;

void *producer(void *arg) {
    for (int i = 1; i <= 10; i++) {
        pthread_mutex_lock(&mutex);
        while (count == BUF_SIZE)               // ① while 而非 if — 防虚假唤醒
            pthread_cond_wait(&not_full, &mutex);
        buffer[in] = i;
        in = (in + 1) % BUF_SIZE;
        count++;
        printf("[producer] 生产 %d, 库存 %d\n", i, count);
        pthread_cond_signal(&not_empty);          // ② 通知消费者
        pthread_mutex_unlock(&mutex);            // ③ 先 signal 再 unlock 更高效
        usleep(100000);
    }
    return NULL;
}

void *consumer(void *arg) {
    for (int i = 1; i <= 10; i++) {
        pthread_mutex_lock(&mutex);
        while (count == 0)                        // ④ 必须 while
            pthread_cond_wait(&not_empty, &mutex);
        int val = buffer[out];
        out = (out + 1) % BUF_SIZE;
        count--;
        printf("[consumer] 消费 %d, 库存 %d\n", val, count);
        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&mutex);
        usleep(150000);
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, producer, NULL);
    pthread_create(&t2, NULL, consumer, NULL);
    pthread_join(t1, NULL);                       // ⑤ detach 或 join 必须二选一
    pthread_join(t2, NULL);
    return 0;
}

要点回顾:① pthread_cond_wait 前必须拿到 mutex;② 判断条件必用 while 防虚假唤醒;③ 先 signal 再 unlock 更优(被唤醒线程不需二次睡眠);④ 每个线程必须 joindetach;⑤ 编译需加 -pthread


网络编程 链接到标题

字节序转换 — htonl / htons / ntohl / ntohs 链接到标题

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);   // Host to Network Long (32 位)
uint16_t htons(uint16_t hostshort);  // Host to Network Short (16 位)
uint32_t ntohl(uint32_t netlong);    // Network to Host Long
uint16_t ntohs(uint16_t netshort);   // Network to Host Short

功能:在主机字节序和网络字节序(大端)之间转换。网络传输中多字节整数必须用大端序。

注意:x86 是小端,htonl(1)0x01000000;大端机器上这些函数是空操作。端口和 IP 赋值前必须转换,接收后必须转回来。大端也叫"网络字节序"。

地址转换 — inet_pton / inet_ntop 链接到标题

#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

功能inet_pton(presentation to numeric)将 IP 字符串如 "192.168.1.1" 转为二进制(struct in_addr)。inet_ntop 做反向转换。afAF_INET(IPv4)或 AF_INET6(IPv6)。

注意inet_pton 返回 1=成功、0=格式无效、-1=af 不支持。inet_ntopsize 建议用 INET_ADDRSTRLEN(16)或 INET6_ADDRSTRLEN(46)。不要用已过时的 inet_addr/inet_ntoa(不支持 IPv6,非线程安全)。

socket — 创建套接字 链接到标题

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

功能:创建套接字,返回 fd,失败返回 -1。

参数

参数 常见取值
domain AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地 Unix 域)
type SOCK_STREAM(TCP,面向连接)、SOCK_DGRAM(UDP,无连接)
protocol 通常填 0(自动选择),也可显式指定 IPPROTO_TCPIPPROTO_UDP

注意:返回的 fd 默认为阻塞模式。需非阻塞时用 fcntl(fd, F_SETFL, O_NONBLOCK) 或创建时传入 SOCK_NONBLOCK 标志。

bind — 绑定地址到套接字 链接到标题

#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能:将 socket 绑定到指定的 IP 和端口。服务端必调,客户端可省略(由内核自动分配临时端口)。

注意:绑定特权端口(<1024)需 root。bind 失败常见原因——端口已被占用(EADDRINUSE)。设置 SO_REUSEADDR 可在 TIME_WAIT 后立即重用端口。

listen — 开始监听 链接到标题

#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);

功能:将 socket 标记为被动模式,准备接受连接。backlog 指定已完成连接队列(accept queue)的最大长度。

注意:内核维护两个队列——未完成握手队列(syn queue)和已完成握手队列(accept queue)。backlog 仅影响后者。/proc/sys/net/core/somaxconn 是系统范围的 backlog 上限。调用 listen 前必须先 bind

accept — 接受连接 链接到标题

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能:从 accept 队列中取出一个已完成的连接,返回新的 socket fd(用于与客户端通信)。addraddrlen 可获取客户端地址(不关心可传 NULL)。

注意:返回的新 fd 和监听 fd 是两个不同的 fd。监听 fd 持续存在接受后续连接;新 fd 仅用于与该客户端通信,用完后 close。若 accept 队列为空且 socket 为阻塞模式,accept 会阻塞。

connect — 发起连接 链接到标题

#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能:客户端向服务端发起连接。TCP 下完成三次握手。成功返回 0,失败返回 -1。

注意:阻塞模式下 connect 超时由内核决定(通常约 20 秒)。非阻塞 socket 上 connect 立即返回 -1 且 errno = EINPROGRESS,之后用 select/poll/epoll 检测可写即可。连接失败后 socket 不可再 connect,需重新创建。

send / recv — TCP 收发数据 链接到标题

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

功能send 发送数据(等价于 write + flags),recv 接收数据(等价于 read + flags)。TCP 是流协议,没有消息边界

常用 flags

flag 含义
MSG_PEEK 窥探数据,不将其从缓冲区移除
MSG_DONTWAIT 本次调用非阻塞(等价于 O_NONBLOCK
MSG_WAITALL recv 阻塞直到收到 len 字节(仍可能被信号中断)

注意必须循环收发——TCP 不保证一次调用收/发完所有数据。和 read/write 一样,返回值可能少于请求量(部分读/部分写)。flags 一般传 0。

sendto / recvfrom — UDP 收发数据 链接到标题

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

功能sendto 向指定地址发送数据报,recvfrom 接收数据报并获取发送方地址。用于 UDP(无连接)通信,也支持原始套接字。

参数区别

参数 sendto recvfrom
后两个参数 目标地址 + 地址长度 输出:发送方地址 + 地址长度指针
addrlen 值传递(socklen_t 指针传递(socklen_t *),调用前需初始化为 sizeof(addr)

UDP 注意事项

  • UDP 有消息边界——一次 recvfrom 收一条完整数据报,不会像 TCP 那样黏包/拆包。
  • 一次 recvfrom 只收一个数据报,如果 buf 太小,多余数据会被丢弃,不会报错,下次读到的是下一条数据报。
  • recvfromsrc_addraddrlen 可为 NULL/NULL(不关心对方地址时)。
  • sendto 不能用于已 connect 的 TCP socket(应用 send/write),但 connect 过的 UDP socket 可以用 send/write 代替 sendto
  • TCP 的 send/read/write/recv 能用,只是不获取对端地址,但一般不推荐。

常用 flags(与 send/recv 共享):

flag 含义
MSG_PEEK recvfrom 窥探数据,不移除
MSG_DONTWAIT 本次非阻塞
MSG_TRUNC recvfrom 返回数据报真实长度(即使被截断),需配合 MSG_PEEK

UDP echo 服务端示例

int fd = socket(AF_INET, SOCK_DGRAM, 0);

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family      = AF_INET;
addr.sin_port        = htons(8080);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(fd, (struct sockaddr *)&addr, sizeof(addr));

while (1) {
    char buf[1024];
    struct sockaddr_in cli;
    socklen_t cli_len = sizeof(cli);

    ssize_t n = recvfrom(fd, buf, sizeof(buf), 0,
                         (struct sockaddr *)&cli, &cli_len);
    if (n <= 0) continue;

    sendto(fd, buf, n, 0, (struct sockaddr *)&cli, cli_len);
}
close(fd);

关键点:① recvfromaddrlen 必须先初始化为 sizeof(sockaddr_in);② UDP 一个数据报对应一条消息,不需要像 TCP 那样循环收/发;③ 用同一个 socket 可以给不同客户端分别回包(无连接)。

setsockopt — 设置套接字选项 链接到标题

#include <sys/types.h>
#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);

功能:设置 socket 的各种选项。levelSOL_SOCKET 表示通用选项,IPPROTO_TCP 表示 TCP 层选项。

常用选项

optname 含义
SO_REUSEADDR 允许重用本地地址(TIME_WAIT 后可立即 bind)
SO_KEEPALIVE 启用 TCP keep-alive 探测
SO_RCVBUF / SO_SNDBUF 设置接收/发送缓冲区大小
TCP_NODELAY 禁用 Nagle 算法(level 用 IPPROTO_TCP

注意SO_REUSEADDR 必须在 bind 前设置。所有 TCP 服务端都应设置此选项,否则服务重启需等 TIME_WAIT(2MSL ≈ 60 秒)。

shutdown — 半关闭连接 链接到标题

#include <sys/socket.h>

int shutdown(int sockfd, int how);

功能:关闭 socket 的单向或双向通信,不释放 fd。close 关闭 fd 并减少引用计数(多进程共享时不会立即关闭连接),shutdown 直接操作连接无关引用计数。

how 取值SHUT_RD(关读)、SHUT_WR(关写,对端收到 EOF)、SHUT_RDWR(全部关闭)。

注意close vs shutdown——想立即给对方发 FIN 用 shutdown,想释放 fd 用 close。即使 dup 了 fd,shutdown 也会立即关闭连接。

select — I/O 多路复用 链接到标题

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

void FD_ZERO(fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);

功能:同时监控多个 fd 的可读/可写/异常状态。返回就绪 fd 的总数,0 超时,-1 出错。

注意

  • nfds = max(fd) + 1(不是 fd 个数,是最大 fd 号 + 1)。
  • fd_set 大小有限(默认 1024),大量连接用 pollepoll
  • select修改传入的 fd_set——每次调用前必须用 FD_ZERO/FD_SET 重新设置。
  • 被信号中断返回 -1 且 errno = EINTR(即使 SA_RESTART 也不会自动重启 select)。

poll — I/O 多路复用 链接到标题

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
    int   fd;      /* 监控的文件描述符,-1 表示忽略该项 */
    short events;  /* 请求监控的事件(输入) */
    short revents; /* 实际发生的事件(输出) */
};

功能:同时监控多个 fd 的就绪状态。fds 为 pollfd 数组,nfds 为数组长度,timeout 为超时毫秒数(-1 表示永远等待,0 表示立即返回)。返回就绪 fd 总数,0 超时,-1 出错。

常用事件

含义
POLLIN 数据可读
POLLOUT 数据可写
POLLERR 发生错误(revents 中自动设置,无需 events 指定)
POLLHUP 对端挂断
POLLRDHUP 对端关闭写(半关闭),需 _GNU_SOURCE
POLLNVAL fd 未打开

events 常用组合POLLIN | POLLOUT 同时监控读写。

注意

  • 调用后检查 revents 判断具体事件,不需要每次重建事件集(与 select 不同,poll 不修改 events)。
  • timeout 单位是毫秒,与 selectstruct timeval(微秒)不同。timeout = -1 会永久阻塞。
  • fd = -1 的数组项会被内核忽略,可用于稀疏管理。
  • 被信号中断返回 -1 且 errno = EINTRSA_RESTART 不会自动重启 poll)。

epoll — I/O 多路复用 链接到标题

#include <sys/epoll.h>

int epoll_create(int size);                // 已过时,保留 size 仅兼容
int epoll_create1(int flags);              // 推荐:flags 支持 EPOLL_CLOEXEC

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;   /* 事件类型 */
    epoll_data_t data;     /* 用户数据(常存放 fd 或指针) */
};

功能:Linux 独有的高性能 I/O 多路复用。核心思想:将 fd 的"注册"和"等待"分离——epoll_ctl 一次性注册,epoll_wait 只返回就绪事件,无需每次扫描全部 fd。

epoll_ctl 操作

操作 含义
EPOLL_CTL_ADD 注册新 fd 到 epoll 实例
EPOLL_CTL_MOD 修改已注册 fd 的事件类型
EPOLL_CTL_DEL 从 epoll 实例删除 fd

常用事件

含义
EPOLLIN 数据可读
EPOLLOUT 数据可写
EPOLLERR 错误(自动触发,无需 events 中指定)
EPOLLHUP 对端挂断(自动触发)
EPOLLRDHUP 对端关闭写(半关闭),需 _GNU_SOURCE
EPOLLET 边缘触发(默认是水平触发 LT)
EPOLLONESHOT 事件触发一次后自动禁用,需 EPOLL_CTL_MOD 重新启用

注意

  • 水平触发 (LT,默认):只要缓冲区有数据就反复通知,与 poll/select 行为一致,编程简单。
  • 边缘触发 (ET):仅在状态变化时通知一次。必须用非阻塞 fd,每次循环 read/write 直到返回 EAGAIN,否则可能饿死后续事件。ET 模式可减少系统调用次数,适合高并发。
  • EPOLLONESHOT 场景:多线程下,一个事件被某线程处理后,其他线程不可能再触发该 fd 的同一事件,避免竞态。处理后需 EPOLL_CTL_MOD 重新注册。
  • epoll_create1(0) 等价于旧版 epoll_create(0);建议用 epoll_create1(EPOLL_CLOEXEC) 防止 exec 后 fd 泄露。
  • epoll_waittimeout 单位是毫秒(与 poll 一致,与 select 不同),-1 永久阻塞。
  • epoll_wait 返回的 events 数组已填好就绪事件,用户只需遍历 [0, n) 即可——仅遍历就绪项,不是全部注册项。
  • 关闭 fd 时,epoll 会自动将该 fd 从内核事件表中移除,无需手动 EPOLL_CTL_DEL
  • 管道/套接字对端关闭与 EPOLLHUP/EPOLLRDHUP:对端 closeshutdown 时,epoll 会将 EPOLLHUP(完全关闭)或 EPOLLRDHUP(半关闭)与 EPOLLIN 同时报告。若同时监听了 EPOLLOUT,也可能触发可写。推荐始终关注 EPOLLRDHUP 以精确识别半关闭。

select / poll / epoll 对比 链接到标题

特性 select poll epoll
最大 fd 值 < 1024(位图限制) 无限制(int 范围) 无限制
最大监听数 1024 ulimit -n max_user_watches(百万级)
数据结构 固定位图 动态数组 红黑树 + 就绪链表
每次调用拷贝数据 全部 fd_set(3 个) 整个 pollfd 数组 不拷贝(只注册一次)
内核遍历 O(max_fd) O(nfds) O(1)(回调直接挂链表)
用户态遍历 O(max_fd) O(nfds) O(就绪数)
事件集可重用 否(每次需重建) 是(按 revents 判断) 是(内核维护状态)
适用场景 兼容老代码 中等规模、跨平台 高并发 C10K/C10M
跨平台 POSIX,通用 POSIX,通用 Linux 专有

sockaddr 结构体用法 链接到标题

#include <netinet/in.h>

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));                     // ① 必须先清零
addr.sin_family = AF_INET;
addr.sin_port   = htons(8080);                      // ② 端口用 htons
inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);  // ③ IP 用 inet_pton
// 或者绑定所有本地 IP:
addr.sin_addr.s_addr = htonl(INADDR_ANY);           // ④ INADDR_ANY 也需 htonl
bind(fd, (struct sockaddr *)&addr, sizeof(addr));   // ⑤ 必须强转

注意sockaddr_in 定义在 <netinet/in.h>,传给 bind/connect/accept 时必须强转为 (struct sockaddr *)必须先 memset 清零,否则 sin_zero 中的垃圾值会导致 bind 失败。


网络综合示例:TCP Echo 服务端 链接到标题

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>

int main() {
    int sfd = socket(AF_INET, SOCK_STREAM, 0);

    int opt = 1;
    setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // ① 端口复用

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));                // ② 必须先清零
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(8080);                 // ③ 字节序转换
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
    listen(sfd, 5);                                // ④ backlog

    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(sfd, &rfds);                            // ⑤ 监控监听 fd 的可读
    int maxfd = sfd;

    while (1) {
        fd_set tmp = rfds;                         // ⑥ select 会修改 fd_set,需备份
        int n = select(maxfd + 1, &tmp, NULL, NULL, NULL);
        if (n <= 0) continue;

        if (FD_ISSET(sfd, &tmp)) {                 // ⑦ 新连接到达
            struct sockaddr_in cli;
            socklen_t len = sizeof(cli);
            int cfd = accept(sfd, (struct sockaddr *)&cli, &len);
            char ip[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip)); // ⑧ 地址转字符串
            printf("[连接] %s:%d\n", ip, ntohs(cli.sin_port));
            FD_SET(cfd, &rfds);                    // ⑨ 将客户端 fd 加入监控
            if (cfd > maxfd) maxfd = cfd;
        }

        for (int fd = 0; fd <= maxfd; fd++) {
            if (fd == sfd || !FD_ISSET(fd, &tmp)) continue;
            char buf[1024];
            ssize_t n = recv(fd, buf, sizeof(buf), 0);  // ⑩ 接收
            if (n <= 0) {
                if (n == 0) printf("[断开]\n");
                close(fd);
                FD_CLR(fd, &rfds);                 // ⑪ 从监控集移除
            } else {
                send(fd, buf, n, 0);               // ⑫ 回显
            }
        }
    }
    close(sfd);
    return 0;
}

要点回顾:① SO_REUSEADDR 必须在 bind 之前设置;② 端口和地址赋值必须用 htons/htonl;③ select 每次调用前需重建 fd_set(因为会被修改);④ nfds = maxfd + 1;⑤ recv 返回 0 表示对端关闭连接;⑥ inet_ntop 用于日志/调试输出 IP。


文件 I/O 链接到标题

open — 打开/创建文件 链接到标题

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

功能:返回文件描述符,失败返回 -1 并设置 errno

常用 flagsO_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND, O_NONBLOCK

注意O_CREAT 时必须给出第三个参数 mode(如 0644),否则权限不确定。modeumask 影响。

close — 关闭文件描述符 链接到标题

#include <unistd.h>

int close(int fd);

功能:关闭 fd,释放内核资源。成功返回 0,失败返回 -1

注意:一定要检查返回值——NFS 上的 close 可能失败(数据未刷盘)。多次 close 同一个 fd 是 bug。

read — 读文件 链接到标题

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

功能:从 fd 读最多 count 字节到 buf。返回实际读取字节数,0 表示 EOF,-1 出错。

注意:返回的字节数可以少于请求数(普通文件遇到 EOF、pipe/socket 部分数据到达、被信号中断)。必须循环读到期望的量。被信号中断时 errno = EINTR,见"慢速系统调用与信号"节。

write — 写文件 链接到标题

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

功能:向 fd 写最多 count 字节。返回实际写入字节数,-1 出错。

注意:和 read 一样,返回值可能少(磁盘满、信号中断)。写 pipe/socket 时可能写不满。同样需要循环写。

lseek — 移动文件偏移 链接到标题

#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

功能:修改 fd 的文件偏移量。whence 取值:

  • SEEK_SET — 从文件头偏移 offset
  • SEEK_CUR — 从当前位置偏移
  • SEEK_END — 从文件末尾偏移

注意:对 pipe、socket、tty 调用 lseek 会失败(ESPIPE)。lseek 超过文件末尾再写入会产生"空洞文件"(hole,占元数据不占磁盘)。

truncate / ftruncate — 截断文件 链接到标题

#include <unistd.h>
#include <sys/types.h>

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

功能:将文件截断到指定长度。超出的数据丢失。truncate 用路径,ftruncate 用 fd(需要打开为可写)。

注意:不可逆操作。如果 length 大于原文件,文件被扩展(扩展部分填 0,形成空洞)。


文件描述符控制 链接到标题

fcntl — 文件描述符控制 链接到标题

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */);

功能:对 fd 执行各种操作:

  • F_GETFL / F_SETFL:获取/设置文件状态标志(如设为 O_NONBLOCK
  • F_DUPFD:复制 fd(比 dup 更精确)
  • F_GETFD / F_SETFD:获取/设置 fd 标志(目前只有 FD_CLOEXEC

注意:设置 O_NONBLOCK 等标志时,必须先 F_GETFL按位或 后再 F_SETFL,不能直接覆盖,否则其他标志丢失。

dup / dup2 — 复制文件描述符 链接到标题

#include <unistd.h>

int dup(int oldfd);
int dup2(int oldfd, int newfd);

功能:复制 fd。新 fd 与旧 fd 指向同一内核文件表项(共享文件偏移和状态标志)。

dup — 返回可用最小 fd。 dup2 — 指定新 fd 号码;若 newfd 已打开则先关闭它。若 oldfd == newfd,直接返回 newfd,不关闭。

注意dup2 的 close + dup 是原子的(这很重要,避免竞态)。二者不共享 FD_CLOEXEC 标志。常用于 fork+exec 前实现 I/O 重定向:dup2(fd, STDOUT_FILENO)


文件元数据 链接到标题

stat / lstat / fstat — 获取文件信息 链接到标题

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);

功能:获取文件元数据(大小、权限、inode、时间戳等)。

stat vs lstat:如果 path 是符号链接,stat 返回链接目标的信息,lstat 返回链接自身的信息。

注意struct stat 中时间字段(st_mtime 等)有的是 time_t,用 difftime() 比较。

#include <unistd.h>

int link(const char *oldpath, const char *newpath);
int unlink(const char *pathname);

link — 为现有文件创建硬链接(同一 inode 的别名)。 unlink — 删除目录项;当硬链接计数归零且无进程打开文件时,文件被真正删除。

注意unlink 不能删目录(用 rmdir)。硬链接不能跨文件系统、不能链接目录(root 也不行)。unlink 不检查文件内容是否被占用,靠引用计数。


目录操作 链接到标题

opendir / closedir / readdir — 目录操作 链接到标题

#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);

功能:打开目录、逐条读取目录项、关闭目录。readdir 返回 NULL 表示读完或出错(需检查 errno 区分)。

dirent 关键字段d_name(文件名),d_ino(inode 号),d_type(文件类型,有些文件系统不支持)。

注意readdir 返回的指针可能被下一次调用覆盖,不是线程安全的。目录读取顺序无保证。


进程间通信 (IPC) 链接到标题

pipe — 创建匿名管道 链接到标题

#include <unistd.h>

int pipe(int pipefd[2]);

功能:创建单向数据通道。pipefd[0] 读端,pipefd[1] 写端。成功返回 0。

注意:只能用于父子/兄弟进程(通过 fork 继承 fd)。读空 pipe 会阻塞到有数据或写端全部关闭。写满 pipe(通常 64KB)会阻塞。所有写端关闭后,read 返回 0(EOF)。所有读端关闭后,write 触发 SIGPIPE(默认动作是终止进程)。

mkfifo — 创建命名管道 (FIFO) 链接到标题

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

功能:创建有文件名的管道,任意进程可通过路径打开,不限于父子关系。modeumask 限制。

注意open FIFO 时,以 O_RDONLY 打开会阻塞直到有进程以写打开(反之亦然)。O_NONBLOCK 可避免阻塞。FIFO 数据不落盘——文件只是命名入口,内核中的缓冲区才存数据。

Unix Domain Socket — 本地进程间通信 链接到标题

#include <sys/un.h>

struct sockaddr_un {
    sa_family_t sun_family;  /* AF_UNIX */
    char        sun_path[108]; /* socket 文件路径(Linux),其他系统可能更短 */
};

功能AF_UNIX(又称 AF_LOCAL)域套接字,专门用于同一台机器上的进程间通信。走内核缓冲区,不走网络栈,性能远高于 TCP/UDP loopback。支持 SOCK_STREAM(可靠、有序、面向连接)和 SOCK_DGRAM(数据报,在内核中也是可靠有序的——不会丢包)。

服务端写法(SOCK_STREAM) 链接到标题

#include <sys/un.h>
#include <sys/socket.h>
#include <stddef.h>   // offsetof
#include <unistd.h>   // unlink
#include <string.h>

int main() {
    const char *PATH = "/tmp/my.sock";

    int fd = socket(AF_UNIX, SOCK_STREAM, 0);           // ① STREAM 通信

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, PATH, sizeof(addr.sun_path) - 1); // ② 路径,务必 -1 防溢出

    unlink(PATH);                                        // ③ 删除残留文件,避免 EADDRINUSE
    bind(fd, (struct sockaddr *)&addr, sizeof(addr));    // ④ bind 会创建 socket 文件
    listen(fd, 5);

    while (1) {
        int cli = accept(fd, NULL, NULL);                // ⑤ 不需要客户端地址时传 NULL
        write(cli, "hello\n", 6);
        close(cli);
    }
    close(fd);
    unlink(PATH);                                        // ⑥ 退出时清理文件
}

客户端写法(SOCK_STREAM) 链接到标题

int cli = socket(AF_UNIX, SOCK_STREAM, 0);

struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/my.sock", sizeof(addr.sun_path) - 1);

connect(cli, (struct sockaddr *)&addr, sizeof(addr));    // 连接服务端
// ... read / write ...
close(cli);

SOCK_DGRAM 数据报 链接到标题

// 服务端
int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/dgram.srv", sizeof(addr.sun_path) - 1);
unlink("/tmp/dgram.srv");
bind(fd, (struct sockaddr *)&addr, sizeof(addr));

char buf[256];
socklen_t len = sizeof(addr);
ssize_t n = recvfrom(fd, buf, sizeof(buf), 0,
                     (struct sockaddr *)&addr, &len);
// ⑤ 客户端也必须 bind 后,服务端才可以用 sendto 回包
// 客户端(也必须 bind 才能接收回复)
int fd = socket(AF_UNIX, SOCK_DGRAM, 0);

struct sockaddr_un my_addr;                              // 客户端自己的地址
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sun_family = AF_UNIX;
strncpy(my_addr.sun_path, "/tmp/dgram.cli", sizeof(my_addr.sun_path) - 1);
unlink("/tmp/dgram.cli");
bind(fd, (struct sockaddr *)&my_addr, sizeof(my_addr));  // bind 后才有"回信地址"

struct sockaddr_un srv;                                  // 服务端地址
memset(&srv, 0, sizeof(srv));
srv.sun_family = AF_UNIX;
strncpy(srv.sun_path, "/tmp/dgram.srv", sizeof(srv.sun_path) - 1);

sendto(fd, "ping", 4, 0, (struct sockaddr *)&srv, sizeof(srv));
close(fd);
unlink("/tmp/dgram.cli");

注意

  • 路径长度sun_path 在 Linux 上是 108 字节,不同 Unix 系统可能有差异(BSD 是 104,Solaris 是 108)。可移植写法用 sizeof(addr.sun_path) - 1
  • stale 文件bind 会创建 socket 文件,但进程崩溃/crash 后不会自动删除。下次启动时 bind 会因文件已存在而失败(EADDRINUSE)。必须 unlinkbind
  • 路径限制:必须是绝对路径且目录需存在。不能用相对路径,也不支持 ~ 展开。/tmp 是最常见的选择。
  • 对比 TCP loopback:Unix domain socket 比 127.0.0.1 的 TCP 快约 2 倍(无协议栈开销、无校验和、无路由),是高性���本地 IPC 的首选。

额外功能 — 凭据传递与 fd 传递

// 发送端:通过辅助数据传递自己的 PID/UID/GID(SCM_CREDENTIALS)或 fd(SCM_RIGHTS)
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char cbuf[CMSG_SPACE(sizeof(int))];  // 传一个 fd 需要的空间

msg.msg_control    = cbuf;
msg.msg_controllen = sizeof(cbuf);

cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type  = SCM_RIGHTS;         // 传 fd
cmsg->cmsg_len   = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &fd_to_send, sizeof(int));

msg.msg_controllen = cmsg->cmsg_len;
sendmsg(sockfd, &msg, 0);             // fd 通过辅助数据"飞"过去

// 接收端用 recvmsg 配合 CMSG_* 宏解析,获取传过来的 fd

注意CMSG_* 宏来自 <sys/socket.h>SCM_CREDENTIALS(传递 struct ucred)和 SCM_RIGHTS(传递 fd)是 Unix domain socket 独有的杀手级特性,对网络套接字不适用。

与 127.0.0.1 TCP 对比

特性 Unix Domain Socket 127.0.0.1 TCP
走网络栈 是(loopback 接口)
速度 快 ~2x 有协议栈开销
地址 文件路径 IP + 端口
凭据传递 支持 SCM_CREDENTIALS 不支持
fd 传递 支持 SCM_RIGHTS 不支持
跨主机 否(127.0.0.1 也只在本机)
清理 需手动 unlink TCP 无残留文件
权限控制 文件权限(chmod 仅限 root 绑定 <1024 端口

内存映射 链接到标题

mmap — 映射文件或匿名内存 链接到标题

#include <sys/mman.h>

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

功能:将文件内容或匿名内存映射到进程地址空间,返回映射区起始地址,失败返回 MAP_FAILED

prot 取值PROT_READ | PROT_WRITE | PROT_EXEC | PROT_NONE(组合使用)。

flags

flag 含义
MAP_SHARED 修改写回文件,其他映射同一文件的进程可见
MAP_PRIVATE COW——修改不写回文件,不影响其他进程
MAP_ANONYMOUS 不关联文件(fd 传 -1),类似 malloc 大块内存

注意offset 必须是页大小(sysconf(_SC_PAGE_SIZE) 即 4096)的整数倍。映射超过文件大小的区域在访问时收到 SIGBUSmmapread/write 少一次内核拷贝,大文件随机访问更高效。munmap 或进程结束时映射自动解除。


PCB(进程控制块)— task_struct 中与编程相关的重要成员 链接到标题

Linux 内核中,每个进程由一个 task_struct 结构体表示(定义在 <linux/sched.h>),即 PCB。内核调度、内存管理、信号处理等都依赖其中的字段。以下列出与系统编程关系最密切的成员:

进程标识 链接到标题

成员 含义
pid_t pid 进程 ID(用户态 gettid() 返回值,每个线程唯一)
pid_t tgid 线程组 ID;主线程的 tgid == pid(用户态 getpid() 实际返回 tgid
pid_t ppid 父进程 ID(getppid()

注意fork 后子进程 tgid == pidgetpid() 返回 tgidgettid() 返回内核 pid,二者在线程场景下不同。

进程状态 链接到标题

成员 含义
volatile long state 进程当前状态:TASK_RUNNING(0 — 运行中或就绪)、TASK_INTERRUPTIBLE(可中断睡眠)、TASK_UNINTERRUPTIBLE(不可中断睡眠,即 D 状态)、TASK_STOPPEDTASK_TRACEDEXIT_ZOMBIE(僵尸)、EXIT_DEAD
int exit_state 退出状态(僵尸 / 已死亡)
int exit_code 退出码(WEXITSTATUS 的来源)
int exit_signal 退出时发给父进程的信号(通常 SIGCHLD

编程要点ps 看到的 D 状态就是 TASK_UNINTERRUPTIBLE,通常进程卡在内核 IO 上。僵尸进程对应 EXIT_ZOMBIE——进程已结束但父进程未 wait(),PCB 未被回收。

调度相关 链接到标题

成员 含义
int prio / int static_prio / int normal_prio 动态优先级 / 静态优先级(nice 值映射,100-139)/ 归一化优先级
unsigned int rt_priority 实时优先级(SCHED_FIFO / SCHED_RR 策略用,1-99)
unsigned int policy 调度策略:SCHED_NORMAL(CFS)、SCHED_FIFOSCHED_RRSCHED_BATCHSCHED_IDLESCHED_DEADLINE
struct sched_entity se CFS 调度实体(包含 vruntime——虚拟运行时间,CFS 用红黑树选 vruntime 最小的进程)
cpumask_t cpus_allowed 进程允许运行的 CPU 集合(taskset / sched_setaffinity 控制)

编程要点:用户态 nice() / setpriority() 修改 static_priosched_setscheduler() 修改 policyrt_priority/proc/[pid]/sched 可以查看调度信息。

文件与 IO 链接到标题

成员 含义
struct fs_struct *fs 文件系统信息:umask、根目录(/proc/[pid]/root)、当前工作目录(cwd
struct files_struct *files 打开文件表(fdtable 数组);每个 fd 对应一个 struct file 指针
struct nsproxy *nsproxy 命名空间代理(mnt、uts、ipc、pid、net、cgroup、time 命名空间)

编程要点fork 后父子共享 files_struct(引用计数 +1),所以 fd 在父子间共享偏移量——除非设置了 FD_CLOEXECexec 时关闭)。clone(CLONE_FILES) 决定是否共享文件表。

内存管理 链接到标题

成员 含义
struct mm_struct *mm 进程地址空间描述符(代码段、数据段、堆、栈、mmap 区域等)
struct mm_struct *active_mm 当前实际使用的 mm;内核线程没有自己的 mm,借用前一个进程的 active_mm

编程要点mm->mmap 是 VMA(vm_area_struct)链表的头,/proc/[pid]/maps 的内容就来自它。mm->arg_start / arg_end 指向命令行参数;mm->env_start / env_end 指向环境变量。线程可通过 clone(CLONE_VM) 共享 mm_struct

信号处理 链接到标题

成员 含义
struct signal_struct *signal 信号描述符(线程组共享):挂起信号集、资源限制(rlim 数组)等
struct sighand_struct *sighand 信号处理函数表(struct k_sigaction 数组,每个信号一个 handler),引用计数
sigset_t blocked 当前阻塞(屏蔽)的信号集
sigset_t pending 本线程私有的挂起信号

编程要点signal->rlimstruct rlimit 数组,用户态 getrlimit/setrlimit 修改的就是它(如 RLIMIT_NOFILERLIMIT_STACK)。信号 handler 通过 sighand 设置,/proc/[pid]/statusSigCgt/SigBlk/SigIgn 来自这些字段。

时间与统计 链接到标题

成员 含义
u64 utime / u64 stime 用户态 CPU 时间 / 内核态 CPU 时间(times()/proc/[pid]/stat 的来源)
u64 start_time 进程启动时间(/proc/[pid]/stat 第 22 列的 boot-based 时间戳)

进程关系 链接到标题

成员 含义
struct task_struct __rcu *real_parent 真实父进程(fork 的那一个)
struct task_struct __rcu *parent 当前父进程(收到 SIGCHLD 的那个,ptrace 附加时会改变)
struct list_head children 子进程链表
struct list_head sibling 兄弟进程链表(同一父进程的子进程链表节点)

编程要点real_parent != parent 最常见的情形是进程被调试器 ptrace 附加——调试器成为 parent

其他杂项 链接到标题

成员 含义
struct tty_struct *tty 控制终端(/proc/[pid]/stat 第 7 字段的来源)
kuid_t loginuid 审计用的登录 UID
int link_count 硬链接计数(防止 task_struct 被过早释放)
struct task_struct *group_leader 线程组 leader 指针
struct cred *cred 进程凭证:uidgideuidegidsuidsgidfsuidfsgidcap_effective
char comm[TASK_COMM_LEN] 进程名(最多 15 字符,prctl(PR_SET_NAME) / /proc/[pid]/comm
unsigned long nvcsw / nivcsw 自愿/非自愿上下文切换计数(/proc/[pid]/status 中可见)
struct rlimit rlim[RLIM_NLIMITS] 进程资源限制(getrlimit/setrlimit 操作的对象)

编程要点cred 结构中 uid/euid/suidsetuid 机制的核心——fork 继承、exec 视 SUID 位变化。rlimRLIMIT_NOFILEulimit -nRLIMIT_CORE 控制 core dump 大小。

会话与进程组 链接到标题

成员 含义
pid_t pgrp 进程组 ID(getpgrp()
pid_t session 会话 ID(getsid()

编程要点setsid() 创建新会话并成为 session leader(脱离控制终端的关键一步)。shell 的作业控制依赖进程组——Ctrl+C 发给前台进程组的所有进程,kill(-pgid, sig) 发给整个进程组。


总结:从用户态编程角度看,task_struct 中我们直接/间接打交道的字段就是 PID 系列(fork/exec/wait 的基石)、files(每个 fd 追到这里)、mmmmapbrk、地址空间的真相)、信号三件套(blocked/pending/sighand)、调度字段nice/sched_setscheduler 的真实作用点)。理解这些字段,/proc 下的大部分文件、ps/top/strace 输出背后的含义便一目了然。


常见头文件总汇 链接到标题

#include <unistd.h>      // read, write, close, lseek, dup, dup2, truncate, ftruncate,
                          // link, unlink, getopt, fork, exec, pipe, alarm, sleep
#include <sys/wait.h>    // wait, waitpid
#include <fcntl.h>       // open, fcntl (O_*, F_* 宏)
#include <sys/types.h>   // off_t, ssize_t, mode_t
#include <sys/stat.h>    // stat, lstat, fstat, mkfifo, struct stat, mode 宏
#include <dirent.h>      // opendir, readdir, closedir, struct dirent
#include <signal.h>      // signal, sigaction, kill, sigprocmask, sigemptyset,
                          // sigfillset, sigaddset, sigdelset, sigismember
#include <sys/time.h>    // setitimer, struct itimerval
#include <sys/mman.h>    // mmap, munmap, mprotect
#include <pthread.h>     // pthread_create, pthread_exit, pthread_join, pthread_self,
                          // pthread_detach, pthread_cancel, pthread_mutex_*, pthread_cond_*,
                          // pthread_rwlock_*, pthread_attr_*
#include <semaphore.h>   // sem_init, sem_wait, sem_post, sem_destroy
#include <string.h>      // strerror, memset

/* ---------- 网络编程 ---------- */
#include <sys/socket.h>  // socket, bind, listen, accept, connect, send, recv,
                          // sendto, recvfrom, setsockopt, shutdown
#include <sys/un.h>      // struct sockaddr_un (AF_UNIX)
#include <sys/select.h>  // select, FD_ZERO, FD_SET, FD_CLR, FD_ISSET
#include <poll.h>        // poll, struct pollfd
#include <sys/epoll.h>   // epoll_create, epoll_ctl, epoll_wait, epoll_event
#include <netinet/in.h>  // struct sockaddr_in, INADDR_ANY
#include <arpa/inet.h>   // htonl, htons, ntohl, ntohs, inet_pton, inet_ntop