0x80 0x21 软中断

系统调用是一个软中断,中断号是0x80,它是上层应用程序与Linux系统内核进行交互通信的唯一接口。



这个中断的设置在kernel/sched.c中441行函数中
oid sched_init(void)

{

int i;

struct desc_struct * p;



if (sizeof(struct sigaction) != 16)  
panic("Struct sigaction MUST be 16 bytes");
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
p = gdt+2+FIRST_TSS_ENTRY;
for(i=1;i<NR_TASKS;i++) {
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
} /* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
ltr(0);
lldt(0);
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt);
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call); } <!-- more --> 最后一句就将0x80中断与system_call(系统调用)联系起来。 通过int 0x80,就可使用内核资源。


通常应用程序都是使用具有标准接口定义的C函数库间接的使用内核的系统调用,即应用程序调用C函数库中的函数,C函数库中再通过int 0x80进行系统调用。 所以,系统调用过程是这样的:
应用程序调用libc中的函数->libc中的函数引用系统调用宏->系统调用宏中使用int 0x80完成系统调用并返回



其中sys_call_table的类型是fn_ptr类型,其中sys_call_table[0]元素为sys_setup,它的类型是fn_ptr类型,它实际上是函数sys_setup的



入口地址。



它的定义如下:



typedef int (*fn_ptr) (); // 定义函数指针类型。



下面的实例代码有助于理解函数指针:



进程控制
每个进程都有一个非负整数表示的唯一进程ID
虽然唯一,不过可以复用,但不是立刻复用,而是使用延迟算法,防止将新进程误认为是使用同一ID的某个已经终止的先前进程.



特殊进程:



ID为0的是调度进程,该进程是内核的一部分,不执行任何磁盘上的程序
ID为1的是Init进程,init通常读取与系统有关的初始化文件(/etc/rc*文件、/etc/inittab文件、/etc/init.d/中的文件)
ID为2的是页守护进程,负责支持虚拟存储器系统的分页操作



除了进程ID,每个进程还有一些其他标识符:



#include
pid_t getpid(void);//返回调用进程的进程ID
pid_t getppid(void);//调用进程的父进程ID
uid_t getuid(void);//调用进程的实际用户ID
uid_geteuid(void);//调用进程的有效用户ID
gid_t getgid(void);//调用进程的实际组ID
gid_t getegid(void);//调用进程的有效组ID
fork调用
#include
pid_t fork(void);
//子进程返回0
//父进程返回子进程ID
//出错返回-1
fork函数被调用一次将返回两次,在子进程中返回0,在父进程中返回子进程的ID。
子进程获得父进程的数据空间、堆、栈副本



#include
#include
#include
#include
#include <sys/time.h>
#include



int globvar=6;//全局变量
char buf[]=”hello world\r\n”;
int main(void )
{
int var;//栈上变量
pid_t pid;
var = 88;
int *ptr=(int *)malloc(sizeof(int));
*ptr=2;



if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1)
{
printf("write error\r\n");
return -1;
}
printf("before fork\r\n");

if((pid=fork())<0)
{
printf("fork error");
return -1;
}
else if(pid==0)//child
{
++*ptr;
++var;
++globvar;
}
else//parent
{
sleep(2);
}

printf("pid = %ld, globvar = %d, &var = %ld , var = %d , *ptr = %d , ptr=%ld\r\n",(long)getpid(),globvar,(long)&var, var ,*ptr,(long)ptr);
free(ptr);
return 0; } 直接执行:


./fork
打印结果:



hello world
before fork
pid = 16722, globvar = 7, &var = 140722924809824 , var = 89 , *ptr = 3 , ptr=22728720
pid = 16721, globvar = 6, &var = 140722924809824 , var = 88 , *ptr = 2 , ptr=22728720
我们看到地址都是一样的,但是值不一样,说明子进程中发生了拷贝,但是为什么地址一样呢?



这里就涉及到物理地址和逻辑地址(或称虚拟地址)的概念。



操作系统讲逻辑地址转化成物理地址的过程叫做地址重定位。



分为:



静态重定位–在程序装入主存时已经完成了逻辑地址到物理地址和变换,在程序执行期间不会再发生改变。
动态重定位–程序执行期间完成,其实现依赖于硬件地址变换机构,如基址寄存器。



逻辑地址:
在计算机体系结构中是指应用程序角度看到的内存单元(memory cell)、存储单元(storage element)、网络主机(network host)的地址。
逻辑地址往往不同于物理地址(physical address),通过地址翻译器(address translator)或映射函数可以把逻辑地址转化为物理地址。



物理地址:
物理地址(英语:physical address),也叫实地址(real address)、二进制地址(binary address),
它是在地址总线上,以电子形式存在的,使得数据总线可以访问主存的某个特定存储单元的内存地址。
在和虚拟内存的计算机中,物理地址这个术语多用于区分虚拟地址。尤其是在使用内存管理单元(MMU)转换内存地址的计算机中,
虚拟和物理地址分别指在经MMU转换之前和之后的地址。



网上看到一篇很好的介绍物理地址、逻辑地址的博客:
http://www.cppblog.com/fwxjj/archive/2009/05/27/85897.html



了解了物理地址和逻辑地址,再看上述问题:



在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,
但其对应的物理空间是同一个。



当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,
如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。
而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。



fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。



fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。



每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)而值不同没什么奇怪。



具体过程是这样的:
fork子进程完全复制父进程的栈空间,也复制了页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,



但是会把父子共享的页面标记为“只读”类似mmap的private的方式),如果父子进程一直对这个页面是同一个页面



,直到其中任何一个进程要对共享的页面“写操作”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。



而把原来的只读页面标记为“可写”,留给另外一个进程使用这就是所谓的“写时复制”



参考:http://www.cnblogs.com/zhangchaoyang/articles/2317420.html



上述代码如果执行:



./fork > see.txt
则打开see.txt文件,输出为:



hello world
before fork
pid = 14001, globvar = 7, &var = 140725591119472 , var = 89 , *ptr = 3
before fork
pid = 14000, globvar = 6, &var = 140725591119472 , var = 88 , *ptr = 2
多打印了一个before fork这是什么原因?



首先,stdin和stdout都是行缓冲,也就是遇到\n将flush缓冲区,因此在之前直接执行./fork时只打印一个before fork



因为缓冲区刷新了。



但是当重定向文件时,变成了标准输出变成全缓冲,因此,子进程就复制了缓冲区。



用一句话解释:



面向终端的缓冲时行缓冲,当并不指向交互式设备时,他们是全缓冲



因此,子进程复制了父进程的缓冲区



—————————————————–软中断————————————————————————————–
在Linux 的汇编语言中(AT&T,x86汇编两种语法的一种),int 指令被称为软中断指令 ,可以用此指令去故意产生一个异常 ,(异常与中断有点类似, 过程相当于CPU从用户模式切换到特权模式, 然后跳转到内核代码中执行异常处理的代码。)



在int 指令中的立即数 0x80 是一个参数 ,可以用于执行系统调用。
而int3则主要用于设置断点(对程序进行调试) 



——————————————————软中断实现过程—————————————————————————–



原因:通过限定应用程序和内核程序使用不同的内存分配函数,将用户空间程序限制在0-3G空间,将内核程序限制在3G~4G空间,这样就实现了用户空间和内核空间的隔离;



交互方式:通过 软中断(int $0x80 )实现用户空间与内核空间的交互。



 系统调用是一个软中断,中断号是0x80,它是上层应用程序与Linux系统内核进行交互通信的唯一接口。通过int 0x80,就可使用内核资源。不过,通常应用程序都是使用具有标准接口定义的C函数库间接的使用内核的系统调用,即应用程序调用C函数库中的函数,C函数 库中再通过int 0x80进行系统调用。



  所以,系统调用过程是这样的:
    应用程序调用libc中的函数->libc中的函数引用系统调用宏->系统调用宏中使用int 0x80完成系统调用并返回



在linux中 , int $0x80 这种异常被称作系统调用(systerm call) ,内核中也提供了许多系统服务供用户程序使用 ,但这些系统服务不能像库函数一样用的为所欲为, 原因在于 :执行用户程序的时候CPU处于用户模式 ,经由异常处理的 程序进入到内核 ,用户只能通过寄存器传递几个参数 ,之后就要按照内核设计好的代码路线走 ,调用结束后 ,CPU在切回到用户模式, 继续执行下一条 int $0x80 的下一条指令 。这个方式该不了 (除非你改系统代码)。。。这也是保证系统服务的安全。



因为中断的处理过程中,同种类型的中断是被禁止的。并且中断处理应该越短越好,这样才能减少丢失的中断。所以linux将中断处理分为两部分。关键紧急的事情在中断上下文处理,不紧急或者花费时间较多的事情在所谓的下半部分中执行。中断的下半部分是一种内核机制,它运行的时候允许中断的产生,可以分为软中断与工作队列。软中断又包含:tasklet 与内核定时器。软中断是一种特殊的内核控制路径,它不属于任何进程,所以不能被抢占,不可以睡眠。而工作队列是一种内核线程,有工作的时候醒来工作,没事的时候处于睡眠状态。



一. 软中断



软中断是静态分配的,这也就意味这如果想定义新的软中断就必须重新编译内核。软中断可以并发的运行在多处理器上,即使同一个软中断也是这样。所以,软中断函数必须是可重入函数,而且需要使用自选锁来保护数据结构。linux使用有限数量的软中断,一般而言不需要定义新的软中断,因为tasklet就足够用了。而且tasklet不必是可重入的。目前linux使用以下几种软中断:搞优先级的tasklet,内核定时器,网卡接收软中断,网卡发送软中断,SCSI命令处理软中断,低优先级的软中断。



软中断使用的数据结构是softirq_vec数组,该数组包含softirq_action的32个元素,也就意味着linux总共可以有32个软中断。softirq_action结构有两个域:一个是action指针一个是data。与软中断相关的进程描述符的字段是thread_info里面的preempt_count。它包含:抢占计数器,软中断计数器,中断计数器。内核用来了解进程运行的环境。in_interupt函数读取这个字段,只要有一个计数器不为0,那么就返回1.说明进程运行在中断上下文,这时是禁止内核抢占的。



内核处理软中断需要三步:(1)初始化软中断,初始化softirq_vec数组,初始化软中断处理函数以及所使用的数据结构。(2)激活软中断。raise_softirq激活软中断。(3)周期性检查,并处理软中断。内核在do_IRQ完成中断处理调用irq_exit的时候会检查未处理的软中断。或者在ksoftirqd/n线程被唤醒时检查软中断。



如果确实有未处理的软中,那么内核调用do_softirq函数处理软中断。这个函数依次处理激活的软中断,执行注册的函数。主要完成以下几步:



(1)调用in_interrupt,如果返回1说明处于中断上下文。函数返回。



(2)调用__do_softirq函数



这个函数是处理软中断的基本函数,它主要吧软中断的位掩码复制到局部变量pending中,清除本地CPU的软中断位图。根据pending的每一位执行对应的软中断处理函数。注意在软中断处理过程中可能产生新的软中断。所以,这个函数会循环执行,直到没有新的软中断。



在软中断处理过程中,__do_softirq是循环执行的,如果有软中断不停的产生新的软中断,那么会带来一个问题就是__do_softirq会一直占用CPU,用户进程没有机会运行。但是如果__do_softirq不循环检查新的软中断,那么软中断就会延迟很久执行。为了解决这个问题,linux采用ksoftirqd内核线程的办法。__do_softirq会循环有限次来处理新的软中断,如果还有新的软中断就会唤醒ksoftirqd内核线程来执行,这个内核线程的优先级比较低,所以即使有大量的软中断需要处理,对用户进程的影响也比较小。



二. tasklet



tasklet也是一种软中断,但是tasklet比软中断有着更好的并发特性。是io驱动程序首选的可延迟函数方法。tasklet有如下的特性:



(1)tasklet可以在内核运行的时候定义



(2)相同类型的tasklet不能在用一个CPU上并发运行,也不能在不同的CPU上并发运行



(1)不同类型的tasklet不能在同一个CPU上并发运行,但是可以在不同的CPU上并发运行。所以如果不同的tasklet访问相同的数据结构,需要加一定的锁保护



三. 工作队列



工作队列由内核线程来执行。主要的数据结构是workqueue_struct结构。这个结构是是一个cpu_workqueue_struct的数组。cpu_workqueue_struct结构是工作哦队列的基本结构。主要有此工作队列工作的链表,还有内核线程的进程描述符的指针。工作队列的工作是由work_struct结构组成,这个结构有需要执行的函数,以及传输的数据等字段。建立工作队列是一项非常耗时的操作,因为它会建立一个内核线程。所以linux默认建立了一个工作队列来供使用。每一个CPU一个这样的工作队列。



当进程执行系统调用时,先调用系统调用库中定义某个函数,该函数通常被展开成前面提到的_syscallN的形式通过INT 0x80来陷入核心,其参数也将被通过寄存器传往核心。
在这一部分,我们将介绍INT 0x80的处理函数system_call。
思考一下就会发现,在调用前和调用后执行态完全不相同:前者是在用户栈上执行用户态程序,后者在核心栈上执行核心态代码。那么,为了保证在核心内部执行完系统调用后能够返回调用点继续执行用户代码,必须在进入核心态时保存时往核心中压入一个上下文层;在从核心返回时会弹出一个上下文层,这样用户进程就可以继续运行。
那么,这些上下文信息是怎样被保存的,被保存的又是那些上下文信息呢?这里仍以x86为例说明。
在执行INT指令时,实际完成了以下几条操作:
(1) 由于INT指令发生了不同优先级之间的控制转移,所以首先从TSS(任务状态段)中获取高优先级的核心堆栈信息(SS和ESP);
(2) 把低优先级堆栈信息(SS和ESP)保留到高优先级堆栈(即核心栈)中;
(3) 把EFLAGS,外层CS,EIP推入高优先级堆栈(核心栈)中。
(4) 通过IDT加载CS,EIP(控制转移至中断处理函数)
然后就进入了中断0x80的处理函数system_call了,在该函数中首先使用了一个宏SAVE_ALL



在这里所做的所有工作是:
Ⅰ.保存EAX寄存器,因为在SAVE_ALL中保存的EAX寄存器会被调用的返回值所覆盖;
Ⅱ.调用SAVE_ALL保存寄存器上下文;
Ⅲ.判断当前调用是否是合法系统调用(EAX是系统调用号,它应该小于NR_syscalls);
Ⅳ.如果设置了PF_TRACESYS标志,则跳转到syscall_trace,在那里将会把当前程挂起并向其父进程发送SIGTRAP,这主要是为了设置调试断点而设计的;
Ⅴ.如果没有设置PF_TRACESYS标志,则跳转到该系统调用的处理函数入口。这里是以EAX(即前面提到的系统调用号)作为偏移,在系统调用表sys_call_table中查找处理函数入口地址,并跳转到该入口地址。



软中断:



  1. 编程异常通常叫做软中断

  2. 软中断是通讯进程之间用来模拟硬中断的 一种信号通讯方式。

  3. 中断源发中断请求或软中断信号后,CPU或接收进程在适当的时机自动进行中断处理或完成软中断信号对应的功能

  4. 软中断是软件实现的中断,也就是程序运行时其他程序对它的中断;而硬中断是硬件实现的中断,是程序运行时设备对它的中断。



硬中断:



  1. 硬中断是由外部事件引起的因此具有随机性和突发性;软中断是执行中断指令产生的,无面外部施加中断请求信号,因此中断的发生不是随机的而是由程序安排好的。

  2. 硬中断的中断响应周期,CPU需要发中断回合信号(NMI不需要),软中断的中断响应周期,CPU不需发中断回合信号。

  3. 硬中断的中断号是由中断控制器提供的(NMI硬中断中断号系统指定为02H);软中断的中断号由指令直接给出,无需使用中断控制器。

  4. 硬中断是可屏蔽的(NMI硬中断不可屏蔽),软中断不可屏蔽。



区别:



  1. 软中断发生的时间是由程序控制的,而硬中断发生的时间是随机的

  2. 软中断是由程序调用发生的,而硬中断是由外设引发的

  3. 硬件中断处理程序要确保它能快速地完成它的任务,这样程序执行时才不会等待较长时间



Category linux