Kernel 学习

Linux Kernel

C 相关知识

C语言代码经过编译以后,并没有生成最终的可执行文件(.exe 文件),而是生成了一种叫做目标文件(Object File)的中间文件(或者说临时文件)。目标文件也是二进制形式的,它和可执行文件的格式是一样的

对于 Visual C++,目标文件的后缀是.obj;对于 GCC,目标文件的后缀是.o

目标文件并不能直接执行,首先需要载入到链接器中,链接器确认main函数为初始进入点,把符号引用绑定到内存地址,把所有的目标文件集中在一起,在加上库文件,从而产生可执行文件

静态链接

函数库的一份拷贝是可执行文件的物理组成部分

静态链接的模块被链接-编辑载入以便运行

动态链接

可执行文件只是包含了文件名,载入器在运行时寻找程序所需的函数库

动态链接的模块被链接-编辑载入,并在运行时链接以便运行

回调函数

回调函数的好处和作用就是解耦

来自维基百科的对回调(Callback)的解析:In computer programming, a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time. This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback. 也就是说,把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫做回调。如果代码立即被执行就称为同步回调,如果在之后晚点的某个时间再执行,则称之为异步回调。

来自Stack Overflow某位大神简洁明了的表述:A "callback" is any function that is called by another function which takes the first function as a parameter。 也就是说,函数 F1 调用函数 F2 的时候,函数 F1 通过参数给 函数 F2 传递了另外一个函数 F3 的指针,在函数 F2 执行的过程中,函数F2 调用了函数 F3,这个动作就叫做回调(Callback),而先被当做指针传入、后面又被回调的函数 F3 就是回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<softwareLib.h> // 包含Library Function所在读得Software library库的头文件

int Callback() // Callback Function
{
// TODO
return 0;
}
int main() // Main program
{
// TODO
Library(Callback);
// TODO
return 0;
}

只要我们改变传进库函数的参数,就可以实现不同的功能,这样是很灵活的,并且丝毫不需要修改库函数的实现,这就是解耦。

带参的回调函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<stdio.h>

int Callback_1(int x) // Callback Function 1
{
printf("Hello, this is Callback_1: x = %d ", x);
return 0;
}

int Callback_2(int x) // Callback Function 2
{
printf("Hello, this is Callback_2: x = %d ", x);
return 0;
}

int Callback_3(int x) // Callback Function 3
{
printf("Hello, this is Callback_3: x = %d ", x);
return 0;
}

int Handle(int y, int (*Callback)(int))
{
printf("Entering Handle Function. ");
Callback(y);
printf("Leaving Handle Function. ");
}

int main()
{
int a = 2;
int b = 4;
int c = 6;
printf("Entering Main Function. ");
Handle(a, Callback_1);
Handle(b, Callback_2);
Handle(c, Callback_3);
printf("Leaving Main Function. ");
return 0;
}

可以看出是通过增加一个参数来保存回调函数的参数值。

预处理指令

常见的预处理指令:

  • #define 宏定义
  • #undef 未定义宏
  • #include 文本包含
  • #ifdef 如果宏被定义就进行编译
  • #ifndef 如果宏未被定义就进行编译
  • #endif 结束编译块的控制
  • #if 表达式非零就对代码进行编译
  • #else 作为其他预处理的剩余选项进行编译
  • #elif 这是一种#else和#if的组合选项
  • #line 改变当前的行数和文件名称
  • #error 输出一个错误信息
  • #pragma 为编译程序提供非常规的控制流信息

#include

对于#include <io.h> ,编译器从标准库路径开始搜索 对于#include "io.h",编译器从用户的工作路径开始搜索

#error

1
2
3
#ifndef UNIX
#error This software requires the UNIX OS.
#endif

#progma

#progma主要功能是为编译程序提供非常规的控制流信息

预定义标识符

为了处理一些有用的信息,预处理定义了一些预处理标识符,虽然各种编译器的预处理标识符不尽相同,但是他们都会处理下面这4种:

