Linux 系统调用的本质

简单概念

  1. fd
    在这里插入图片描述
#include <unistd.h>
#include <string.h>

int main(int argc,char* argv[])
{
    char buf[20]={0};
    read(0,buf,15);
    write(1,buf,strlen(buf));

    return 0;
}
  1. 如果想查看某个系统编程的接口,比如想查看 open 函数的用法,可以这样操作:man 2 open。可以看到open函数在哪里定义的,以及open函数的使用方法。
  2. 首先看一个简单的demo1
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc,char* argv[])
{
    int fd=0;
    fd=open("hello.txt",O_RDWR|O_CREAT,0666);
    if(fd == -1)
    {
        printf("open file failed!\n");
        return -1;
    }
    char string[128]="Hello Word!\n";
    write(fd,string,strlen(string));
    fsync(fd); //1.为什么要用fsync?因为调用write(...)函数时候,并没有真正写入到磁盘上,可能还在缓冲区中,等缓冲区满了之后,再一起写到磁盘中。
               //2.所以,fsync(...) 是强制让缓冲区中的内容写到磁盘中。
    char* buf=(char*)malloc(20);
    memset(buf,0,20);
    lseek(fd,0,SEEK_SET);   // 1.SEEK_SET : 代表文件开始 SEEK_CUR:文件当前位置 SEEK_END:文件末尾
                            // 2.因为上面调用 write(...)函数,使得文件指针偏移了,现在使用lseek(...)将文件指针还原。
    read(fd,buf,strlen(string));
    printf("%s",buf);

    free(buf);
    close(fd);

    return 0;
}

3.自己动手实现一个cp 命令

#include <stdio.h>
#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:\n copy src dst\n");
        return 1;
    }

    int srcfd = open(argv[1],O_RDONLY);
    if(srcfd == -1)
    {
        perror("open");
        return 1;
    }
    int dstfd = open(argv[2],O_CREAT|O_WRONLY,0666);
    if(dstfd == -1)
    {
        perror("open");
        return 1;
    }

    int len=0;
    char buffer[BUFFERSIZE] = {0};
    while((len=read(srcfd,buffer,BUFFERSIZE)) > 0)
    {
        if(write(dstfd,buffer,len) != len)
        {
            perror("error");
            return 2;
        }
    }
    if(len<0)
    {
        perror("read error");
        return 3;
    }
    close(srcfd);
    close(dstfd);

    return 0;
}

系统调用与C标准库函数

  1. 一般来说C语言中很多函数 fopen、fclose、fseek、fread、fwrite等待都是对Linux 系统调用的封装。
    在这里插入图片描述
    在这里插入图片描述
    一般来说,Linux各种系统的调用流程如上图所示。

  2. 首先用户空间调用 read 函数,通过中断机制,进入到内核空间,内核空间有一个虚拟文件系统(VFS),虚拟文件系统(VFS)是对内核中各种各样的设备的抽象。

  3. 我们知道Linux中一切皆文件,对每个设备都抽象成一个文件。每个文件的 读/写 又是不一样的,因此需要一个公用的VFS虚拟文件系统对内核的文件进行抽象。

系统调用的基本流程:

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

内嵌汇编剖析系统调用的本质

一个应用程序运行起来有三种加载方式

  1. 用户进程 -> 第三方库(或者叫做中间件) -> Linux系统调用 -> Linux 内核 -> 计算机硬件。
  2. 用户进程 -> Linux系统API -> Linux系统调用 -> Linux 内核 -> 计算机硬件。
  3. 用户进程 -> Linux系统调用 -> Linux 内核 -> 计算机硬件。

系统调用的本质:就是模式切换

  1. 系统模式切换依赖于CPU提供的工作方式
  2. 一般来说,大部分CPU至少具体两种工作方式
  3. 高特权级(Ring 0)(内核模式) :可以访问任意的数据,包括外围设备,比如网卡,硬盘
  4. 低特权级(Ring 1)(用户模式):只能受限的访问内存,并且不允许访问外围设备,可被打断。

看一下下面这个例子,与我们平常写的程序有几点不同:

1.没有 #include<stdio.h>,2.没有main()函数,3.内嵌汇编代码


void print(const char* s,int l);
void exit(int code);

void program()
{
    print("Hello World!\n",13);
    exit(0);
}

void print(const char* s,int l)  //封装了系统调用
{
    asm volatile (
        "movl $4,%%eax\n" //指定编号为4的系统调用(sys_write)
        "movl $1,%%ebx\n" //指定sys_write的输出目标,1为标准输出
        "movl %0,%%ecx\n" //指定输出字符串地址
        "movl %1,%%edx\n" //指定输出字符串长度
        "int $0x80    \n" //执行系统调用,请求内核干事情,请求 编号为4的系统调用(sys_write)向屏幕输出
        :                 //忽虐输出参数
        : "r"(s),"r"(l)
        : "eax","ebx","ecx","edx" //保留寄存器,不用于关联变量
    );
}

void exit(int code)     //封装了系统调用
{
    asm volatile (
        "movl $1, %%eax\n"   //1号系统调用,结束当前进程
        "movl $0, %%ebx\n"
        "int $0x80   \n"
        :
        : "r"(code)
        : "eax","ebx"
    );
}

运行:gcc -m32 -e program -fno-builtin -nostartfiles program.c -o program.out && && ./program.out
note:如果有报错记得,切换到正确的gcc版本。
可以看到程序正确的运行出来了,这里我们根本没有包含#include<stdio.h> 还有C语言的运行时库 ,这里我们走的是:用户进程 -> Linux系统调用 -> Linux 内核 -> 计算机硬件,这一条路径,传统的是使用第三方库(glic.so) 。

strace证明使用了系统调用

  1. strace 系统调用探测器,strace用于监控进程与内核的交互(监控系统调用)
  2. strace用于追踪进程内部状态(定位运行时问题)
  3. strace按序输出进程运行过程系统调用名称,参数和返回值

strace -o ./program.log ./program.out

execve("./program.out", ["./program.out"], 0x7fff5fb7c860 /* 58 vars */) = 0
brk(NULL)                               = 0x57ba6000
arch_prctl(0x3001 /* ARCH_??? */, 0xffad6458) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7fbe000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
set_thread_area({entry_number=-1, base_addr=0xf7fbe9c0, limit=0x0fffff, seg_32bit=1, contents=0, read_exec_only=0, limit_in_pages=1, seg_not_present=0, useable=1}) = 0 (entry_number=12)
mprotect(0x5663a000, 4096, PROT_READ)   = 0
write(1, "Hello World!\n", 13)          = 13
exit(0)                                 = ?
+++ exited with 0 +++

可以看到的确使用了系统调用。

posted @ 2023-01-02 17:47  repinkply  阅读(69)  评论(0)    收藏  举报