Skip to content
← 返回C列表
开发实践

C POSIX 完整实践:双文件字节统计库 · C字节统计项目怎样准备和构建至C字节统计项目应该验证哪些失败 ​

返回分篇目录

先具备 C 指针、分离编译和基本所有权知识。本专题沿文件契约、线程生存期、完整项目、排错推进。示例未执行,仅完成静态文本/模板检查,不声称运行通过。

专题入口:C POSIX 文件接口:描述符、短读写与错误契约 · C POSIX 线程:创建、互斥与退出前收拢 · C POSIX 完整实践:双文件字节统计库 · C POSIX 排错:系统错误、死锁与不完整输出

scan.c(同源文件,示例未执行)
c
#include "scan.h"
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

struct worker {
    struct scan *owner;
    char *path;
    int error;
};
struct scan {
    pthread_mutex_t mutex;
    struct worker jobs[2];
    uint64_t total;
    int attempted;
    int ready;
};

/* Broken synchronization means lifetime safety cannot be established. */
static void require_success(int code) {
    if (code != 0) abort();
}

static void *count_file(void *arg) {
    struct worker *w = arg;
    int fd;
    do { fd = open(w->path, O_RDONLY | O_NONBLOCK); }
    while (fd < 0 && errno == EINTR);
    if (fd < 0) { w->error = errno; return NULL; }
    struct stat info;
    if (fstat(fd, &info) != 0) w->error = errno;
    else if (!S_ISREG(info.st_mode)) w->error = EINVAL;
    uint64_t bytes = 0;
    unsigned char buffer[4096];
    while (w->error == 0) {
        ssize_t n = read(fd, buffer, sizeof buffer);
        if (n > 0) {
            if ((uint64_t)n > UINT64_MAX - bytes) w->error = EOVERFLOW;
            else bytes += (uint64_t)n;
        } else if (n == 0) break;
        else if (errno != EINTR) w->error = errno;
    }
    /* Never blindly retry close: the descriptor may already be released. */
    if (close(fd) != 0 && w->error == 0) w->error = errno;
    if (w->error == 0) {
        require_success(pthread_mutex_lock(&w->owner->mutex));
        if (bytes > UINT64_MAX - w->owner->total) w->error = EOVERFLOW;
        else w->owner->total += bytes;
        require_success(pthread_mutex_unlock(&w->owner->mutex));
    }
    return NULL;
}

int scan_create(const char *left, const char *right, scan **out) {
    if (out == NULL) return EINVAL;
    *out = NULL;
    if (left == NULL || right == NULL) return EINVAL;
    scan *s = calloc(1, sizeof *s);
    if (s == NULL) return ENOMEM;
    s->jobs[0].path = strdup(left);
    s->jobs[1].path = strdup(right);
    if (s->jobs[0].path == NULL || s->jobs[1].path == NULL) {
        free(s->jobs[0].path); free(s->jobs[1].path); free(s);
        return ENOMEM;
    }
    int code = pthread_mutex_init(&s->mutex, NULL);
    if (code != 0) {
        free(s->jobs[0].path); free(s->jobs[1].path); free(s);
        return code;
    }
    s->jobs[0].owner = s;
    s->jobs[1].owner = s;
    *out = s;
    return 0;
}

int scan_run(scan *s) {
    if (s == NULL) return EINVAL;
    if (s->attempted) return EALREADY;
    s->attempted = 1;
    pthread_t threads[2];
    size_t started = 0;
    int code = 0;
    for (; started < 2; ++started) {
        code = pthread_create(&threads[started], NULL, count_file,
                              &s->jobs[started]);
        if (code != 0) break;
    }
    for (size_t i = 0; i < started; ++i)
        require_success(pthread_join(threads[i], NULL));
    if (code != 0) return code;
    for (size_t i = 0; i < 2; ++i)
        if (s->jobs[i].error != 0) return s->jobs[i].error;
    s->ready = 1;
    return 0;
}

static int write_all(int fd, const char *data, size_t length) {
    size_t done = 0;
    while (done < length) {
        ssize_t n = write(fd, data + done, length - done);
        if (n > 0) done += (size_t)n;
        else if (n == 0) return EIO;
        else if (errno != EINTR) return errno;
    }
    return 0;
}

