1. 什么是系统调用
操作系统为用户态运行的进程与硬件设备(如 CPU、磁盘、打印机等等)进行交互提供了一组接口。
在应用程序和硬件之间设置这样一个接口层具有很多优点,
- 首先,这使得编程更加容易,把用户从学习硬件设备的低级编程特性中解放出来。
- 其次,极大地提高了系统的安全性,内核在要满足某个请求之前就可以在接口级检查这种请求的正确性。
- 最后,更重要的是,这些接口使得程序更具有可移植性,因为只要不同操作系统所提供的一组接口相同,那么在这些操作系统之上就可以正确地编译和执行相同的程序。
这组接口就是所谓的“系统调用”。
1.1 fork() 系统调用
在 Linux 系统中,如何创建一个进程,就是通过调用系统调用 fork(),fork() 创建一个新进程,fork() 本身就是分叉的意思,也就是当执行 fork() 以后,一个新的进程就诞生了,也就是父子进程都存在了,执行流就一分为二,fork() 给父进程返回子进程的 pid,给子进程返回 0, 如图所示:
-
fork 系统调用头文件:<unistd.h>;
-
fork 系统调用的原型:pid_t fork();
-
fork 系统调用的返回值:pid_t 是进程描述符类型,本质就是一个 int。如果 fork() 函数执行失败,返回一个负数(<0);如果 fork() 调用执行成功,返回两个值:0 和所创建子进程的 ID。
-
fork 系统调用的功能:以当前进程作为父进程创建出一个新的子进程,并且将父进程的所有资源拷贝给子进程,这样子进程作为父进程的一个副本存在。父子进程几乎是完全相同的,但子进程与父进程的 ID 不同。
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
int main(void)
{
pid_t pid; // 定义一个pid 类型的变量, 存放进程号;
printf("Before fork ... \n");
/* 调用fork()系统调用, 创建一个子进程 */
switch(pid = fork()){
/* fork() 失败,返回一个负值; */
case -1:
printf("fork call fail \n");
fflush(stdout);
exit(1); // 调用失败,退出;
// 子进程调用fork(), 返回0;
case 0:
printf("I am child. \n");
printf("The pid of child is:%d \n", getpid());
printf("the pid of child's parent is: %d \n",getppid());
printf("child exiting ... \n");
exit(0); // 子进程退出;
// 父进程调用fork(), 会返回子进程的进程号;
default:
printf("I am father. \n");
printf("the pid of parent is: %d \n", getpid());
printf("the pid of parent's child is %d \n", pid);
}
printf("After fork, program exiting... \n");
exit(0); // 父进程退出;
}
进行编译, 链接, 运行,并反汇编
1. gcc -S hello.c -o hello.s // 编译;
2. // 汇编 : gcc -c hello.s -o hello.o;
3. // 链接 : gcc hello.c -o hello
4. // 装载可执行文件,并执行 ./hello;
./lab_fork
Before fork ...
I am father.
the pid of parent is: 9718
the pid of parent's child is 9719
After fork, program exiting...
I am child.
The pid of child is:9719
the pid of child's parent is: 9718
child exiting ...
2.在执行 fork() 时系统进入到什么态? Fork()调用,创建一个子进程
- fork() 的执行流,分析 getpid() 的执行流。
getpid函数头文件:#include <unistd.h>;
getpid函数原型:int _getpid(void);
getpid函数返回值:程序进入到子进程,并获得子进程的进程号,通过getpid返回当前进程识别码
getpid函数说明:getpid函数用来取得当前进程的进程识别码,
程序利用getpid创建临时文件,从而避免临时文件出现进程拥塞等问题
4.fork() 的执行对你有什么启发?
Fork函数通过系统调用创建一个子进程,
而子进程可以做父进程完全相同的事,
但是传入的ID或者变量不同,
那么两个进程也可以做不同的事,这相当于父进程克隆了自己;
将汇编生成的目标文件, 反汇编成 inel 的汇编格式
objdump -d hello.o -Mintel
~/Documents/course_2610/lab9_fork$ objdump -d lab9.o
lab9.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # f <main+0xf>
f: e8 00 00 00 00 callq 14 <main+0x14>
14: e8 00 00 00 00 callq 19 <main+0x19>
19: 89 45 fc mov %eax,-0x4(%rbp)
1c: 8b 45 fc mov -0x4(%rbp),%eax
1f: 83 f8 ff cmp $0xffffffff,%eax
22: 74 06 je 2a <main+0x2a>
24: 85 c0 test %eax,%eax
26: 74 27 je 4f <main+0x4f>
28: eb 77 jmp a1 <main+0xa1>
2a: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 31 <main+0x31>
31: e8 00 00 00 00 callq 36 <main+0x36>
2. 添加系统调用的方式
添加系统调用的方式有两种,
一种是重新编译内核,
一种是编写内核模块,请参看Linux 添加系统调用的两种方法;
https://www.cnblogs.com/wangzahngjun/p/4992045.html;
3. 系统调用日志收集系统
系统调用是用户程序与系统打交道的唯一入口,因此对系统调用的安全调用直接关系到系统的安全,但对系统管理员来说,某些操作却会给系统管理带来麻烦,比如一个用户恶意地不断调用 fork() 将导致系统负载增加,所以如果我们能收集到是谁调用了一些有危险的系统调用,以及调用系统调用的时间和其他信息,将有助于系统管理员进行事后追踪,从而提高系统的安全性。
本实例收集 Linux 系统运行时系统调用被执行的信息,也就是实时获取系统调用日志,这些日志信息将以可读的形式实时地返回到用户空间,以便做为系统管理或者系统安全分析时的参考数据。
本实例需要完成以下几个基本功能:
第一:记录系统调用日志,将其写入缓冲区(内核中),以便用户读取;
第二:建立新的系统调用,以便将内核缓冲中的系统调用日志返回到用户空间。
第三:循环利用系统调用,以便能动态实时返回系统调用的日志。
系统调用的实现需要调用内核中的函数,因此,内核版本不同,其内核函数名可能稍有差异,以下实验使用的内核版本为 5.0 平台。内核源代码的默认目录为 /usr/src/linux。
请在学堂在线观看视频,并动手实践。
动手实践-添加系统调用(系统调用日志收集系统
Linux 内核 3.x 的版本,可参看:
Linux 系统调用日志收集程序_x86_64 环境 3.14 版本内核
提示:此实验要重新编译内核,需要在本地完成。