1
2
3
4
5
6
7
8
9
__FILE__ 正在编译的文件的名字

__LINE__ 正在编译的文件的行号

__DATE__ 编译时刻的日期字符串,例如: "25 Dec 2000"

__TIME__ 编译时刻的时间字符串,例如: "12:30:55"

eg. cout<<"The file is :"<<__FILE__"<<"! The lines is:"<<__LINE__<<endl;

POSIX标准

  • POSIX,Portable Operating System Interface,是UNIX系统的一个设计标准,很多类UNIX系统也在支持兼容这个标准,如Linux

  • POSIX是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称

  • Linux基本上逐步实现了POSIX兼容,但并没有参加正式的POSIX认证

  • 当前的POSIX主要分为四个部分:Base Definitions、System Interfaces、Shell and Utilities和Rationale

POSIX大约有100个过程调用,其中最主要的过程调用如下被分为四类:

  • 进程管理
    • pid = fork:创建与父进程相同的子进程
    • pid = waitpid(pid, &statloc, options):等待一个子进程终止
    • s = execve(name, argv, environp):替换一个进程的核心映像
    • exit(status):终止进程执行并返回状态
  • 文件管理
    • fd = open(file, how, ...):打开一个文件供读、写或两者
    • s = close(fd):关闭一个打开的文件
    • n = read(fd, buffer, nbytes):把数据从一个文件读到缓冲区中
    • n = write(fd, buffer, nbytes):把数据从缓冲区写到一个文件中
    • position = lseek(fd, offset, whence):移动文件指针
    • s = stat(name, &buf):取得文件的状态信息
  • 目录和文件系统管理
    • s = mkdir(name, mode):创建一个新目录
    • s = rmdir(name):删除一个空目录
    • s = link(name1, name2):创建一个新目录项name2,并指向name1
    • s = unlink(name):删去一个目录项
    • s = mount(special, name, flag):安装一个文件系统
    • s = umount(special):卸载一个文件系统
  • 杂项
    • s = chdir(dirname):改变工作目录
    • s = chmod(name, mode):修改一个文件的保护位
    • s = kill(pid, signal):发送信号给一个进程
    • seconds = time(&seconds):自1970年1月1日起的流逝时间

头文件

  • unistd.h
    • Linux/Unix系统内置的头文件,包含了许多系统服务的函数原型,例如read函数、write函数和getpid函数等
    • 其作用相当于windows操作系统的"windows.h",是操作系统为用户提供的统一API接口,方便调用系统提供的一些服务

Makefile Grammar

Makefile文件里描述的是编译的时候依赖关系,宏定义,编译参数,链接生成的程序名字等等

文件包含

  • include 文件名
    • 将其他makefile文件包含进来,形成更大的makefile文件

变量定义

  • 变量名 := 变量值
    • 变量定义
  • 变量名 += 变量值1 [变量值2] [...]
    • 追加新的变量值
  • 自动变量
    • 自动变量的值会依据规则中的target 和 prerequisites自动计算其值,以$开始
    • **$@** 为规则中的target名称
    • $< 为规则中第一个prerequisite名称

