1. 进程同步和P、V操作

若有三个并发进程互斥,如果利用信号灯方式控制其同步关系,那么三个进程的同步过程如下图:
first

2. Linux系统功能调用的实现机制

2.1 Linux系统调用的进入

Linux系统的软中断指令是汇编语言指令int 0x80,执行该指令会发生中断,处理机的状态由用户态自陷到内核态(在更新的芯片中添加了sysenter汇编语言指令等)。int 0x80指令使用的异常向量号是128(即16进制的80),该异常向量包含了内核系统调用程序的入口地址。在内核初始化时,已将系统调用处理程序的入口地址送入向量128的中断描述符表表项中,设置段地址为内核开始的地方,段内偏移则指向系统调用处理程序system_call()。当应用程序请求操作系统服务,发出int 0x80指令时,就会从用户态自陷到内核态,并从system_call()开始执行系统调用处理程序。当系统调用处理完毕后,通过iret汇编语言指令返回到用户态。

2.2 系统调用号和系统调用表

2.2.1 系统调用号

在Linux中,每个系统调用被赋予一个唯一的系统调用号。用户空间的进程通过系统调用号指明要执行的具体系统调用
系统调用号定义在include/asm-i386/unistd.h头文件中,这个头文件定义了该系统所有的系统调用号。格式如下:

#define _NR_restart_syscall   0
#define _NR_exit              1
#define _NR_fork              2
#define _NR_read              3
#define _NR_write             4
#define _NR_open              5
......
......
#define _NR_mq_getsetattr     282

该文件中的每一行表示为#define_NR_name NNN,其中,”NR“是一种约定,name为系统调用的名称,而NNN则是该系统调用对应的号码。该文件的最后,还定义了几个与系统调用相关的关键的宏。

2.2.2 系统调用表

系统调用表记录了内核中所有已注册的系统调用,它是系统调用的跳转表,实际上是一个函数指针数组,表中依次保存所有系统调用的函数指针,以方便总的系统调用处理函数system_call进行索引。Linux系统调用表保存在arch/i386/kernel/下的entry.S中,其形式如下:

ENTRY(sys_call_table)
.long sys_restart_syscall       /* 0 */
.long sys_exit                  /* 1 */
.long sys_fork                  /* 2 */
.long sys_read                  /* 3 */
.long sys_write                 /* 4 */
.long sys_open                  /* 5 */
......
......
.long sys_mq_getsetattr         /* 6 */    

文件中有许多.long SYMBOL_NAME(sys_ni_syscall)的结构,”.”代表当前地址,”sys_call_table”代表数组首地址

2.3 系统调用处理程序

系统调用处理程序是system_call(),该函数的主要工作如下。

  • 通过宏SAVE_ALL保护异常处理程序中要用到的所有寄存器到寄存器到内核堆栈中,其中,指令地址和处理机状态已在中断进入过程中保护(eflags、cs、eip、ss、esp寄存器除外)
  • 进行系统调用正确性检查,如对用户态进程传递来的系统调用号进行有效性检查,若该号大于或等于系统调用表的表项数,系统调用处理程序就终止
  • 根据eax中所包含的系统调用号,调用其对应的服务例程
  • 系统服务例程结束时,通过宏RESTORE_ALL恢复寄存器,最后通过iret指令返回

3. 增加一个新的系统调用方法

如果需要扩充Linux系统的功能,增加一个新的系统调用需要做的工作包括以下几个方面:

  • 描述新增加功能的服务例程
  • 增加一个新的系统调用号
  • 在系统调用表中登记新的系统调用号以及对应的服务例程
  • 新增加的服务例程要被Linux系统接受,必须重新编译内核,生成新的包含新增服务例程的内核

当要增加一个新的系统调用时,首先要确定新增的服务例程的名字,因为确定了这个名字后,在系统调用中的几个相关的名字也就确定了。如:

  • 新增加的系统调用名为mysyscall
  • 系统调用的编号名字为_NR_mysyscall
  • 内核中系统调用服务例程的名字为sys_mysyscall

3.1 添加新的服务例程

编写新增的服务例程加到内核中去,即在/usr/src/linux/kernel/sys.c文件中增加一个新的函数,该函数的名字是sys_mysyscall。函数体内是新增加的功能描述,在Linux系统增加一个新的系统调用时,首先要保证整个控制过程正确,所以在开始调试时,新增功能尽量简单,如下例,其功能是返回一个整形值。

asmlinkage int sys_mycall(int number) {
    return number;
}

3.2 增加新的系统调用号

定义系统调用号,在文件include/asm-i386/unistd.h中添加一项
#define _NR_mysyscall XX
XX 为增加的系统调用号,此数字选一未用值,一般在已定义的系统调用号的最后增加一项,如下所示:

#define _NR_restart_syscall    0
#define _NR_exit                      1
#define _NR_fork                     2
#define _NR_read                    3
#define _NR_write                   4
#define _NR_open                   5
......
......
#define _NR_mq_getsetattr    282
#define _NR_mysyscall            283

3.3 修改系统调用表

在文件/arch/i386/kernel/entry.S中的系统调用表 sys_call_table中添加新增的系统调用,sys_call_table数组包含指向内核中每个系统调用的指针,这样就在数组中增加了新的内核函数的指针。在清单最后添加一行。如下所示:

ENTRY(sys_call_table)
.long sys_restart_syscall       /* 0 */
.long sys_exit                         /* 1 */
.long sys_fork                        /* 2 */
.long sys_read                       /* 3 */
.long sys_write                      /* 4 */
.long sys_open                      /* 5 */
......
......
.long sys_mq_getsetattr       /* 6 */
.long sys_mysyscall               /* 7 */

3.4 重新编译内核并启动新内核

为使新的系统调用生效,需要重建Linux的内核。
对于不同的计算机配置、Linux不同的版本等,重新编译内核命令可能会有所不同。