linux軟中段和系統呼叫深入研究

加油2019發表於2020-12-27

arm軟中斷模式

arm7種模式
在這裡插入圖片描述
有中斷模式,但是並沒有軟中斷模式。那麼arm的軟中斷是什麼呢?
arm的軟中斷是arm從使用者模式切換到特權模式,也就是linux中從使用者態切換到核心態的過程。
swi命令觸發軟中斷

linux系統中,swi異常向量程式碼:

linux系統呼叫

x86 架構是硬中斷int 80,中斷號為80來實現系統呼叫的;
arm架構是使用swi命令,使arm切換為軟中斷模式,執行swi異常向量表中的異常向量。

軟中斷的異常向量

arm中異常象量表:

異常型別偏移地址(低)偏移地址(高)
復 位0x000000000xffff0000
未定義指令0x000000040xffff0004
軟中斷0x000000080xffff0008
預取指令終0x0000000c0xffff000c
資料終止0x000000100xffff0010
保留0x000000140xffff0014
中斷請求(IRQ)0x000000180xffff0018
快速中斷請求(FIQ)0x0000001c0xffff001c

軟中斷中斷向量在arn異常向量表中偏移為0x08的地址。
來看linux程式碼的處理函式

.L__vectors_start:
	W(b)	vector_rst
	W(b)	vector_und
	W(ldr)	pc, .L__vectors_start + 0x1000   /*軟中斷異常的handle*/
	W(b)	vector_pabt
	W(b)	vector_dabt
	W(b)	vector_addrexcptn
	W(b)	vector_irq
	W(b)	vector_fiq

軟中斷異常的handle,是異常向量表後0x1000的位置,即異常向量後剛好一個page(4K),我們知道linux記憶體分佈,在。
在這裡插入圖片描述
vector段是向量表段,vector在編譯連結時,連結在程式碼段之後,在linux核心初始化時後將vector段複製到核心空間的第一頁,之後機器異常入口就設定為0xffff0000(PAGE_OFFSET)。所以存在兩個vector段。程式碼段是從0xffff1000開始的。

記憶體管理中的零頁不在實體記憶體的第一塊(存在mmu的系統)

關於記憶體管理子系統中零頁的地址問題:
ZERO_PAGE,零頁。linux中,在分配記憶體時遵從一個原則,寫時分配。COW,寫時複製,在程式建立時有講到。linux分配一塊記憶體,一開始之後對映到零頁上,只有當往這塊記憶體寫內容時才會真正分配記憶體。
而這個零頁的位置,在沒有mmu的系統中才位於實體記憶體開始處,而在有mmu的系統中是當記憶體管理子系統初始化時會分配一個頁作為零頁。所以零頁不在實體記憶體開始的第一頁。

/*pgtable-nommu.h*/
#define ZERO_PAGE(vaddr)	(virt_to_page(0))
/*pgtable.h*/
extern struct page *empty_zero_page;
#define ZERO_PAGE(vaddr)	(empty_zero_page)

empty_zero_page即為ZERO_PAGE,在pagin_init中初始化

void __init paging_init(const struct machine_desc *mdesc)
{
...
	zero_page = early_alloc(PAGE_SIZE);
	bootmem_init();
	empty_zero_page = virt_to_page(zero_page);
...
}

在說會核心空間記憶體分佈:
linux連結指令碼vmlinux.lds.s中有各記憶體區的說明

SECTIONS
{
	. = PAGE_OFFSET + TEXT_OFFSET;    /*PAGE_OFFSET 是核心空間的起始地址,TEXT_OFFSET預留一個頁的空間給向量表*/

	/*刪掉一些空置的斷*/
	.text : {			/* Real text segment		*/
		_stext = .;		/* Text and read-only data	*/
		ARM_TEXT
	}

	_etext = .;			/* End of text section */

	RO_DATA(PAGE_SIZE)    /*只讀段*/

	. = ALIGN(4);
	__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) {
		__start___ex_table = .;
		ARM_MMU_KEEP(*(__ex_table))
		__stop___ex_table = .;
	}

