C POSIX 完整实践:双文件字节统计库 · C字节统计项目怎样准备和构建至C字节统计项目应该验证哪些失败
先具备 C 指针、分离编译和基本所有权知识。本专题沿文件契约、线程生存期、完整项目、排错推进。示例未执行,仅完成静态文本/模板检查,不声称运行通过。
专题入口:C POSIX 文件接口:描述符、短读写与错误契约 · C POSIX 线程:创建、互斥与退出前收拢 · C POSIX 完整实践:双文件字节统计库 · C POSIX 排错:系统错误、死锁与不完整输出
scan.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(同源文件,示例未执行)
#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
#endifTEST-SPEC.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(同源文件,示例未执行)
/* 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 等诊断,工具无报告也不能证明所有调用前提成立。
继续阅读
- 知识专题
- 知识专题
- 知识专题