全部博文(73)
分类: LINUX
2009-04-23 16:42:18
Linux系统调用分析
计算机962班 周从余
一. 与系统调用有关的一些基本知识
1.系统调用的定义
在OS的核心中都设置了一组用于实现各种系统共能的子程序,并将它们提供
给用户程序调用.每当用户在程序中需要OS提供某种服务时,便可利用一条系统
调用命令,去调用所需的系统过程.所以说系统调用在本质上是一种过程调用.系统
调用是进程和操作系统之间的接口,这些调用一般就是一些汇编指令集,在Linux
系统中这些调用是用C语言和汇编编写的。用户只有通过这些系统调用才能使
用操作系统提供的一些功能.
2.系统调用与过程调用的区别
过程调用调用的是用户程序,它运行在用户态;其被调用过程是系统过程,运行在系统态下.
系统调用是通过软中断机制进入OS核心,经过核心分析后,才能转向响应的命令
处理程序.系统调用返回时通常需要重新调度.系统调用允许嵌套调用.
3.中断与异常
中断(interrupt)是由外部事件的,可以随时随地发生(包括在执行程序时)所以
用来响应硬件信号。在80386中,又把中断分为两种:
可屏蔽中断(Miscible Interrupt) MI
不可屏蔽中断(NonMaskable Interrupt) NMI
异常(exception)是响应某些系统错误引起的,也可以是响应某些可以在程序中
执行的特殊机器指令引起的. 异常也分为两种:
处理器异常,(指令内部异常 如overflow 等)
编程(调试)异常(debugger)
每一个异常或中断都有一个唯一的标识符,在linux文献中被称为向量。指令内 部
异常和NMI(不可屏蔽中断)的中断向量的范围从0—31。32-255的任何向量都 可
以用做
可屏蔽中断
编程(调试)异常
至于可屏蔽中断则取决于该系统的硬件配置。外部中断控制器(External interrupt
controler)在中断响应周期(interrtupt acknowledge cycle)把中断向量放到总线上。
中断和异常的优先级:
最高 :除调试错误以外的所有错误 最低: INTR中断。
中断指令INTO,INTn,INT3
当前指令的调试中断
下一指令的调试中断
不可屏蔽中断
4.Intel386 提供的功能
Intel386认识两种事件类:异常与中断。两者都会强制性创建一个进程或任务。
中断能在任何不可预料的时间发生,来响应硬件的信号.386能辨认两种中断来源
可屏蔽中断和不可屏蔽中断.并能辨认两种异常来源:处理器检测异常和程序异常
每一个中断和异常都有一个号码,都对应着一个相应的矢量地址,不可屏蔽中断
和处理器检测异常都已经被安排在0到31的矢量表中了,可屏蔽中断的矢量地
址由硬件决定,外部中断控制器在中断认可时钟周期时将矢量地址放到总线上。
任何在32到255范围内的矢量,都可以作为可屏蔽中断和程序异常用。以下是所
有可能的中断和异常的列表:
0 Divide error
1
Debug exception 3
NMI interrupt
4
INTO-detected overflow
5 Bound range exceeded 6 Invalid opcode
7
coprocessor not available
8 double fault
9 coprocessor segment overrun 10
invalid task state segment 11
segment not present
12 stack fault
13 general protection
14
page fault
15
reserved
16
coprocessor error
17-31 reserved
32-255 maskable interrupt |
二. Linux系统调用的流程
1.Linux系统调用的简单流程
通常,在OS的核心中都设置了一组用于实现各种系统功能的子程序(过程),并将它们提供给用户调用。每当用户在程序中需要OS提供某种服务时,变可利用一条系统调用命令,去调用系统过程。它一般运行在系统态;通过中断进入;返回时通常需要重新调度(因此不一定直接返回到调用过程)。
Linux系统调用的流程非常简单,它由0x80号中断进入系统调用入口,通过使用系统调用表保存系统调用服务函数的入口地址来实现.
Processor |
调用syscallN( ) |
调用 int $0x80 |
System_call |
调用实际服务程序 |
返回 |
2.Linux系统中断和异常的使用
Linux中,系统调用的执行是通过中断或异常的方式来进行的,他将执行相应
的机器代码指令,来产生中断或异常信号,产生中断或异常的重要效果是系统自
动将用户3模式切换为核心模式,并安排异常处理程序的执行。
Linux设置了一个可屏蔽中断int 0x80,我们用向量0x80来把控制传给kernel
这个中断向量的设置(初始化)将在下文提到,这里就不多说了。得一提的是,存在一个syscallX()宏(x是作为实际程序调用时的参数)可以方便的调用那么多的syscall.(syscallX() // usr/src/libc/syscall)每个syscallX()宏都可以扩展成为一段汇编代码,通过一个中断来初始化系统调用堆栈和调用_system_call()函数。
有关syscallX( )的介绍
0x80将控制传递给核心。0x80就是系统调用的一个矢量地址。这个中断矢量表是在系统启动时就初始化好的,以及一些矢量地址,如系统时钟。当用户系统调用时,执行如下:
每个系统调用都通过lib库体现。每一个系统调用在lib库中一般是一个宏syscallX(),X是具体某个调用的数字参数。有的系统调用更复杂,因为它们有可变的参数列表,但它们仍用一样的入口指针。
每个系统调用宏将展开成一个汇编段,用来建立调用的堆栈段,然后通过
调用中断int $0x80调用--ENTRY(system_call).
注:syscallX()宏在/usr/include/linux/unistd.h中,
以下是用_syscallX()宏定义的一些系统调用。
static inline _syscall0(int,idle)
static inline _syscall0(int,fork)
static inline _syscall2(int,clone,unsigned long,flags,char *,esp)
static inline _syscall0(int,pause)
static inline _syscall0(int,setup)
static inline _syscall0(int,sync)
static inline _syscall0(pid_t,setsid)
static inline _syscall3(int,write,int,fd,const char *,buf,off_t,count)
static inline _syscall1(int,dup,int,fd)
static inline _syscall3(int,execve,const char *,file,char **,argv,char
**,envp)
static inline _syscall3(int,open,const char *,file,int,flag,int,mode)
static inline _syscall1(int,close,int,fd)
static inline _syscall1(int,_exit,int,exitcode)
static inline _syscall3(pidt,t,waitpid,pid_t,pid,int *,wait_stat,int
options)
static inline _syscall0(int,idle)
static inline _syscall0(int,fork)
static inline _syscall2(int,clone,unsigned long,flags,char *,esp)
static inline _syscall0(int,pause)
static inline _syscall0(int,setup)
static inline _syscall0(int,sync)
static inline _syscall0(pid_t,setsid)
static inline _syscall3(int,write,int,fd,const char *,buf,off_t,count)
static inline _syscall1(int,dup,int,fd)
static inline _syscall3(int,execve,const char *,file,char **,argv,char
**,envp)
static inline _syscall3(int,open,const char *,file,int,flag,int,mode)
static inline _syscall1(int,close,int,fd)
static inline _syscall1(int,_exit,int,exitcode)
static inline _syscall3(pid_t,waitpid,pid_t,pid,int
*,wait_stat,int,options)
当int $0x80执行后,调用才传送到核心入口指针ENTRY(system_call)。
在宏_syscallX(Parameter) 中
x 表示系统调用所需的参数的数目。
Parameter 是一组参数。
SyscallX() 宏的第一个参数表明,该系统调用最后调用的同名函数的返回值的类型。
SyscallX() 宏的第二个参数表明,该系统调用的同名函数名。
后面是系统调用所需要的每个参数,
例:setuid()
syscall1(int,setuid,uid_t,uid)
该例中,
int是setuid的返回类型,
setuid是函数名。
Uid_t是参数类型,
Uid是参数 。
用做系统调用的参数类型有一个限制,他们的容量不能超过4个字节,因为在执行 int 0x80 时,所有的参数都是通过寄存器传递的,而在386体系结构中,寄存器是32位的.所以,他们的容量不能超过4个字节(32位)。使用CPU寄存器做参数传递的另一个限制是,可以传递的参数的数目,使用CPU寄存器做参数传递最多可以传递五个参数,所以,一共定义了六个不同的syscallX()宏。(从syscall0()…到 syscall5()宏)一旦syscallX()宏被调用,系统使用其调用的特定参数进行扩展,(宏展开)得到的结果是一个与系统调用同名的函数。他可以在用户的程序中被调用。当syscall()被调用后,并没有任何的系统代码被执行,直到syscall()调用了int 0x80 ,中断0x80 把调用(控制)传给核心入口地址中的_system_call(),这个入口地址对任何系统调用都是一样的。 _System_call() 负责保护所有的寄存器,并检查系统调用是否合法,如果合法那么根据从_sys_call_table中找出的偏移量,把控制权转给真正的系统。最后,当
系统调用完成后,_system_call() 还要负责调用_ret_from_sys_call()来断后。_Ret_from_sys_call()检查是否有必要重新调度,如果有的话,调用他。
3.Linux系统对系统调用矢量的初始化
为中断向量准备空间
在head.S中调用(head.S在保护模式下的核心初始化中执行)
Startup_32() //linux/boot/head.s Setup_idt() //linux/boot/head.s |
Startup_32() 调用 setup_idt来把一切都设置好。Setup_idt()函数初始化了IDT表,包括256个函数入口(每个入口4字节,共1024字节),但是,没有一个中断向量在这时被真正的设置好了,现在的IDT只是一个空架子,
Setup_idt()是在paging机制刚起作用的时候被调用的,这时,kernel 刚被移到0xC0000000的地方。
IDT表 属性字0X8E00,所有entry的中断服务程序为ignore_int()
ignore_int()只打印“unknown interrupt”。此时,idt寄存器尚未指向本表。
说白了,刚才这一段的作用就是为idt表准备空间。
Setup_idt()的代码如下:
setup_idt:
lea ignore_int,%edx
movl $(KERNEL_CS << 16),%eax
movw %dx,%ax /* selector = 0x0010 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea SYMBOL_NAME(idt),%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
ret
设置系统中断
Linux进入保护模式对一些必要的核心数据进行初始化后,转入start_kernel()模块。该模块调用trap_init函数设置IDT表各项内容(arch/i386/kernel/traps.c).
void
trap_init(void)
{
……
set_call_gate(&default_ldt,lcall7);
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3);
/* int3-5 设置成system_gate(实际为DPL设置成3的 */
set_system_gate(4,&overflow);
/* 386陷阱门)可以让任意用户访问和调用. */
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
/* 各项只能由操作系统访问的出错陷阱处理入口 */
set_trap_gate(7,&device_not_available);
/* trap_gate 实际为DPL设置成0的 */
set_trap_gate(8,&double_fault);
/* 386陷阱门 */
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
for (i=18;i<48;i++)
set_trap_gate(i,&reserved);
set_system_gate(0x80,&system_call);
/*把中断0x80的入口设置为system_call*/
……
}
其中与系统调用相关的是:set_system_gate(0x80,&system_call);
设定了0x80中断
set_system_gate的原形(在文件arch/i386/kernel/traps.c中)定义如下:
#define
set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
其中“_set_gate()”也是在该文件中定义的宏:
#define
_set_gate(gate_addr,type,dpl,addr) \
__asm__ __volatile__ (“movw
%%dx,%%ax\n\t” \
“movw %2,%%dx\n\t” \
“movl %%eax,%0\n\t” \
“movl %%edx,%
:”=m” (*((long *) (gate_addr))), \
“=m” (*(1+(long *) (gate_addr))) \
:”i” ((short) (0x8000+(dpl<<13)+(type<<8))),
\
“d” ((char *) (addr)),”a” (KERNEL_CS << 16) \
:”ax”,”dx”)
gate_addr是一个指向64位门描述符的指针.上述代码所做的实际上是把门描述符对应的32位偏移地址(offset)设置成addr(处理程序的入口地址),段选择子(selector)设置成KERNEL_CS核心段的段地址(因为各类中断和陷阱的处理程序都在核心部分),门描述符属性字中的类型字段(Type)设置成type的值,而描述符的DPL字段设置成dpl的值。
因此, set_system_gate(0x80,&system_call)用宏展开后,实际上就是把中断描述表(IDT)的第0x80项设置成为入口地址为system_call,描述符特权级DPL为3的386陷阱门。
这样,当用户程序使用INT 0x80指令时,就实现了应用程序从处于Ring 3用户地址空间向Ring 0级的操作系统核心空间的切换,并把CPU的控制权交给了操作系统,由操作系统来执行具体的各项系统调用。
3.INT
0x80 (即system_call)的具体实现
当用户调用INT 0x80而进入system_call函数后,首先检查用来存放系统调用编号的eax的值是否超出IDT表的项数NR_syscalls(NR_syscalls是在“/include/linux/sys.h”文件中定义的宏,其值为256,表示80x86微机上最多可容纳的系统调用个数)。如没有超出的话,就根据eax的值从系统调用表(sys_call_table)中得到对应的系统调用入口,并通过call 指令转入各个具体函数(sys_*)的处理过程。
系统调用表(sys_call_table) 在“/arch/i386/entry.S”中定义,该表保存了所有Linux基于Intel x86系列体系结构的计算机的166个系统调用入口地址,其中每项都被说明成 long型。下面是其中几项:
.data
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_setup)
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
………
.long SYMBOL_NAME(sys_chmod)
.long SYMBOL_NAME(sys_chown)
.long 0 /*
专门为afs_syscall保留的系统调用 */
………
.long SYMBOL_NAME(sys_mremap)
.long 0,0 /* 2 个被保留的系统调用 */
.long SYMBOL_NAME(sys_vm86)
.space (NR_syscalls-166)*4
这个sys_call_table以偏移量的方式来确定实际相应的系统调用代码,如
sys_setup,sys_fork等,这些都是实际服务函数的入口地址,当系统调用被认为是合法的时候(即调用 INT 0x80时,eax的值小于NR_syscalls),将会进入这些具体的系统服务过程,执行相应的工作,完成所要求的功能。
system_call的原代码也在Entry.S文件中,下面将对其作一分析,以清晰它的主要流程.
ENTRY(system_call)
pushl
%eax # save orig_eax
SAVE_ALL
# 调用宏“SAVE_ALL” 保存现有通用寄存器.关于该宏的具体作用以 及所牵涉的数据结构将与“RESTORE_ALL”一起在下文介绍.
movl
$-ENOSYS,EAX(%esp)
# 将返回码–ENOSYS(表示由于调用了不存在的sys_
call而出现错误)存入刚才由SAVE_ALL压进堆栈的
EAX字段,以便当下面的代码检测到这种错误时,向用 户程序反馈信息
cmpl $(NR_syscalls),%eax # 检测该系统调用是否合法,是否调用了不存在的
jae ret_from_sys_call # sys_call,如是,则出错返回.
movl
SYMBOL_NAME(sys_call_table)(,%eax,4),%eax
# 表示将eax的值乘以4个字节,找到在sys_call_table中
的实际地址,(因为在sys_call_table中,每一个项长度
为4个字节),并把相应系统调用代码的线性地址存入eax
寄存器,以便使用.
testl %eax,%eax # 检测是否调用了被保留的sys_calls(此时eax=0),
je
ret_from_sys_call # 如是,则出错返回.
movl
SYMBOL_NAME(current_set),%ebx
# 把指向当前进程PCB的指针赋与ebx寄存器
andl
$~CF_MASK,EFLAGS(%esp) # clear carry -
assume no errors
movl
%db6,%edx
movl
%edx,dbgreg6(%ebx)
# 保存当前硬件调试状态寄存器(DR6 ,当出现调试异常 事故时,处理机就把DR6置位,以表明异常事故的类型) 的信息.注:dbgreg6已在Entry.S中定义为值52,即当前 进程控制块偏移量为52字节处是用来保存硬件调试状 态寄存器的(相应的还定义了其他字段的偏移量).
testb $0x20,flags(%ebx) # 检测当前进程控制块的flags字段的PF_TRACESYS位 是否置位,即进程是否处于调试状态.
jne
call
*%eax # 正式调用所选的系统调用(返回值存放在eax寄存器中).
movl
%eax,EAX(%esp) # save the return value
jmp
ret_from_sys_call
# 系统调用返回.
ALIGN
1: call
SYMBOL_NAME(syscall_trace)
movl
ORIG_EAX(%esp),%eax
call
*SYMBOL_NAME(sys_call_table)(,%eax,4)
movl
%eax,EAX(%esp) # save the return value
movl
SYMBOL_NAME(current_set),%eax
call SYMBOL_NAME(syscall_trace
"=m" (*(1+(long *) (gate_addr))) \
:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"d" ((char *) (addr)),"a" (KERNEL_CS << 16) \
:"ax","dx")
4.总结Linux中系统调用的全部过程:
1.系统使用宏syscallX()将相应的系统调用定义为其同名函数。调用中断
int 0x80.并将参数传送到相应的寄存器中,供ENTRY(system_call)使用。
2.进入ENTRY(system_call)中,当系统调用合法时,根据索引值,在
sys_call_table中找到相应的实际服务程序入口地址,并调用它。最后返回
到syscallX()中,包括返回值。
3.进入sys_call_table指定的相应实际服务程序,在这里,完成真正的具体工作。
三.系统调用的实例分析
在了解了系统调用大致流程后,我们以一个具体的调用实例来进一步说明这个流程,以加深印象.现在,我们就以系统调用sys_open来加以说明:
1. syscallX()宏的初始化
1.
static inline _syscall3(int,open,const char *,file,int,flag,int,mode)
这是一个用来作为打开文件的系统调用宏,其原形由_syscall3宏决定:
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile ("int $0x80" : "=a" (__res) : "0"\
(__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3))); \
if (__res>=0)return (type) __res; errno=-__res; \
return -1; \
}
即为:
int open(const char* file,int flag,int mode)
{
long _res;
__asm__volatile("int$0x80":"=a"(_res):"0"(__NR_##open),
"b"((long)file)),"c"((long)flag)),
"d"((long)(mode));
if(__res>=0)return(int)__res;errno=-__res;
return -1;
}
这就是一个完整的系统调用,也就是我们编程中常常遇到的函数open。用来打开一个
文件.。在这里,open被定义为内联,主要是为了提高该系统调用的速度,并不是所
有的系统调用都使用内联的,在这个版本的Linux中,只定义了如上一些内联函数。
(见"Linux如何处理中断和异常")
在这个函数中,有三个参数,file,flag,mode.它们的值分别被存入寄存器ebx,ecx,edx
并且eax指定为将返回值传回给_res。eax在这里被初始化为__NR_open,而__NR_open
在unistd.h中被宏定义为数值5
#define __NR_open 5
这个数值就是实际系统调用程序段sys_open入口地址在sys_call_table中的索引,
这个参数被放入eax,将会作为sys_call_table的索引使用。__asm__volatile将所有
参数指定,并调用中断int 0x80,如上所述由于中断矢量表初始化时,已经将其指向矢量地址&system_call,因此,系统转入该代码入口--ENTRY(system_call)
2. 系统调用的通用入口地址:
所有的系统调用都将使用这个入口ENTRY(system_call),无论其参数到底是什么。
其最重要的参数--sys_call_table的索引都是放在寄存器eax中的,因此总能以
此找到相应的系统调用服务程序。
如上所述,首先SAVE_ALL保存现场,然后将 eax的值与$(NR_syscalls)比较
因为__NR_open的值为5,没有超过256个系统调用的限制,因此合法,继续执行下去。
Call *SYMBOL_NAME(sys_call_table)(,%eax,4)将以eax的值5,在sys_call_table
中找到sys_open的入口地址,进入该程序。
3.进入实际的系统调用服务程序:
asmlinkage int sys_open(const char * filename, int flags, int mode)
{
char * tmp;
int fd, error;
/*关闭中断,在执行该服务程序时,不许其它中断响应
lock_kernel();
/*找到一个空的文件描述符入口,然后将其空闲位置为否*/
/*如果找不到,返回负值*/
fd = get_unused_fd();
if (fd < 0)
goto out;
/*大概尝试在内存页面内查找该文件*/
tmp = getname(filename);
error = PTR_ERR(tmp);
/*如果打开文件有错,退出*/
if (IS_ERR(tmp))
- goto out_fail;
/*调用do_open打开文件*/
error = do_open(tmp, flags, mode, fd);
/*将文件名装入quicklist,放入内存中,加快下次的寻找*/
putname(tmp);
if (error)
goto out_fail;
out:
/*解锁,打开中断*/
unlock_kernel();
return fd;
out_fail:
/*将fd号的空闲标志位置位,表示已释放*/
put_unused_fd(fd);
fd = error;
goto out;
}
当程序执行完了之后,系统返回到ENTRY(system_call)中,然后按照第二节
所叙述的流程,一步步进行。直到返回到原来的syscall3()宏定义的函数
error = PTR_ERR(tmp);
/*如果打开文件有错,退出*/
if (IS_ERR(tmp))
- goto out_fail;
/*调用do_open打开文件*/
error = do_open(tmp, flags, mode, fd);
/*将文件名装入quicklist,放入内存中,加快下次的寻找*/
putname(tmp);
if (error)
goto out_fail;
out:
/*解锁,打开中断*/
unlock_kernel();
return fd;
out_fail:
/*将fd号的空闲标志位置位,表示已释放*/
put_unused_fd(fd);
fd = error;
goto out;
}
当程序执行完了之后,系统返回到ENTRY(system_call)中,然后按照第二节
所叙述的流程,一步步进行。直到返回到原来的syscall3()宏定义的函数
putname(tmp);
if (error)
goto out_fail;
out:
/*解锁,打开中断*/
unlock_kernel();
return fd;
out_fail:
/*将fd号的空闲标志位置位,表示已释放*/
put_unused_fd(fd);
fd = error;
goto out;
}
当程序执行完了之后,系统返回到ENTRY(system_call)中,然后按照第二节
所叙述的流程,一步步进行。直到返回到原来的syscall3()宏定义的函数
open中,此时,变量_res已取得返回值,如果文件打开时出错,此时_res
为负值,并将其绝对值赋给全局变量errno,作为以后处理错误信息
的参数,并且返回-1。如果文件打开成功,将返回_res.
到此为止,就是一个完整的Linux中的系统调用.
四.Linux系统调用分析的总结
系统调用是操作系统与用户程序间的主要接口,系统调用是很底层的操作,这部分
直接关系到用户对整个系统资源的使用问题。系统调用构架是否做的好,直接关系
到这个操作系统的效率和稳定性。因此,能够有一个成熟,完善的系统调用方式
是非常重要的,而它的设计也是较复杂的,它与进程的调度,中断的响应都有直接关系。在我分析的这个版本的Linux中,系统调用是很有层次性的,用户不能直接的使用系统提供的服务程序,而必须经过一个宏调用,进入系统调用入口,经过系统的许多确认和调度后,才允许被调用,这也是几乎所有操作系统所必须做到的 。
经过对系统调用部分的分析,加深了我对操作系统的理解。在没有接触过操作
系统原代码以前,总认为操作系统是很神秘的,在这之后,我才发现其实它也是用我们已经学过的C和汇编写的,只是需要巧妙的数据结构和高效率的算法.如果我们在这两方面有了一定的造诣,我想我们也能写出一个操作系统来.通过这次实验,我学到了许多编程方面的技巧,如模块的思想,C与汇编的连用等等.同时,我阅读程序的能力也有了一定的提高,这次实验受益非浅.
参考文献:
David A Rusling “The Linux Kernel”
Michael
K. Johnson,Stanley
Scalsky “How System Calls Work on Linux/i
<
<