#ifdef CONFIG_ARM_UNWIND
	ARM_UNWIND_SECTIONS
#endif

	NOTES

#ifdef CONFIG_STRICT_KERNEL_RWX
	. = ALIGN(1<<SECTION_SHIFT);
#else
	. = ALIGN(PAGE_SIZE);
#endif
	__init_begin = .;      /*init段開始標誌*/

	ARM_VECTORS    /*異常向量表*/
	INIT_TEXT_SECTION(8)
	.exit.text : {
		ARM_EXIT_KEEP(EXIT_TEXT)
	}
	.init.proc.info : {
		ARM_CPU_DISCARD(PROC_INFO)
	}
	.init.arch.info : {
		__arch_info_begin = .;
		*(.arch.info.init)
		__arch_info_end = .;
	}
	.......

vmlinux.lds.h中

#define ARM_VECTORS							\
	__vectors_start = .;						\
	.vectors 0xffff0000 : AT(__vectors_start) {			\
		*(.vectors)						\                /*vertort斷*/
	}								\
	. = __vectors_start + SIZEOF(.vectors);				\
	__vectors_end = .;						\
									\
	__stubs_start = .;						\
	.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) {		\
		*(.stubs)						\       /*stubs段,vector_swi函式*/
	}								\
	. = __stubs_start + SIZEOF(.stubs);				\
	__stubs_end = .;						\
									\
	PROVIDE(vector_fiq_offset = vector_fiq - ADDR(.vectors));

vector段後0x1000的為.stubs段開始。來看看stubs段放了什麼東西。
繼續回到entry-armv.S

	.section .stubs, "ax", %progbits       /*stubs段宣告*/
	@ This must be the first word
	.word	vector_swi                    /*存放vector_swi的地址*/

找到了軟中斷的入口,vector_swi函式。

軟中斷的處理入口vector_swi

進入vector_swi函式:

/*=============================================================================
 * SWI handler
 *-----------------------------------------------------------------------------
 */

	.align	5
ENTRY(vector_swi)
/*現場保護*/
.....
	/*
	 * Get the system call number.
	 */
/*OABI 和EABI獲取系統呼叫號會有區別,EABI,系統呼叫號在R7暫存器中,OABI則儲存在R10中*/
	 /* Pure EABI user space always put syscall number into scno (r7).
	 */

	/* saved_psr and saved_pc are now dead */

	uaccess_disable tbl
	/*獲取系統呼叫表*/
	adr	tbl, sys_call_table		@ load syscall table pointer
	....
	/*獲取當前執行的程式*/
	get_thread_info tsk
	
	/*
	 * Reload the registers that may have been corrupted on entry to
	 * the syscall assembly (by tracing or context tracking.)
	 */
 TRACE(	ldmia	sp, {r0 - r3}		)

