一、前言
- 主要使用C语言标准库函数实现程序。
- C语言本身只提供一些基础的语言特性和标准库函数,如内存管理、字符串处理、文件IO等。标准C语言库并不包含进程、线程、网络库的内容,这些高级特性需要使用操作系统提供的API或第三方库来实现。
- 本文主要环境是使用类Unix系统,基于Linux的Ubuntu 20.04TLS 搭建APUE环境。环境搭建参考上篇使用Ubuntu搭建apue环境的绝佳姿势,附带一系列工具操作
二、 UNIX简要知识点
系统体系
systemcall: 系统调用是实现其功能的内核代码的入口点。如write(2)函数
library routines: 公用函数库,是构建在系统调用接口之上.
shell : 提供一个运行其他应用程序的接口。
应用可以选择使用公共函数库或直接通过系统调用。
程序设计
Unix中一切皆文件。
从标准输入,到标准输出,标准错误,每个都有对应描述符。
标准IO
- 内核提供unbuffered非缓存I/O。如:open(2),read(2),write(2),lseek(2),close(2)
- 内核提供buffered 缓存IO。如:fopen(3),fread(3),fwrite(3),getc(3),putc(3)
tips :
fread(3) 中的数字 3 是与库函数相关的一个章节号。在 Unix/Linux 系统中,手册页(Man Pages)是一种文档形式,用于描述特定命令、库函数等的使用和语法。手册页被组织成不同的章节,每个章节都涉及不同的主题。这些章节通常是:
- 用户命令(User Commands):这个章节包括用户可以直接调用的命令,如 ls、cd 和 grep 等。
- 系统调用(System Calls):这个章节包括系统调用和 C 库函数等。
- 库函数(Library Functions):这个章节包括 C 库函数和标准函数库等。
- 特殊文件(Special Files):这个章节包括很少使用的设备和文件,如 /dev 和 /proc 等。
- 文件格式和约定(File Formats and Conventions):这个章节包括文件格式和约定,如配置文件格式等。
- 游戏和娱乐(Games and Demos):这个章节包括游戏和演示软件等。
- 杂项(Miscellaneous):这个章节包括其他未分类的文档。
使用man man
可以使用man 命令来访问这些内容,试试输入 man 3 fread
时间
有时我们想知道一个程序花费了多长时间运行
这个时间用三个不同值来衡量
- clock time :已经过去的总时间
- user CPU time :用户花费的时间
- systerm CPU time:系统CPU 或者花费在内核空间过程的时间
你可能认为 clocktime =User time + System time,但是被忽略了一种进程被阻塞的场景:比如在等待IO 。
让我们来验证一下,找系统中一个目录中源码有多少行
time find /usr/include/ -name '*.[ch]' -exec cat {} ; | wc -l
改进方式
time find /usr/include/ -name '*.[ch]' -print | xargs cat | wc -l
对比差异 , 可以看见各个阶段花费时间。
我们可以使用time测量方式,来优化我们的程序,找到花费时间最多的地方。
三、 一些常见命令简约实现
ls命令
简版列出文件目录命令,是后续主要实现程序目标,最终达到和具有和系统ls命令一样的功能(如附加参数 ,usage等)
#include <stdio.h> //提供输入输出函数定义
#include <dirent.h> //提供读取目录和目录数据结构定义 POSIX 标准
#include <errno.h> //提供错误代码定义
#include <stdlib.h> //提供 exit() 函数定义
#include <string.h> //提供 strerror() 函数定义
// 为了方便,后面使用apue.h,里面自动包含常用的头
int main(int argc, char *argv[]) {
DIR *dir = NULL; //定义指向目录数据结构的指针
struct dirent *df; //定义指向目录项数据结构的指针
if (argc != 2) { //检查命令行参数个数是否为2
fprintf(stderr, "usage: %s dirent_name", argv[0]); //输出使用说明
exit(EXIT_FAILURE); //退出程序
}
errno = 0; //将错误代码初始化为0
if ((dir = opendir(argv[1])) == NULL) { //尝试打开指定目录,如果失败则输出错误信息
fprintf(stdout, "unable to open '%s' %s", argv[1], strerror(errno));
exit(EXIT_FAILURE); //退出程序
}
while ((df = readdir(dir)) != NULL) { //循环读取目录中的每个目录项
fprintf(stdout, "%s\n", df->d_name); //输出目录项名称
}
exit(EXIT_SUCCESS); //正常结束程序
}
cp 命令
#include "apue.h"
#define BufferSize BUFSIZ
int main(int c, char *a[]) {
int n;
char buf[BufferSize];
while ((n = read(STDIN_FILENO, buf, BufferSize)) > 0) {//n 为实际读取到的字节数,可能不足BufferSize的长度
if (write(STDOUT_FILENO, buf, n) != n) // 注意第三个参数为n,确保只将已经读取到的有效数据写入文件,从而保证程序的正确性和稳定性
err_sys("write error");
}
if (n < 0)
err_sys("read error!");
exit(0);
}
编译,使用 将a.txt内容复制到b.txt
gcc -std=c99 -Wall -Wextra 02_cp.c -lapue -o 02_cp
./02_cp <a.txt >b.txt
cat 命令
使用fgetc putc实现
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[]) {
errno = 0;
int c;
while ((c = fgetc(stdin)) != EOF) {
if (putc(c, stdout) == EOF) {
fprintf(stderr, "Unable to write: %s", strerror(errno));
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}
使用read write实现
#include "stdio.h"
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#define BF 4096
int main(int c, char *v[]) {
int n;
char buff[BF];
errno = 0;
while ((n = read(STDIN_FILENO, buff, BF)) > 0) {
if (write(STDOUT_FILENO, buff, n) != n) {
fprintf(stderr, "Fail write:%s", strerror(errno));
exit(EXIT_FAILURE);
}
}
if (n < 0) {
fprintf(stderr, "Fail read:%s", strerror(errno));
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
// 使用
./cat <a.txt
shell 程序
实现一个shell程序,使其能运行其他命令
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <unistd.h>
// 获取用户输入的函数
char *getinput(char *buffer, size_t buflen) {
printf("%s@tsh> ", getlogin());
return fgets(buffer, buflen, stdin);
}
// SIGINT 信号处理函数
void sig_handle(int signo) {
printf("\nCaught SIGINT (Signal #%d)!\n%s@tsh> ", signo, getlogin());
(void) fflush(stdout);
}
int main(int argc, char *argv[]) {
char buf[BUFSIZ]; // 存储用户输入的缓冲区
pid_t pid; // 子进程 ID
int status; // 进程状态
// 注册 SIGINT 信号处理函数
if (signal(SIGINT, sig_handle) == SIG_ERR) {
fprintf(stderr, "signal error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// 循环读取用户输入
while (getinput(buf, sizeof(buf))) {
buf[strlen(buf) - 1] = '\0'; // 去除换行符
int index = 0;
char *temp;
char *args[BUFSIZ];
// 将用户输入分割成多个子串
args[index++] = strtok(buf, " \n");
while ((temp = strtok(NULL, " \n")) != NULL) {
args[index++] = temp;
}
args[index] = NULL;
// 创建子进程执行命令
if ((pid = fork()) == -1) { // 失败处理
fprintf(stderr, "shell: can't fork: %s\n",
strerror(errno));
continue;
} else if (pid == 0) { // 子进程执行
execvp(args[0], args); // 执行用户命令
fprintf(stderr, "shell: couldn't exec %s: %s\n", buf,
strerror(errno)); // 如果执行失败,则输出错误信息
exit(EX_UNAVAILABLE); // 退出子进程
}
// 等待子进程结束
if ((pid = waitpid(pid, &status, 0)) < 0) {
fprintf(stderr, "shell: waitpid error: %s\n",
strerror(errno));
}
}
exit(EX_OK); // 退出程序
}
具体的执行过程如下:
- 使用
signal()函数注册 SIGINT 信号的处理函数sig_handle()。 - 进入一个循环。每次循环中,使用
getinput()函数获取一行用户输入,并将其存储在字符数组buf中。 - 对于每行用户输入,先使用
strtok()函数将其分解为多个子串,存储在字符指针数组args中,以便后续执行。 - 创建一个子进程,并在子进程中调用
execvp()函数,执行用户输入的命令。如果创建子进程失败,则回到循环开头。 - 在父进程中等待子进程执行结束,并输出相应的错误信息(如果有的话)。
- 循环结束后,退出程序并返回相应的状态码。
测试
四、 最后
经过前面的步骤,我们基本大致能够对UNIX环境编程有了一些印象。这些年,无论上层的变化有多大多快,都是在这些基础之上衍生的,而的底层的一些实现往往是我们所忽略的部分。如果我们了解这些构造,知道其运作方式,将更有助于应对应用层编程,无形中会帮助我们能在编程道路上走得更远更踏实。