重生之我是操作系统(二)----C语言开发与编译
简介
GCC,glicb,GNU C
- GCC
GCC全程GNU Compiler Collection,是 GNU 项目开发的编译器套装,
GCC最早的时候,是为了编译GNU/Linux系统和程序而生的,后续支持多种编程语言持 C、C++、Objective-C、Fortran、Ada、Go等。 - glibc
全程GNU C Library,glibc 是 GNU 项目实现的 C 语言标准库,提供操作系统底层接口(如系统调用、内存管理、I/O 操作等) - GNU C
GNU 项目下的 C 语言实现,对C语言进行拓展。
这三者相辅相成,GCC是编译器,glibc是运行时,GNU则定义了C语言标准与拓展
这三者可以理解为,JIT,CLR,C#的关系
POSIX
POSIX 即 “可移植操作系统接口(Portable Operating System Interface)”,是由电气和电子工程师协会(IEEE)制定的一系列标准,旨在为操作系统定义一个接口规范,以确保应用程序在不同的操作系统上具有良好的可移植性
其主要内容包括
- 系统调用和库
定义了操作系统应提供的核心服务,如IO/进程/线程等 - shell和工具
规定了标准命令接口与工具,如awk,echo,cd等标准命令 - 程序接口
包括语言,函数库,等接口规范
它们的目的都是为了统一规范,从而提高可移植性。
遗憾的是,Windows 本身不支持 POSIX 标准,Windows 主要使用 Win32 API,与 POSIX 的设计哲学不同。
通过 WSL、Cygwin 等工具可实现高度兼容,使得大部分 POSIX 应用能在 Windows 上运行或编译,但性能肯定存在损耗。
C语言编译过程
预处理
预处理是第一个阶段,主要是处理源代码中的替换宏,文件包含,条件编译,注释移除
等任务。
使用-E命令可以看到源码预处理后的内容
gcc -E main.c -o main.i
编译
编译阶段,编译器会将预处理的源代码文件转换成汇编代码。
gcc -S main.i -o main.s
汇编
此阶段由汇编器(Assembler)完成,主要是将汇编代码翻译成目标机器的二进制形式。
主要包含如下几个任务
- 符号解析
- 指令翻译
- 地址关联
- 重定位
- 代码优化
gcc -C main.s -o main.o
链接
此阶段有链接器完成,主要是将各个目标文件以及可能用到的库文件链接到一起
,生成最终可执行程序。
比如我们调用了printf()函数,这个函数是在stdio.h中声明的,它来源于glibc库。所以我们的main.c 不仅仅要链接#Include ,还要递归链接其引用的其它库。
链接方式主要有3种
- 静态链接
将所需的库在编译时,一并打包进最终的执行文件。也就是把库代码复制一遍,这样依赖自包含。
gcc -static main.o -o main.o -o main
- 动态链接
gcc默认的链接方式,依赖的库文件会在需要时调用运行时库文件。这样代码的体积就会大幅缩小
gcc main.o -o main.o -o main
- 混合链接
顾名思义,两者合一。
可以指定某些库动态链接,某些库静态链接。
这样可以更加灵活的适配系统,比如有些版本的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 等函数。
-
性能开销
系统调用是进入内核来操作,涉及到用户态与内核态的开销。
而大多数库调用仅在用户态执行,开销较小。 -
可移植性
系统调用可移植性较差,不同的OS提供的系统API各不相同。
库调用可移植性较好,尤其是一些标准库函数,如 C 语言标准库中的函数,在不同的操作系统和编译器上都可以使用。这是因为标准库函数的实现是遵循统一的标准,不同的操作系统和编译器会对标准库函数进行适配 -
错误处理
系统调用通常通过返回值(LastError)来表示调用的结果.
库函数的错误处理方式各不相同,有些库函数会通过返回值来表示错误,有些库函数会通过设置特定的错误标志或调用回调函数来处理错误
文件描述符
文件描述符(file description,fd)只是操作系统为底层资源(文件,socket)提供的一个抽象,或者说是一个引用。类似Windowis平台下的"句柄"
其中,fd的0,1,2具有特殊含义。
*. 0代表标准输入(stdin)
*. 1代表标准输出(stdout)
*. 2代表标准错误(stderr)
当我们执行open等系统调用时,内核会创建一个新的struct file,这个数据结构记录了元数据,路径,权限等。然后将该数据结构维护在文件描述符表中(struct fdtable,是一个数组),最后将偏移量
返回给程序。也就是fd_id。
因此文件描述符的本质,是一个内核数据结构的指针
。
每创建一个进程,内核都会为进程分配一个文件描述符表。如果父进程中创建了子进程。那么父子进程共享同一个文件描述符表
眼见为实
cd /usr/src/linux-hwe-6.11-headers-6.11.0-21/include/linux/fs.h