local_restart:
	/*檢查程式flag,是否開啟系統呼叫追蹤,儲存到r10中*/
	ldr	r10, [tsk, #TI_FLAGS]		@ check for syscall tracing
	stmdb	sp!, {r4, r5}			@ push fifth and sixth args

	tst	r10, #_TIF_SYSCALL_WORK		@ are we tracing syscalls?
	bne	__sys_trace
	/*執行系統呼叫的handle,彙編巨集,在entry-header.S中*/
	/*tbl: 系統呼叫表
	* scno:系統呼叫號
	* r10:是否進行系統呼叫追蹤標誌
	* __ret_fast_syscall:系統呼叫返回使用者態,恢復現場,檢查搶佔,排程等
	*/
	invoke_syscall tbl, scno, r10, __ret_fast_syscall
...
	/*快速系統呼叫的出口*/
	b	ret_fast_syscall
#endif
ENDPROC(vector_swi)

entry-header.S中給暫存器起了別名

scno	.req	r7		@ syscall number
tbl	.req	r8		@ syscall table pointer
why	.req	r8		@ Linux syscall (!= 0)
tsk	.req	r9		@ current thread_info

關於系統呼叫號的獲取,OABI和EABI區別有所不同,見
https://www.cnblogs.com/DF11G/p/10172520.html

OABI方式系統呼叫
SWI{cond} immed_24
immed_24: 24位立即數,指定了系統呼叫號,引數用通用暫存器傳遞

MOV R0,#34
SWI 12

EABI方式系統呼叫

MOV R7,#34
SWI 0X0

系統呼叫號由R7暫存器決定。

系統呼叫號和入口

sys_call_table的定義
entry-common.S中

#define COMPAT(nr, native, compat) syscall nr, native
#ifdef CONFIG_AEABI
#include <calls-eabi.S>
#else
#include <calls-oabi.S>
#endif
#undef COMPAT
	syscall_table_end sys_call_table

以eabi為例

NATIVE(0, sys_restart_syscall)
NATIVE(1, sys_exit)
NATIVE(2, sys_fork)
NATIVE(3, sys_read)
NATIVE(4, sys_write)
NATIVE(5, sys_open)
NATIVE(6, sys_close)
NATIVE(8, sys_creat)
NATIVE(9, sys_link)
NATIVE(10, sys_unlink)
NATIVE(11, sys_execve)
NATIVE(12, sys_chdir)
NATIVE(14, sys_mknod)
NATIVE(15, sys_chmod)
NATIVE(16, sys_lchown16)
NATIVE(19, sys_lseek)
NATIVE(20, sys_getpid)

系統呼叫函式定義
include/linux/syscalls.h中定義
SYSCALL_DEFINE* 巨集

系統呼叫影響效能影響在哪?

系統呼叫是通過軟中斷陷入核心的,然後執行註冊的軟中斷處理函式。
其過程中存在如下消耗:

  1. arm模式切換,陷入核心,需要儲存上下文,所以頻繁的系統呼叫,會放大這一開銷,降低程式碼執行效率。
  2. 系統呼叫退出時可能產生系統排程,可能會很容易發生排程,得不到足夠的執行時間。
    最主要的消耗還是儲存/恢復現場的消耗。

linux陷入核心的幾種方式

在linux核心(SVC模式)中只有硬體中斷和軟中斷
硬體中斷陷入核心的arm處理器模式的切換過程

usr -> 硬體中斷(irq模式) -> svc模式; 短暫的盡力irq模式,主要的處理還是在svc模式下。
軟中斷的切換過程,就是系統呼叫:
usr->系統呼叫(SVC)

linux軟中斷

此軟中斷非彼軟中斷!!!
linux核心的軟中斷是純軟體的實現,和arm的軟中斷區分開來。和系統呼叫的軟中斷不是同一個軟中斷。
此軟中斷和tasklet類比,是一個一個的任務,是由定時執行的,由tick_timer或者其他方式喚醒執行的,是由核心執行緒[ksoftirqd%d]核心執行緒執行的。

原理

linux軟體中斷,借用硬體中斷思想,一箇中斷號對應一個handle的思路。
kernel/softirq.c中

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

每個cpu都有自己的ksoftirqd%d核心執行緒執行這些,軟中斷。
softirq和smp,可以同時在smp的多個cpu上用執行同樣的softirq處理函式。也就是可以並行處理,所以,irq執行緒和處理函式只能訪問perCPU的變數,否則存在同步的問題。

程式碼走讀

在這裡插入圖片描述
open_softirq註冊軟中斷處理函式

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

raise_softirq觸發softirq

void raise_softirq(unsigned int nr)
{
	unsigned long flags;

	local_irq_save(flags);
	raise_softirq_irqoff(nr);
	local_irq_restore(flags);
}
inline void raise_softirq_irqoff(unsigned int nr)
{
	__raise_softirq_irqoff(nr);
	if (!in_interrupt())
		wakeup_softirqd();
}

軟中斷處理執行緒ksoftirqd%d

static struct smp_hotplug_thread softirq_threads = {
	.store			= &ksoftirqd,
	.thread_should_run	= ksoftirqd_should_run,
	.thread_fn		= run_ksoftirqd,
	.thread_comm		= "ksoftirqd/%u",
};

核心執行緒函式run_ksoftirqd
呼叫__do_softirq函式實際處理。

static void run_ksoftirqd(unsigned int cpu)
{
	local_irq_disable();
	if (local_softirq_pending()) {
		/*
		 * We can safely run softirq on inline stack, as we are not deep
		 * in the task stack here.
		 */
		__do_softirq();
		local_irq_enable();
		cond_resched();
		return;
	}
	local_irq_enable();
}

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
/*軟中斷處理時長在2ms一下,如果處理完軟中斷有發生則繼續執行*/
	unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
	unsigned long old_flags = current->flags;
	int max_restart = MAX_SOFTIRQ_RESTART;
	struct softirq_action *h;
	bool in_hardirq;
	__u32 pending;
	int softirq_bit;

	/*
	 * Mask out PF_MEMALLOC s current task context is borrowed for the
	 * softirq. A softirq handled such as network RX might set PF_MEMALLOC
	 * again if the socket is related to swap
	 */
	current->flags &= ~PF_MEMALLOC;

	pending = local_softirq_pending();
	account_irq_enter_time(current);

	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
	in_hardirq = lockdep_softirq_start();

restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0);

	local_irq_enable();

	h = softirq_vec;    /*軟中斷處理函式陣列*/

	while ((softirq_bit = ffs(pending))) {
		unsigned int vec_nr;
		int prev_count;

		h += softirq_bit - 1;

		vec_nr = h - softirq_vec;   /*軟中斷號*/
		prev_count = preempt_count();

		kstat_incr_softirqs_this_cpu(vec_nr);

		trace_softirq_entry(vec_nr);
		h->action(h);            /*執行軟中斷處理函式*/
		trace_softirq_exit(vec_nr);
		if (unlikely(prev_count != preempt_count())) {
			pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
			       vec_nr, softirq_to_name[vec_nr], h->action,
			       prev_count, preempt_count());
			preempt_count_set(prev_count);
		}
		h++;
		pending >>= softirq_bit;
	}

	rcu_bh_qs();
	local_irq_disable();

	pending = local_softirq_pending();
	/*如果又來軟中斷,時間還有則繼續處理,如果不需要排程的話*/
	if (pending) {
		if (time_before(jiffies, end) && !need_resched() &&
		    --max_restart)
			goto restart;

		wakeup_softirqd();
	}

	lockdep_softirq_end(in_hardirq);
	account_irq_exit_time(current);
	__local_bh_enable(SOFTIRQ_OFFSET);
	WARN_ON_ONCE(in_interrupt());
	current_restore_flags(old_flags, PF_MEMALLOC);
}

核心執行緒的執行時機和優先順序
喚醒:

  1. raise_softirq
  2. irq_exit()
  3. 如果執行一輪軟中斷後,時間超過2ms但是又有軟中斷來,主動調出後等待下次排程執行;如果軟中斷過多會導致處理不過來。
    優先順序?
    瀏覽程式碼,並沒有設定該核心執行緒的優先順序的地方,即使用的預設優先順序,即nice值0,和我們應用執行緒沒有什麼區別。

軟中斷的使用場景

  1. 定時器時間到處理handle;
  2. 網路收發包
  3. RCU
    inlucde/linux/interrupt.h中定義
enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	IRQ_POLL_SOFTIRQ,
	TASKLET_SOFTIRQ,
	SCHED_SOFTIRQ,
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
			    numbering. Sigh! */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS
};

中斷處理下半段的軟中斷和系統呼叫的軟中斷的區別

上文已經闡述。

結論

作者注

2020/12/27 凌晨4.01
知其然知其所以然,愛linux,愛生活,碎覺。

相關文章