Chapter3-1(续)

系统调用(掌握)和库函数(当题目指定系统调用时不能使用)

系统调用和库函数都以C函数的形式出现

系统调用

  • Linux内核的对外接口
  • 用户程序和内核之间唯一的接口
  • 提供最小接口

库函数

  • 依赖于系统调用
  • 提供复杂功能(例如:标准I/O库)

以I/O作为样例

  • 非缓存IO
    • 读写操作直接调用系统调用
    • 文件描述符
    • Not in ANSI C, but in POSIX.1 and XPG3
  • 缓存IO
    • 使用标准IO库
    • 处理很多细节,如缓存分配以及优化长度执行IO
    • 流是一个文件中的指针

系统IO调用

文件描述符

一个非负的整形(如在文件中STDIN_FILENO (0), STDOUT_FILENO (1), STDERR_FILENO (2))

一般的文件操作的步骤:打开,读/写,(lseek),关闭

基本的IO函数

先给一个文件的最简单的读写样例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
/* a rudimentary example program */
#include <fcntl.h>
main()
{
int fd, nread;
char buf[1024];
/*open file “data” for reading */
fd = open(“data”, O_RDONLY);
/* read in the data */
nread = read(fd, buf, 1024);
/* close the file */
close(fd);
}
open/creat方法

用于打开或者创建一个文件或者设备

1
2
3
4
5
6
7
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
(Return: a new file descriptor if success; -1 if failure)

flags参数:文件的访问模式

  • O_RDONLY:只读操作
  • O_WRONLY:只写操作
  • O_RDWR:读写操作
  • O_APPEND:文件以追加(append)模式打开
  • O_TRUNC:如果文件已经存在且是一个常规文件且允许以写模式打开则截断该文件长度为0(清空文件?)
  • O_CREAT:如果文件不存在则被创建
  • O_EXCL:和O_CREAT一起使用,如果文件已经存在则是一个错误且打开文件失败。

creat方法等价于以(O_CREAT|O_WRONLY|O_TRUNC)作为flag使用open方法

mode参数:指定创建新文件的权限,列表如下:

mode参数

新建文件权限由mode和umask共同决定,如图

文件权限

close方法
1
2
3
#include <unistd.h>
int close(int fd);
(Return: 0 if success; -1 if failure)
  • 主要系统调用要求掌握,指定系统调用的题不能用C库
  • 进程相关的系统调用不要求掌握
  • LCTL不考
  • C库简答题可能要考
  • 权限要掌握
  • 文件锁要求掌握,扩展的文件锁不要求掌握
  • 锁的标志位不要求掌握
  • 锁的系统调用要求掌握