int scan_report(const scan *s, int fd) {
    if (s == NULL || !s->ready) return EINVAL;
    char text[64];
    int n = snprintf(text, sizeof text, "bytes=%" PRIu64 "\n", s->total);
    if (n < 0 || (size_t)n >= sizeof text) return EOVERFLOW;
    return write_all(fd, text, (size_t)n);
}

void scan_destroy(scan *s) {
    if (s == NULL) return;
    require_success(pthread_mutex_destroy(&s->mutex));
    free(s->jobs[0].path);
    free(s->jobs[1].path);
    free(s);
}
scan.h(同源文件,示例未执行)
c
#ifndef BYTE_SCAN_H
#define BYTE_SCAN_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct scan scan;
/* 0 = success, otherwise an errno-style error number, never -1.
 * out must be non-null. Failure sets *out = NULL. Paths are copied.
 * The handle has one owner. No concurrent calls on the same handle.
 * Files must be regular, finite, stable during run; paths are trusted.
 */
int scan_create(const char *left, const char *right, scan **out);
/* Run exactly once. Joins all successfully created workers before return.
 * Failure publishes no result; repeat calls return EALREADY.
 * Cancellation, fork while running, and external pthread manipulation
 * are unsupported. Unexpected internal join/lock failure aborts.
 */
int scan_run(scan *s);
/* Borrow output fd; never closes it. Requires a successful run.
 * Output failure can leave a prefix written; caller handles SIGPIPE.
 */
int scan_report(const scan *s, int fd);
/* NULL is accepted; otherwise a valid exclusively owned handle required. */
void scan_destroy(scan *s);
#ifdef __cplusplus
}
#endif
#endif
TEST-SPEC.md(同源文件,示例未执行)
md
# 验收规格(全部未执行)

| 场景 | 操作与观测 | 预期 |
| --- | --- | --- |
| LF样本 | README 的命令 | bytes=7 LF;退出0 |
| 重复路径 | 两参数都用 left.txt | bytes=8 LF;退出0 |
| 空文件 | 将一个参数设为读者准备的零字节普通文件 | 只计另一个输入;退出0 |
| 参数缺失 | 不传参数 | usage 到 stderr;退出2 |
| 文件缺失 | 第二参数为不存在的路径 | run 的错误到 stderr;stdout空;退出1 |
| 非普通文件 | 一个参数为目录 | 错误到 stderr;stdout空;退出1 |
| 输出失败 | 将 stdout 设为无效/不可写描述符的受控测试环境 | report失败;退出1,可能已有前缀 |
| 短读 | 接口替身让 read 依次返回2、1、0 | 本文件计3字节,不能计4096 |
| EINTR | 接口替身让 read/write 首次 -1/EINTR 后成功 | 重试且不丢失已处理偏移 |
| 第二次创建线程失败 | 在受控测试版本替换 pthread_create 返回 EAGAIN | 先 join 第一个线程,再返回EAGAIN |
| 一次性状态 | C测试驱动 create/run/run/destroy | 第二次run为EALREADY;仅销毁一次 |
| 构造失败 | 受控分配替身使第二次strdup失败 | out为空,已分配块释放,无mutex销毁 |
| 溢出 | 受控内部计数替身接近UINT64_MAX | EOVERFLOW,无成功报告 |

test_scan.c 已提供基础 C 测试驱动;短读写、EINTR、线程创建与分配失败的
接口替身尚未实现,不宣称已有自动化故障注入。测试驱动也未编译、未执行。
普通磁盘文件不能可靠制造短写、线程创建失败或64位溢出。
先用真实文件核对正常/缺失/目录/重复项,再单独实现链接替身测试;
替身要保留真实 pthread 生命周期,不允许伪造成功线程ID。
手工检查依据:成功创建数 started 决定 join 范围;所有 join 在返回前完成;
worker只写自己的error,主线程只在join后读;total写受同一mutex保护;
ready只在全部成功后置位。任何不满足这些条件的修改须重新审查。
test_scan.c(同源文件,示例未执行)
c
/* Unexecuted API contract tests. No fault-injection substitutes implemented. */
#include "scan.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void expect(int condition, const char *message) {
    if (!condition) {
        fprintf(stderr, "contract failed: %s\n", message);
        exit(1);
    }
}

