01. linux系统编程入门

入门系统编程,首先理解一下

  • 基本的系统调用和库函数的区别
  • 一切皆文件的思想,都是通过文件描述符来进行操作
  • 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 4096

int 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;
}

以上有几个点:

  1. FILE *: 是 C 语言标准库中的 文件指针,用于管理 文件流。它指向一个 FILE 结构体,该结构体维护了:
    • 文件描述符
    • 缓冲区
    • 当前读/写位置
    • 状态(EOF、错误等)
    • 因为是一个结构体有进一步的包装,所以判断异常要使用ferror(fp) != 0
  2. 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), 一个 抽象层 统一了文件系统接口,让应用程序和内核不需要关心底层使用的是什么文件系统,在内核中提供了统一的文件操作接口

另外,系统调用的流程是:

  1. 软中断:X86下int 0x80;ARM架构下 SWI软中断指令
  2. 寄存器保存相关参数:参数、系统调用号
  3. 进入内核态,执行内核特权指令代码函数
  4. 返回值保存到寄存器
  5. 返回到用户态、系统调用结束

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;
posted @ 2025-03-18 12:10  rustic-stream  阅读(80)  评论(0)    收藏  举报