read/write函数
  • 从一个文件描述符进行读取
  • 1
    2
    3
    #include <unistd.h>
    ssize_t read(int fd, void *buf, size_t count);
    (返回值: 读到的字节数,若已到文件尾为0,若出错为-1)
    1
    2
    3
    4
    5
    6
    7

    - 写到一个文件描述符

    ```c
    #include <unistd.h>
    ssize_t write(int fd, const void *buf, size_t count);
    (返回值: 若成功为已写的字节数,若出错为-1)
lseek 函数

用于重新计算读写的偏移量

用法:

1
2
3
4
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fildes, off_t offset, int whence);
(Return: the resulting offset location if success; -1 if failure)

关于whence参数

SEEK_SET: the offset is set to “offset” bytes,这个表示把文件指针放在你设置的偏移量的位置。
SEEK_CUR: the offset is set to its current location plus “offset” bytes,这个表示文件指针的位置设置在当前位置加上你设置的offset值的位置。
SEEK_END: the offset is set to the size of the file plus “offset” bytes,这个表示文件指针的位置是文件末尾的位置加上设置的offset值的和的位置。

dup/dup2函数

复制一个文件描述符,返回一个新的文件描述符,指向同一个文件

1
2
3
4
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
(Return: the new file descriptor if success; -1 if failure)
fcntl函数

对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性。

1
2
3
4
5
6
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
(返回值: 若成功则依赖于cmd,若出错为-1)

cmd参数

  • F_DUPFD:和dup函数相同
  • F_GETFD/F_SETFD:get/set文件的close-on-exec标志
  • F_GETFL/F_SETFL:get/set文件打开方式的标识
  • F_GETOWN/F_SETOWN:get/set文件的IO能力标识
  • F_GETLK/F_SETLK/F_SETLKW:获得,设置文件锁(封锁,解封),第二个在设置时如果失败会导致直接结束并返回,第三个是会阻塞自身等待文件解锁
ioctl函数

控制设备的函数

1
2
#include <sys/ioctl.h>
int ioctl(int d, int request, ...);
标准I/O库

文件锁

当几个进程同时操作文件时起到作用

  • 记录锁

  • 劝告锁

    检查,加锁由应用程序自己控制

  • 强制锁

    检查,加锁由内核控制

    影响open,read,write等函数

  • 共享锁

  • 排他锁

fcntl设置锁

上文系统调用中的fcntl已经有cmd参数的详细介绍,这里不再做赘述,主要讲一下struct flock的使用

1
2
3
4
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, struct flock *lock);
(返回值: 若成功则依赖于cmd,若出错为-1)
1
2
3
4
5
6
7
8
9
struct flock{
...
short l_type; /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock (F_GETLK only) */
...
}

Chapter 4

内核概念

操作系统是一系列程序的集合,其中最重要的部分构成了内核。

单内核/微内核

  • 单内核是一个很大的进程,内部可以分为若干模块,运行时是一个独立的二进制文件,模块间通讯通过直接调用函数实现
  • 微内核中大部分内核作为独立的进程在特权下运行,通过消息传递进行通讯

Linux内核的能力

  • 内存管理,文件系统,进程管理,多线程支持,抢占式,多处理支持

Linux内核区别于其他UNIX商业内核的优点

  • 单内核,模块支持
  • 免费/开源
  • 支持多种CPU,硬件支持能力非常强大
  • Linux开发者都是非常出色的程序员
  • 通过学习Linux内核的源码可以了解现代操作系统的实现原理

驱动

常见的驱动代码集成在内核源码中

也有第三方开发的驱动可以单独编译为模块.ko文件

编译需要内核头文件的支持

加载模块

  • 底层命令
    • insmod
    • rmmod
  • 高层命令
    • modprobe
    • modprobe -r

模块依赖

一个模块A应用另一个模块B所导出的符号我们就说模块B被模块A引用,如果要装载A,则必须要线装载B,否则,模块B所导出的那些符号的应用就不可能被链接到A中,这就叫模块的依赖

系统可以实现自动按需加载以及自动按需卸载

moddep,ismod,modinfo

模块之间的通讯

模块是为了完成某种特定任务而设计的,功能单一,为了丰富系统的功能,所以模块之间常常需要通信,可以共享变量,数据结构,调用对方的功能函数

模块的命令

  • insmod [module parameters]
    • 用于装载模块
    • 注意:只有超级用户能够使用这个命令
  • rmmod
    • 卸载模块
  • lsmod
    • 查看内核中已经装载的所有模块
    • 这个命令和cat /proc/modules等价
  • modprobe [-r] \
    • 装载一个模块以及这个模块依赖的所有模块

Linux内核模块和应用程序的区别

C语言程序 Linux内核模块
运行 用户空间 内核空间
入口 main module_init()指定
出口 module_exit()指定
运行 直接运行 insmod
调试 gdb kdbug,kdb,kgdb

内核模块不能使用C库进行开发

没有内存保护机制

小内核栈

需要考虑并发

一个简单的内核模块的代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello world\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye world\n");
}
module_init(hello_init);
module_exit(hello_exit);

内核模块解释

内核程序和用户态程序区别

内核态和用户态是处理器的两个状态

当发生中断或者系统调用时暂停正在运行的进程,把处理器状态从用户态转到内核态,执行操作系统服务例程。这就是一次状态转换,此时进程仍然在自己的上下文中执行,仅处理器状态发生了变化,内核在被中断的进程的上下文中进行处理。

在大多数情况下处理器状态的转换不会影响到上下文。

读懂内核程序代码

实在过于硬核,代码长达5页pdf,可以自行学习,在最后一个PDF的第25页开始

字符型驱动程序概念

Linux系统将设备分为三种类型:字符设备、块设备和网络接口设备

文件操作是字符设备驱动的对上接口

两个基本的结构体分别是:file结构体和inode结构体

驱动程序的初始化加载过程是

  • 申请设备号
  • 定义文件操作结构体file_operations
  • 创建并初始化定义结构体cdev
  • 将cdev注册到系统,并和对应的设备号绑定
  • 在/dev文件系统中用mknod创建设备文件,并将该文件绑定到设备号上

每个字符设备或者是块设备都有主设备号和次设备号,主设备号表示一个特定的驱动程序,次设备号用来表示使用该驱动程序的各设备。

注:申请设备号之后的内容过于繁琐,个人认为不考,可以从参考pdf