入门系统编程,首先理解一下
- 基本的系统调用和库函数的区别
- 一切皆文件的思想,都是通过文件描述符来进行操作
- strace命令
文件读写系统调用
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main (void)
{int fd;fd = open("hello", O_RDWR|O_CREAT, 0666); // 0666 (oct) = 110 110 110 (binary) = rw-rw-rw- (文件权限) //所有者(User)| 同组 Group | 其他人 Other的权限:可读 (r),可写 (w),不可执行 (-)if (fd == -1){printf("open file failed!\n");return -1;}char string[20] = "hello world!\n";write (fd, string, 14);fsync (fd); // 同步刷新到磁盘中char *buf = (char*)malloc (20);memset(buf, 0, 20);lseek(fd, 0, SEEK_SET); // SEEK_SET:文件开始 | SEEK_CUR:文件当前位置 | SEEK_END:文件末尾read(fd, buf, 14);printf("%s", buf);free(buf);close(fd);return 0;
}
复制文件
将源文件读出来后写到另一个文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> #define BUFFERSIZE 4096int main(int argc, char *argv[])
{if (argc != 3){printf("Usage: copy src dest\n");return 1;}int srcfd = open(argv[1], O_RDONLY); // 以只读的方式打开文件if (srcfd == -1){perror("open srcfd failed");return 1;}int dstfd = open(argv[2], O_WRONLY|O_CREAT, 0666); // 以只写的方式,不存在时创建的情况打开文件if (dstfd == -1) {perror("open destfd failed");return 1;}int len = 0;char buffer[BUFFERSIZE] = {0};// 读写都会返回读写的长度,需要对长度进行检查while ((len = read(srcfd, buffer, BUFFERSIZE)) > 0){if (write(dstfd, buffer, len) != len) // 对写操作进行检查{perror("Write error");return 2;}}// 读操作如果返回 len<0 就是出现了问题if (len < 0){perror("read error");return 3;}close(srcfd);close(dstfd);return 0;
}
文件系统调用参数选项
以上是文件的系统调用,然后我们来理解一下文件的C标准库
文件C标准库读写文件
#include <stdio.h>
#include <stdlib.h>struct student
{char name[10];int age;float score;
} stu[2];int main (void)
{for (int i = 0; i < 2; i ++) {printf("please input name age score:\n");scanf("%s %d %f", stu[i].name, &stu[i].age, &stu[i].score);}FILE *fp;if ((fp = fopen("hello.dat", "w+")) == NULL){printf("fopen failed!\n");return -1;}fwrite(&stu, sizeof(struct student), 2, fp);if (ferror(fp) != 0) // 检测文件流fp是否发生了错误{printf("fwrite failed!\n");clearerr(fp);return -1;}fflush(fp);rewind(fp); // 等价于 fseek(fp, 0, SEEK_SET)struct student *buf = (struct student*)malloc (2*sizeof(struct student));if (ferror(fp) != 0){printf("fread failed!\n");clearerr(fp);return -1;}printf("姓名\t年龄\t分数\t\n");for (int i = 0; i < 2; i ++)printf("%s\t%d\t%f\n", buf[i].name, buf[i].age, buf[i].score);fclose(fp);free(buf);buf = NULL;return 0;
}
以上有几个点:
FILE *
: 是 C 语言标准库中的 文件指针,用于管理 文件流。它指向一个 FILE 结构体,该结构体维护了:- 文件描述符
- 缓冲区
- 当前读/写位置
- 状态(EOF、错误等)
- 因为是一个结构体有进一步的包装,所以判断异常要使用
ferror(fp) != 0
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
- 用于向文件写入二进制数据
- prt:指向要写入的数据的指针
- size:每个元素的大小
- nmemb:要写入的元素大小
- stream:目标文件流指针
系统调用和C标准库
所以对于系统调用和C标准库,它的不同就比如说在linux下对文件的系统调用是open()
等等,然后有c标准库,它使用了FILE *
,然后fopen
来打开,实际上底层还是使用的open()
只是对这个方法的有一层封装,让其更实用一点,而且FILE *
本身有缓存区,FILE *
结构有缓冲区(buffering),这意味着 fread()
、fwrite()
可能不会每次都直接调用 read()
、write()
,而是先写到缓冲区,等缓冲区满了才调用系统调用,这样能减少系统调用次数,提高性能,而且也提供了更多方便的方法
另一个原因就是因为说如果是不同的平台比如说win下他对文件的系统调用可能不是open()
,如果使用c标准库,你只用 fopen()
而不用管 Linux、Windows、macOS 具体的系统调用,它们会自动适配不同的操作系统。,从而实现跨平台性,这也是很重要的
对linux就是使用虚拟文件系统(VFS), 一个 抽象层 统一了文件系统接口,让应用程序和内核不需要关心底层使用的是什么文件系统,在内核中提供了统一的文件操作接口
另外,系统调用的流程是:
- 软中断:X86下int 0x80;ARM架构下 SWI软中断指令
- 寄存器保存相关参数:参数、系统调用号
- 进入内核态,执行内核特权指令代码函数
- 返回值保存到寄存器
- 返回到用户态、系统调用结束
strace命令的使用
当你想要知道你的程序调用的方法,花费时间时,可以使用strace来观察
-e 的作用就是指定显示,这里就是指定显示open和close的调用
<0.000089> 就是 -T 的作用显示系统调用花费的时间
$ strace -T -e open,close ./a.out
close(3) = 0 <0.000089>
close(3) = 0 <0.000113>
close(3) = 0 <0.000086>
open hello.dat failed!
+++ exited with 255 +++
-tt 就是在前面显示具体的时间戳 07:42:19.519597 这种
$ strace -tt -T -e open,close ./a.out
07:42:19.519597 close(3) = 0 <0.000137>
07:42:19.524255 close(3) = 0 <0.000112>
07:42:19.528934 close(3) = 0 <0.000452>
open hello.dat failed!
07:42:19.532642 +++ exited with 255 +++
-o log 作用就是将输出写到 log 文件中
$ strace -tt -T -e open,close -o log ./a.out
open hello.dat failed!
lqy@lqy:~/projects/system_prog/00. 入门篇/08.strace$ ls
08.strace.c a.out log stract命令使用.md test.dat
明明指定了看open()
的系统调用,为什么没有?
- 不显示open的系统调用是因为在现代 Linux 版本(glibc 2.26+)
open()
可能被openat()
取代,所以strace -e open
可能不会捕获
$ strace -tt -T -e open,openat,close ./a.out
07:49:51.845942 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 <0.000249>
07:49:51.849882 close(3) = 0 <0.000096>
07:49:51.850700 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 <0.000136>
07:49:51.854783 close(3) = 0 <0.000254>
07:49:51.858475 openat(AT_FDCWD, "test.dat", O_RDONLY|O_CREAT, 051010) = 3 <0.000129>
// 这里open返回的 = 3 就是说这个打开后给的文件描述符是 3
// 打开失败就是返回的文件描述符是 -1
07:49:51.859134 openat(AT_FDCWD, "hello.dat", O_WRONLY) = -1 ENOENT (没有那个文件或目录) <0.000114>
07:49:51.860171 close(3) = 0 <0.000116>
open hello.dat failed!
07:49:51.862608 +++ exited with 255 +++
显示错误信息 errno
系统编程错误一般通过函数的返回值表示,执行成功,返回0或正确值,执行失败,返回-1,并把系统全局变量errno赋值,指示具体错误,一般返回值只知道是否执行成功,而更具体的错误由errno
指出,使用perror
可以输出具体错误原因
全局变量errno
由操作系统维护:当系统调用或调动库函数出错时,会重置该值
int main (void)
{int fd;fd = open("hello.dat", O_WRONLY);if (fd == -1){// 会输出 open hello.dat failed!: No such file or directoryopen hello.dat failed: // 输出了用户定义的部分后,后面会加上erron执行的错误perror ("open hello.dat failed!");return -1;}close(fd);return 0;