static void expect_report(scan *s, const char *expected) {
    int fds[2];
    expect(pipe(fds) == 0, "pipe");
    /* Reports are at most 64 bytes; no full-pipe wait before the read. */
    expect(scan_report(s, fds[1]) == 0, "report");
    expect(close(fds[1]) == 0, "close report writer");
    char text[64];
    size_t used = 0;
    for (;;) {
        expect(used < sizeof text, "report fits buffer");
        ssize_t n = read(fds[0], text + used, sizeof text - used);
        if (n > 0) used += (size_t)n;
        else if (n == 0) break;
        else expect(errno == EINTR, "report read");
    }
    expect(close(fds[0]) == 0, "close report reader");
    expect(used == strlen(expected), "report length");
    expect(memcmp(text, expected, used) == 0, "report bytes");
}

int main(int argc, char **argv) {
    expect(argc == 3, "two LF fixture paths required");
    scan *s = NULL;
    expect(scan_create(argv[1], argv[2], NULL) == EINVAL, "null out");
    expect(scan_create(NULL, argv[2], &s) == EINVAL && s == NULL,
           "null path leaves out null");
    expect(scan_run(NULL) == EINVAL, "null run");
    expect(scan_report(NULL, 1) == EINVAL, "null report");
    scan_destroy(NULL);

    expect(scan_create(argv[1], argv[2], &s) == 0, "create");
    expect(scan_report(s, 1) == EINVAL, "report before run");
    expect(scan_run(s) == 0, "run");
    expect(scan_run(s) == EALREADY, "one-shot run");
    expect(scan_report(s, -1) == EBADF, "invalid borrowed fd");
    expect_report(s, "bytes=7\n");
    scan_destroy(s);
    s = NULL;

    expect(scan_create(argv[1], argv[1], &s) == 0, "duplicate create");
    expect(scan_run(s) == 0, "duplicate input accepted");
    expect_report(s, "bytes=8\n");
    scan_destroy(s);
    s = NULL;

    /* Empty path cannot name a file; no reliance on a magic missing name. */
    expect(scan_create(argv[1], "", &s) == 0, "copy empty path");
    expect(scan_run(s) == ENOENT, "open failure propagated");
    expect(scan_report(s, 1) == EINVAL, "failed run not published");
    scan_destroy(s);
    puts("scan contracts passed");
    return 0;
}

C字节统计项目怎样准备和构建 ​

C字节统计项目怎样准备和构建:CMake 负责分离编译,Threads::Threads 负责当前 POSIX 工具链要求的线程编译/链接设置。使用仓库根目录为工作目录,命令和相对输入路径见同源 README。示例未执行,未运行 configure、编译器、make、项目程序或测试,未安装依赖。

Linux Bash 与 macOS Zsh/Bash 采用同一套命令意图,编译器和 SDK 仍需读者预先准备。WSL 中须使用 Linux CMake 与 Linux 编译器;PowerShell 直接运行 Windows 编译器不在本项目边界。静态检查只核对正文、引用和模板,不等同 C/C++ 语法检查或链接通过。

C字节统计项目应该验证哪些失败 ​

C字节统计项目应该验证哪些失败:先验证正常、空文件、重复路径、缺失路径与非普通文件,再用受控接口替身制造短读写、EINTR、EAGAIN 与第二次线程创建失败。最后验证第二次 run 为 EALREADY,失败后报告被拒绝。不能靠耗尽机器线程或申请天文数字文件来制造失败。

TEST-SPEC 明确哪些仅是规格。手工推演重点是每次成功创建都对应 join、每次成功 open 都走 close、每次成功 mutex_init 只有最终 destroy、run 失败不发布 stdout。运行工具就绪后还要独立使用 ASan/TSan 等诊断,工具无报告也不能证明所有调用前提成立。

继续阅读