内置命令

  • 字符串处理函数:subst、patsubst、strip、findstring、filter、filter-out、sort、word、wordlist、words、firstword、lastword

  • 文件名处理函数:dir、notdir、suffix、basename、addsuffix、addprefix、join、wildcard、realpath、abspath

    • dir

      • $(dir name1 [name2] […])

      • 获取name中文件对应的路径

      • E.g.

        1
        $(dir src/foo.c hacks) # 获取src/ ./两个路径
    • nodir

      • $(dir name1 [name2] […])

      • 获取name中除去路径的信息

      • E.g.

        1
        $(dir src/foo.c hacks) # 获取foo.c hacks两个文件信息
    • basename

      • $(basename name1 [name2] […])

      • 获取names中除去后缀信息

      • E.g.

        1
        $(basename src/foo.c src/bar.b hacks.c) # 获得信息src/foo src/bar hacks
    • addsuffix

      • $(addsuffix suffix,name1 [name2] […])
    • wildcard

      • $(wildcard pattern)

      • pattern为匹配的模式

      • E.g.

        1
        $(wildcard %.c) 查找当前目录下文件名以.c结尾的文件
  • 条件循环函数:if

  • 循环处理函数:foreach

    • foreach

      • $(foreach var, list, text)

      • 每循环一次var从list中按顺序取值一个

      • E.g.

        1
        2
        dirs := C_DIR S_DIR
        file := $(foreach dir,$(dirs),$(wildcard $(dir)/*)) # 将C_DIR和S_DIR文件夹下面的所有文件添加到file变量中

源码编译

源码的安装一般由3个步骤组成:配置(configure)、编译(make)、安装(make install)、卸载(make uninstall)

configure

configure通常检查环境,配置编译条件,一般是一个可执行脚本,有很多选项,在待安装的源码路径下使用命令./configure –help输出详细的选项列表。一般用来生成 Makefile,为下一步的编译做准备。

  1. ./configure --prefix

    它的作用是配置安装路径,如:./configure --prefix=/usr,意思是将该软件安装在/usr下面,执行文件就会安装在/usr/bin,资源文件就会安装在/usr/share,--prefix选项还有一个好处:卸载的时候,直接删除一个文件夹(安装目录)即可;移植软件也只需拷贝整个目录到另外一个机器即可(相同的操作系统)。

  2. ./configure

    不配置--prefix选项,安装后可执行文件默认放在/usr/local/bin,库文件默认放在/usr/local/lib,配置文件默认放在/usr/local/etc,其它的资源文件放在/usr/local/share

make

make命令从Makefile中读取指令,然后进行编译。

大多数的源代码包都经过这一步进行编译(当然有些perlpython编写的软件需要调用perl或python来进行编译)。如果在make过程中出现error,你就要记下错误代码(注意不仅仅是最后一行),然后你可以向开发者提交,bugreport(一般install里有提交地址),或者你的系统少了一些依赖库等,这些需要自己仔细研究错误代码。

make install

make install指令用来安装,从Makefile中读取指令,安装到指定的位置。安装的时候一般都需要root权限。

make install之前有些软件需要先运行make checkmake test来进行一些测试。

make uninstall

make uninstall指令用来卸载,在原先make的目录下运行,但前提是makefile文件指定过uninstall,如果没有指定则需要手动删除。

其他

当我们在使用make命令时,常常会在make后面加上其他单词,比如check,install,installcheck…这些单词都是make的参数,我们称之为“目标(targets)”。

最常见的几个target:

  • make all:编译程序、库、文档等(等同于make)
  • make install:安装已经编译好的程序。复制文件树中到文件到指定的位置
  • make unistall:卸载已经安装的程序。
  • make clean:删除由make命令产生的文件
  • make distclean:同时删除./configure和make产生的临时文件
  • make check/make test:测试刚刚编译的软件(某些程序可能不支持)
  • make installcheck:检查安装的库和程序(某些程序可能不支持)
  • make dist:重新打包成packname-version.tar.gz

GCC

几种常见的gcc参数

  • gcc -I /home/hello/include
    • 表示将/home/hello/include作为第一个寻找头文件的目录
    • 寻找顺序依次是:/home/hello/include --> /usr/include --> /usr/local/include
  • gcc -L /home/hello/lib
    • 表示将/home/hello/lib作为第一个寻找库文件的目录
    • 寻找顺序依次是:/home/hello/lib --> /usr/lib --> /usr/local/lib
  • gcc -lworld
    • 在上面的lib的路径中寻找libworld.so动态库文件,如果gcc编译选项中加入了“-static”表示寻找libworld.a静态库文件,程序链接的库名是world
  • gcc -shared hello.c -o libhello.so
    • 将hello.c编译成动态链接库
  • gcc -E hello.c -o test.i
    • 将test.c预处理输出test.i文件
  • gcc -S test.i
    • 将预处理输出文件test.i汇编成test.s文件
  • gcc -c hello.c
    • 仅执行编译操作,不进行连接操作。将hello.c编译成hello.o
  • gcc hello.c
    • 将hello.c编译成一个a.out的可执行文件
  • gcc hello.c -o hello
    • 将hello.c编译出一个名为test的可执行文件,-o用作指定输出文件的文件名
  • gcc hello.c -O2
    • 默认进行O0优化,-O1进行O1优化,-O2进行O2优化,-O3进行O3优化

常见的错误

  • xxxx.h: No such file or directory:引用出错,使用参数-I
    • -I参数可以用相对路径,比如头文件在当前目录,可以用-I.来指定
  • undefined reference to 'xxxxx':链接错误,应该指定链接程序要用到的库,使用参数-l指定
  • /usr/bin/ld: cannot find -lxxx:表明链接程序ld在那3个库文件目录里找不到libxxx.so,使用参数-L指定
    • 大部分libxxxx.so只是一个链接,以RH9为例,比如libm.so它链接到/lib/libm.so.x,/lib/libm.so.6又链接到/lib/libm-2.3.2.so,如果没有这样的链接,还是会出错,因为ld只会找libxxxx.so,所以如果你要用到xxxx库,而只有libxxxx.so.x或者libxxxx-x.x.x.so,做一个链接就可以了ln -s libxxxx-x.x.x.so libxxxx.so

-pthread & -lpthread

一般情况下我们在链接一个(文件名为libxxx.so或libxxx.a等)库时,会使用-lxxx的方式;在Linux中要用到多线程时,需要链接pthread库,按照惯例,我们应该使用-lpthread的方式来进行链接;但很多开源代码都是使用了-pthread参数,而非使用-lpthread,这是因为:

  1. 为了可移植性:在Linux中,pthread是作为一个单独的库存在的(libpthread.so),但是在其他Unix变种中却不一定,比如在FreeBSD中是没有单独的pthread库的,因此在FreeBSD中不能使用-lpthread来链接pthread,而使用-pthread则不会出现这个问题,因为FreeBSD的编译器能正确地将-pthread展开为该系统下的依赖参数。同样的,其他不同的变种也会存在差异。为了保持较高的可移植性,我们最好使用-pthread,即使它目前不是C标准,但基本上是事实标准
  2. 添加额外的标志:在多数系统中,-pthread会被展开为-D_REENTRANT -lpthread,即除了链接pthread库外,还先定义了宏_REENTRANT。定义这个宏的目的,是为了打开系统头文件中的各种多线程支持分支。比如我们常常使用的错误码标志errno,如果没有定义_REENTRANT,则实现为一个全局变量;反之则会为每个线程所独有,从而避免线程竞争错误

目前gcc 4.5.2中已经没有了关于 -lpthread的介绍了。所以以后的多线程编译应该用-pthread,而不是-lpthread

Linux 操作

下载文件 wget

  • wget -O-
    • -O 会把url中获取的数据统一写入 '-O' 指定的file中,这里是写到 '-' 中,即打印到标准输出,通常为控制台,进而可以通过管道传输给其他命令

添加环境变量

暂时添加

1
export PATH=./:$PATH #(将当前路径加入PATH,运行执行文件的时候不必再采用./file的形式,二是直接file即可)

为当前用户永久添加环境变量

1
2
3
# vim ~/.bashrc
# 追加以下文本
export PATH="新增的环境变量:$PATH"

退出 vim 后需要 source ~/.bashrc,让环境立即生效。

为所有用户永久添加某一环境变量

1
2
3
# vim /etc/profile
# 追加以下文本
export PATH="新增的环境变量:$PATH"

退出 vim 后需要 source /etc/profile,让环境立即生效。

/etc/environment 下面添加

1
2
# vim /etc/environment
# 追加环境变量

退出 vim 后需要 source /etc/environment,让环境立即生效。


Kernel 学习
http://shijieq.github.io/2021/12/17/OS/Kernel/
Author
ShijieQ
Posted on
December 17, 2021
Licensed under