重生之我是操作系统(二)----C语言开发与编译

简介

GCC,glicb,GNU C

  1. GCC
    GCC全程GNU Compiler Collection,是 GNU 项目开发的编译器套装,
    GCC最早的时候,是为了编译GNU/Linux系统和程序而生的,后续支持多种编程语言持 C、C++、Objective-C、Fortran、Ada、Go等。
  2. glibc
    全程GNU C Library,glibc 是 GNU 项目实现的 C 语言标准库,提供操作系统底层接口(如系统调用、内存管理、I/O 操作等)
  3. GNU C
    GNU 项目下的 C 语言实现,对C语言进行拓展。

这三者相辅相成,GCC是编译器,glibc是运行时,GNU则定义了C语言标准与拓展
这三者可以理解为,JIT,CLR,C#的关系

POSIX

POSIX 即 “可移植操作系统接口(Portable Operating System Interface)”,是由电气和电子工程师协会(IEEE)制定的一系列标准,旨在为操作系统定义一个接口规范,以确保应用程序在不同的操作系统上具有良好的可移植性
其主要内容包括

  1. 系统调用和库
    定义了操作系统应提供的核心服务,如IO/进程/线程等
  2. shell和工具
    规定了标准命令接口与工具,如awk,echo,cd等标准命令
  3. 程序接口
    包括语言,函数库,等接口规范

它们的目的都是为了统一规范,从而提高可移植性。

遗憾的是,Windows 本身不支持 POSIX 标准,Windows 主要使用 Win32 API,与 POSIX 的设计哲学不同。
通过 WSL、Cygwin 等工具可实现高度兼容,使得大部分 POSIX 应用能在 Windows 上运行或编译,但性能肯定存在损耗。

C语言编译过程

预处理

预处理是第一个阶段,主要是处理源代码中的替换宏,文件包含,条件编译,注释移除等任务。

使用-E命令可以看到源码预处理后的内容

gcc -E main.c -o main.i

image

编译

编译阶段,编译器会将预处理的源代码文件转换成汇编代码。

gcc -S main.i -o main.s

image

汇编

此阶段由汇编器(Assembler)完成,主要是将汇编代码翻译成目标机器的二进制形式。
主要包含如下几个任务

  1. 符号解析
  2. 指令翻译
  3. 地址关联
  4. 重定位
  5. 代码优化
gcc -C main.s -o main.o

image

链接

此阶段有链接器完成,主要是将各个目标文件以及可能用到的库文件链接到一起,生成最终可执行程序。
比如我们调用了printf()函数,这个函数是在stdio.h中声明的,它来源于glibc库。所以我们的main.c 不仅仅要链接#Include ,还要递归链接其引用的其它库。
链接方式主要有3种

  1. 静态链接
    将所需的库在编译时,一并打包进最终的执行文件。也就是把库代码复制一遍,这样依赖自包含。
gcc -static main.o -o main.o -o main
  1. 动态链接
    gcc默认的链接方式,依赖的库文件会在需要时调用运行时库文件。这样代码的体积就会大幅缩小
gcc main.o -o main.o -o main
  1. 混合链接
    顾名思义,两者合一。
    可以指定某些库动态链接,某些库静态链接。
    这样可以更加灵活的适配系统,比如有些版本的linux的装在的lib版本与需要的不一样。那么就可以针对特殊部分进行混合链接
ar crv xxxx main.o

通常情况下,自己写的代码做成静态库,执行程序链接你的库。系统代码用动态库做远程调用。

一个简单的文件IO读写

库调用

#include <stdio.h>
int main(int argc, char const *argv[])
{
    /**
     * const char *__restrict __filename,  要打开的文件名词
           const char *__restrict __modes 访问模式 r:只读模式,w:只写模式,a:追加模式,r+:读写模式
     */
    char * fileName="io.txt";
    FILE *ioFile=fopen(fileName,"w");

    if(ioFile==NULL){
        printf("文件打开报错");
        return 0;
    }
    printf("file open success.");


    /**
     * 写入数据
     */

    // int put_result= fputc(979797,ioFile);
    // if(put_result==EOF){
    //     printf("write file fail. ");
    //     return 0;
    // }

    // printf("write file success. ");

    // int put_result=fputs("三大赛,绘画和,sdsdsd",ioFile);
    // if(put_result==EOF){
    //     printf("write file fail. ");
    //     return 0;
    // }

    char *name="咖喱给给";
    int put_result= fprintf(ioFile,"司法斯蒂芬斯蒂芬锁定?\n哈哈哈哈\n 升起把,为的太阳!\n\t\t %s",name);
    if(put_result==EOF){
        return 0;
    }

    /**
     * 关闭文件
     */
    int result= fclose(ioFile);
    if(result==EOF){
        printf("close file fail. ");
        return 0;
    }

    printf("close file success. ");

    return 0;
}

系统调用

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    //打开文件

    int file_fd=open("io.txt",O_RDONLY);
    if(file_fd==-1){
        perror("文件打开失败");
        exit(EXIT_FAILURE);
    }

    //读取文件

    char buffer[1024];
    int bytes_read;
    bytes_read=read(file_fd,buffer,sizeof(buffer));
    if(bytes_read==-1){
        perror("读取文件出错. ");
        close(file_fd);
        exit(EXIT_FAILURE);
    }
    write(STDOUT_FILENO,buffer,bytes_read);

    //关闭文件
    int close_status= close(file_fd);
    if(close_status==-1){
        perror("关闭文件失败. ");
    }



    return 0;
}

系统调用与库调用的区别

系统调用:
操作系统提供给用户程序的接口,用于请求操作系统内核提供特定的服务,比如创建进程、读写文件、分配内存等。它是用户程序与操作系统内核进行交互的桥梁,直接涉及到对系统资源的访问和控制。
库调用:
对库函数的调用,库函数是预先编写好的代码集合,封装了一些常用的功能。这些库函数可以在用户空间中执行,无需直接与操作系统内核交互。例如,C 语言标准库中的 printf、strlen 等函数。

  1. 性能开销
    系统调用是进入内核来操作,涉及到用户态与内核态的开销。
    而大多数库调用仅在用户态执行,开销较小。

  2. 可移植性
    系统调用可移植性较差,不同的OS提供的系统API各不相同。
    库调用可移植性较好,尤其是一些标准库函数,如 C 语言标准库中的函数,在不同的操作系统和编译器上都可以使用。这是因为标准库函数的实现是遵循统一的标准,不同的操作系统和编译器会对标准库函数进行适配

  3. 错误处理
    系统调用通常通过返回值(LastError)来表示调用的结果.
    库函数的错误处理方式各不相同,有些库函数会通过返回值来表示错误,有些库函数会通过设置特定的错误标志或调用回调函数来处理错误

文件描述符

文件描述符(file description,fd)只是操作系统为底层资源(文件,socket)提供的一个抽象,或者说是一个引用。类似Windowis平台下的"句柄"
其中,fd的0,1,2具有特殊含义。
*. 0代表标准输入(stdin)
*. 1代表标准输出(stdout)
*. 2代表标准错误(stderr)

image

当我们执行open等系统调用时,内核会创建一个新的struct file,这个数据结构记录了元数据,路径,权限等。然后将该数据结构维护在文件描述符表中(struct fdtable,是一个数组),最后将偏移量返回给程序。也就是fd_id。
因此文件描述符的本质,是一个内核数据结构的指针

每创建一个进程,内核都会为进程分配一个文件描述符表。如果父进程中创建了子进程。那么父子进程共享同一个文件描述符表

眼见为实

cd /usr/src/linux-hwe-6.11-headers-6.11.0-21/include/linux/fs.h

image
image

posted @ 2025-04-02 08:47  叫我安不理  阅读(455)  评论(0)    收藏  举报