本文將首先帶您回顧“系統調用”的概念以及它的作用,然后從經典的Hello World開始,逐行代碼層層分析——鴻蒙OS的系統調用是如何實現的。
寫在前面
9月10號 華為開發者大會(HDC)上,華為向廣大開發者宣布了鴻蒙2.0系統開源,源碼托管在國內源碼托管平臺“碼云”上:https://openharmony.gitee.com/
我也第一時間從碼云下載了鴻蒙系統的源代碼,并進行了編譯和分析。當晚回看了HDC上的關于鴻蒙OS 2.0的主題演講,個人最為好奇的是——這次開源的liteos-a內核。因為它支持了帶MMU(內存管理單元)的ARM Cortex-A設備;我們知道,在帶有MMU的處理器上,可以實現虛擬內存,進而實現進程之間的隔離、內核態和用戶態的隔離等等這些功能。
系統調用簡介
引用一張官方文檔中的圖片,看看liteos-a內核在整個系統中的位置。

這次開源的鴻蒙系統中同時包含了兩個內核,分別是liteos-a和liteos-m,其中的liteos-m和以前開源的LiteOS相當,而liteos-a是面向應用處理器的操作系統內核,提供了更為豐富的內核功能。此前已經開源的LiteOS,只是一個實時操作系統(RTOS),它主要面向的是內存和閃存配置都比較低的微控制器。
我們先來簡單回顧一下操作系統課程的一個知識點——系統調用,以及為什么會有系統調用?它的作用是什么?如果你對于這兩個問題以及了然于心,可以直接跳過本段,看后面的源碼分析部分。
在微控制器這樣的系統資源較少的硬件系統(比如STM32、MSP430、AVR、8051)上,通常直接裸跑程序(也就是不使用任何操作系統),或者使用像FreeRTOS、Zephyr這一類的實時操作系統(RTOS)。這些實時操作系統中,應用程序和內核程序直接運行在同一個物理內存空間(因為這些設備一般沒有MMU)上。而RTOS只提供了線程(或者叫任務),線程間同步、互斥等基礎設施;應用程序可以直接調用內核函數(用戶程序和內核程序只是邏輯上的劃分,本質上并沒有太大不同);一旦有一個線程發生異常,整個系統就會重啟。
而在ARM Cortex-A、x86、x86-64這樣的系統資源豐富的硬件系統上,SoC或CPU芯片內部一般集成了MMU,而且CPU有特權級別狀態(狀態寄存器的某些位)。基于特權級別狀態,可以實現部分硬件相關的操作只能在內核態進行,例如訪問外設等,用戶態應用程序不能訪問硬件設備。在這樣的系統上,系統調用是用戶態應用程序調用內核功能的請求入口。通俗的說,系統調用就是在有內核態和用戶態隔離的操作系統上,用戶態進程訪問內核態資源的一種方式。
從Hello World開始
接下來,我們一起從鴻蒙系統源碼分析它在liteos-a內核上是如何實現系統調用的。鴻蒙OS使用了musl libc,應用程序和系統服務都通過musl libc封裝的系統調用API接口訪問內核相關功能。
下面,我們就從經典的helloworld分析整個系統調用的流程。鴻蒙系統目前官方支持了三個芯片平臺,分別是Hi3516DV300(雙核ARM Cortex A-7 @ 900M Hz),Hi3518EV300(單核ARM Cortex A-7 @ 900MHz 內置64MB DDR2內存)和Hi3861V100(單核RISC-V @160M Hz 內置 SRAM 和 Flash)。其中Hi3516和Hi3518是帶有Cortex A7內核的芯片,鴻蒙系統在這兩個平臺使用的內核自然是liteos-a。根據官方指導文檔,我們知道這兩個平臺的第一個應用程序示例都是helloworld,源碼路徑為:applications/sample/camera/app/src/helloworld.c,除去頭部注釋,代碼內容為:
#include#include "los_sample.h" int main(int argc, char **argv) { printf("\n************************************************\n"); printf("\n\t\tHello OHOS!\n"); printf("\n************************************************\n\n"); LOS_Sample(g_num); return 0; }
musl libc的printf函數實現分析
文件路徑:third_party/musl/src/stdio/printf.c:
int printf(const char *restrict fmt, ...)
{
int ret;
va_list ap;
va_start(ap, fmt);
ret = vfprintf(stdout, fmt, ap);
va_end(ap);
return ret;
}
我們看到了,這里使用標準庫的stdout作為第一個參數調用了vfprintf,我們繼續向下分析third_party/musl/src/stdio/vfprintf.c文件:
int vfprintf(FILE *restrict f, const char *restrict fmt, va_list ap)
{
// 刪減若干和參數 f 無關的代碼行
FLOCK(f);
olderr = f->flags & F_ERR;
if (f->mode < 1) f->flags &= ~F_ERR;
if (!f->buf_size) {
saved_buf = f->buf;
f->buf = internal_buf;
f->buf_size = sizeof internal_buf;
f->wpos = f->wbase = f->wend = 0;
}
if (!f->wend && __towrite(f)) ret = -1;
else ret = printf_core(f, fmt, &ap2, nl_arg, nl_type);
if (saved_buf) {
f->write(f, 0, 0);
if (!f->wpos) ret = -1;
f->buf = saved_buf;
f->buf_size = 0;
f->wpos = f->wbase = f->wend = 0;
}
if (f->flags & F_ERR) ret = -1;
f->flags |= olderr;
FUNLOCK(f);
va_end(ap2);
return ret;
}
這里,我們繼續關注三處帶有參數f的調用:__towrite(f),printf_core(f, fmt, &ap2, nl_arg, nl_type),f->write(f, 0, 0);
其中,__towrite的實現位于third_party/musl/src/stdio/__towrite.c(可見和系統調用無關):
int __towrite(FILE *f)
{
f->mode |= f->mode-1;
if (f->flags & F_NOWR) {
f->flags |= F_ERR;
return EOF;
}
/* Clear read buffer (easier than summoning nasal demons) */
f->rpos = f->rend = 0;
/* Activate write through the buffer. */
f->wpos = f->wbase = f->buf;
f->wend = f->buf + f->buf_size;
return 0;
}
從內容上看,__towrite函數的作用是更新文件結構FILE的wpos、wbase、wend成員,以指向待寫入實際文件的內存緩沖區域,同時將rpos、rend值為零。
printf_core的實現也位于src/stdio/vfprintf.c文件:
static int printf_core(FILE *f, const char *fmt, va_list *ap, union arg *nl_arg, int *nl_type)
{
// 刪除了變量定義部分
for (;;) {
/* This error is only specified for snprintf, but since it's
* unspecified for other forms, do the same. Stop immediately
* on overflow; otherwise %n could produce wrong results. */
if (l > INT_MAX - cnt) goto overflow;
/* Update output count, end loop when fmt is exhausted */
cnt += l;
if (!*s) break;
/* Handle literal text and %% format specifiers */
for (a=s; *s && *s!='%'; s++);
for (z=s; s[0]=='%' && s[1]=='%'; z++, s+=2);
if (z-a > INT_MAX-cnt) goto overflow;
l = z-a;
if (f) out(f, a, l);
if (l) continue;
if (isdigit(s[1]) && s[2]=='$') {
l10n=1;
argpos = s[1]-'0';
s+=3;
} else {
argpos = -1;
s++;
}
/* Read modifier flags */
for (fl=0; (unsigned)*s-' '<32 && (FLAGMASK&(1U<<*s-' ')); s++)
fl |= 1U<<*s-' ';
/* Read field width */
if (*s=='*') {
if (isdigit(s[1]) && s[2]=='$') {
l10n=1;
nl_type[s[1]-'0'] = INT;
w = nl_arg[s[1]-'0'].i;
s+=3;
} else if (!l10n) {
w = f ? va_arg(*ap, int) : 0;
s++;
} else goto inval;
if (w<0) fl|=LEFT_ADJ, w=-w;
} else if ((w=getint(&s))<0) goto overflow;
/* Read precision */
if (*s=='.' && s[1]=='*') {
if (isdigit(s[2]) && s[3]=='$') {
nl_type[s[2]-'0'] = INT;
p = nl_arg[s[2]-'0'].i;
s+=4;
} else if (!l10n) {
p = f ? va_arg(*ap, int) : 0;
s+=2;
} else goto inval;
xp = (p>=0);
} else if (*s=='.') {
s++;
p = getint(&s);
xp = 1;
} else {
p = -1;
xp = 0;
}
/* Format specifier state machine */
st=0;
do {
if (OOB(*s)) goto inval;
ps=st;
st=states[st]S(*s++);
} while (st-1=0) goto inval;
} else {
if (argpos>=0) nl_type[argpos]=st, arg=nl_arg[argpos];
else if (f) pop_arg(&arg, st, ap);
else return 0;
}
if (!f) continue;
z = buf + sizeof(buf);
prefix = "-+ 0X0x";
pl = 0;
t = s[-1];
/* Transform ls,lc -> S,C */
if (ps && (t&15)==3) t&=~32;
/* - and 0 flags are mutually exclusive */
if (fl & LEFT_ADJ) fl &= ~ZERO_PAD;
switch(t) {
case 'n':
switch(ps) {
case BARE: *(int *)arg.p = cnt; break;
case LPRE: *(long *)arg.p = cnt; break;
case LLPRE: *(long long *)arg.p = cnt; break;
case HPRE: *(unsigned short *)arg.p = cnt; break;
case HHPRE: *(unsigned char *)arg.p = cnt; break;
case ZTPRE: *(size_t *)arg.p = cnt; break;
case JPRE: *(uintmax_t *)arg.p = cnt; break;
}
continue;
case 'p':
p = MAX(p, 2*sizeof(void*));
t = 'x';
fl |= ALT_FORM;
case 'x': case 'X':
a = fmt_x(arg.i, z, t&32);
if (arg.i && (fl & ALT_FORM)) prefix+=(t>>4), pl=2;
if (0) {
case 'o':
a = fmt_o(arg.i, z);
if ((fl&ALT_FORM) && pINTMAX_MAX) {
arg.i=-arg.i;
} else if (fl & MARK_POS) {
prefix++;
} else if (fl & PAD_POS) {
prefix+=2;
} else pl=0;
case 'u':
a = fmt_u(arg.i, z);
}
if (xp && p<0) goto overflow;
if (xp) fl &= ~ZERO_PAD;
if (!arg.i && !p) {
a=z;
break;
}
p = MAX(p, z-a + !arg.i);
break;
case 'c':
*(a=z-(p=1))=arg.i;
fl &= ~ZERO_PAD;
break;
case 'm':
if (1) a = strerror(errno); else
case 's':
a = arg.p ? arg.p : "(null)";
z = a + strnlen(a, p<0 ? INT_MAX : p);
if (p<0 && *z) goto overflow;
p = z-a;
fl &= ~ZERO_PAD;
break;
case 'C':
wc[0] = arg.i;
wc[1] = 0;
arg.p = wc;
p = -1;
case 'S':
ws = arg.p;
for (i=l=0; i=0 && l<=p-i; i+=l);
if (l<0) return -1;
if (i > INT_MAX) goto overflow;
p = i;
pad(f, ' ', w, p, fl);
ws = arg.p;
for (i=0; i<0U+p && *ws && i+(l=wctomb(mb, *ws++))<=p; i+=l)
out(f, mb, l);
pad(f, ' ', w, p, fl^LEFT_ADJ);
l = w>p ? w : p;
continue;
case 'e': case 'f': case 'g': case 'a':
case 'E': case 'F': case 'G': case 'A':
if (xp && p<0) goto overflow;
l = fmt_fp(f, arg.f, w, p, fl, t);
if (l<0) goto overflow;
continue;
}
if (p < z-a) p = z-a;
if (p > INT_MAX-pl) goto overflow;
if (w < pl+p) w = pl+p;
if (w > INT_MAX-cnt) goto overflow;
pad(f, ' ', w, pl+p, fl);
out(f, prefix, pl);
pad(f, '0', w, pl+p, fl^ZERO_PAD);
pad(f, '0', p, z-a, 0);
out(f, a, z-a);
pad(f, ' ', w, pl+p, fl^LEFT_ADJ);
l = w;
}
if (f) return cnt;
if (!l10n) return 0;
for (i=1; i<=NL_ARGMAX && nl_type[i]; i++)
pop_arg(nl_arg+i, nl_type[i], ap);
for (; i<=NL_ARGMAX && !nl_type[i]; i++);
if (i<=NL_ARGMAX) goto inval;
return 1;
inval: // 刪除了錯誤處理代碼
overflow: // 刪除了錯誤處理代碼
}
從注釋和代碼結構可以看出,這個函數實現了格式化字符串展開的主要流程,這里又調用了out和pad兩個函數,從命名猜測應該分別是向內存緩沖區寫入內容和填充內容的函數,它們的實現也位于vfprintf.c中:
static void out(FILE *f, const char *s, size_t l)
{
if (!(f->flags & F_ERR)) __fwritex((void *)s, l, f);
}
static void pad(FILE *f, char c, int w, int l, int fl)
{
char pad[256];
if (fl & (LEFT_ADJ | ZERO_PAD) || l >= w) return;
l = w - l;
memset(pad, c, l>sizeof pad ? sizeof pad : l);
for (; l >= sizeof pad; l -= sizeof pad)
out(f, pad, sizeof pad);
out(f, pad, l);
}
它們又調用了__fwritex,它的實現位于third_party/musl/src/stdio/fwrite.c:
size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f)
{
size_t i=0;
if (!f->wend && __towrite(f)) return 0;
if (l > f->wend - f->wpos) return f->write(f, s, l);
if (f->lbf >= 0) {
/* Match /^(.*\n|)/ */
for (i=l; i && s[i-1] != '\n'; i--);
if (i) {
size_t n = f->write(f, s, i);
if (n < i) return n;
s += i;
l -= i;
}
}
memcpy(f->wpos, s, l);
f->wpos += l;
return l+i;
}
這里又出現了vfprintf中出現的f->write(f, s, i),下面我們就分析這個函數實際底是什么?
我們先找到它的定義prebuilts/lite/sysroot/usr/include/arm-liteos/bits/alltypes.h:
#if defined(__NEED_FILE) && !defined(__DEFINED_FILE) typedef struct _IO_FILE FILE; #define __DEFINED_FILE #endif
以及third_party/musl/src/internal/stdio_impl.h:
struct _IO_FILE {
unsigned flags;
unsigned char *rpos, *rend;
int (*close)(FILE *);
unsigned char *wend, *wpos;
unsigned char *mustbezero_1;
unsigned char *wbase;
size_t (*read)(FILE *, unsigned char *, size_t);
size_t (*write)(FILE *, const unsigned char *, size_t); // <--關注它
off_t (*seek)(FILE *, off_t, int);
unsigned char *buf;
size_t buf_size;
FILE *prev, *next;
int fd;
int pipe_pid;
long lockcount;
int mode;
volatile int lock;
int lbf;
void *cookie;
off_t off;
char *getln_buf;
void *mustbezero_2;
unsigned char *shend;
off_t shlim, shcnt;
FILE *prev_locked, *next_locked;
struct __locale_struct *locale;
};
我們再繼續尋找stdout的各個成員值是什么?
可以找到third_party/musl/src/stdio/stdout.c文件中的:
static unsigned char buf[BUFSIZ+UNGET];
hidden FILE __stdout_FILE = {
.buf = buf+UNGET,
.buf_size = sizeof buf-UNGET,
.fd = 1, // fd 為 1 和多數UNIX系統一樣
.flags = F_PERM | F_NORD,
.lbf = '\n',
.write = __stdout_write, // <-- write 成員在這里
.seek = __stdio_seek,
.close = __stdio_close,
.lock = -1,
};
FILE *const stdout = &__stdout_FILE; // <-- stdout 在這里
third_party/musl/src/stdio/__stdout_write.c文件中:
size_t __stdout_write(FILE *f, const unsigned char *buf, size_t len)
{
struct winsize wsz;
f->write = __stdio_write;
if (!(f->flags & F_SVB) && __syscall(SYS_ioctl, f->fd, TIOCGWINSZ, &wsz))
f->lbf = -1;
return __stdio_write(f, buf, len);
}
這段代碼里調用了SYS_ioctl系統調用,但主體流程是下方的函數__stdio_write,它的實現在third_party/musl/src/stdio/__stdio_write.c文件中:
size_t __stdio_write(FILE *f, const unsigned char *buf, size_t len)
{
struct iovec iovs[2] = {
{ .iov_base = f->wbase, .iov_len = f->wpos-f->wbase },
{ .iov_base = (void *)buf, .iov_len = len }
};
struct iovec *iov = iovs;
size_t rem = iov[0].iov_len + iov[1].iov_len;
int iovcnt = 2;
ssize_t cnt;
for (;;) {
cnt = syscall(SYS_writev, f->fd, iov, iovcnt); // <-- 看這里!
if (cnt == rem) {
f->wend = f->buf + f->buf_size;
f->wpos = f->wbase = f->buf;
return len;
}
if (cnt < 0) {
f->wpos = f->wbase = f->wend = 0;
f->flags |= F_ERR;
return iovcnt == 2 ? 0 : len-iov[0].iov_len;
}
rem -= cnt;
if (cnt > iov[0].iov_len) {
cnt -= iov[0].iov_len;
iov++; iovcnt--;
}
iov[0].iov_base = (char *)iov[0].iov_base + cnt;
iov[0].iov_len -= cnt;
}
}
至此,我們看到了printf函數最終調用到了兩個系統調用SYS_ioctl和SYS_write。
musl libc的syscall函數實現分析
在上一節中,我們看到printf最終調用到了兩個長得像系統調用的函數syscall和__syscall。
系統調用宏syscall的實現
在musl代碼倉(third_party/musl)下搜索:
$ find . -name '*.h' | xargs grep --color -n '\ssyscall('
./kernel/include/unistd.h:198:long syscall(long, ...);
./src/internal/syscall.h:44:#define syscall(...) __syscall_ret(__syscall(__VA_ARGS__))
./include/unistd.h:199:long syscall(long, ...);
可以找到third_party/musl/src/internal/syscall.h:
#define __syscall(...) __SYSCALL_DISP(__syscall,__VA_ARGS__) #define syscall(...) __syscall_ret(__syscall(__VA_ARGS__))
這里可以看到它們兩者都是宏,而syscall調用了__syscall,而__syscall又調用了__SYSCALL_DISP,它的實現也在同一個文件中:
#define __SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n #define __SYSCALL_NARGS(...) __SYSCALL_NARGS_X(__VA_ARGS__,7,6,5,4,3,2,1,0,) #define __SYSCALL_CONCAT_X(a,b) a##b #define __SYSCALL_CONCAT(a,b) __SYSCALL_CONCAT_X(a,b) #define __SYSCALL_DISP(b,...) __SYSCALL_CONCAT(b,__SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__)
我們以__stdio_write中調用syscall處進行分析,即嘗試展開syscall(SYS_writev, f->fd, iov, iovcnt);
syscall(SYS_writev, f->fd, iov, iovcnt); => __syscall_ret(__syscall(SYS_writev, f->fd, iov, iovcnt)) // 展開syscall => __syscall_ret(__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)); // 展開__syscall
先忽略最外層的__syscall_ret,展開__SYSCALL_DISP部分:
__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt) => __SYSCALL_CONCAT(__syscall, __SYSCALL_NARGS(SYS_writev, f->fd, iov, iovcnt))(SYS_writev, f->fd, iov, iovcnt) // 展開 __SYSCALL_DISP
忽略外層的__SYSCALL_CONCAT,展開__SYSCALL_NARGS_X部分:
__SYSCALL_NARGS(SYS_writev, f->fd, iov, iovcnt) => __SYSCALL_NARGS_X(SYS_writev, f->fd, iov, iovcnt,7,6,5,4,3,2,1,0,) // 展開 __SYSCALL_NARGS => 3 // 展開 __SYSCALL_NARGS_X // SYS_writev, f->fd, iov, iovcnt 和宏參數 a,b,c,d 對應 // 7,6,5,4 和宏參數 e,f,g,h 對應 // 3 和宏參數 n 對應 // 宏表達式的值為 n 也就是 3,
回到__SYSCALL_CONCAT展開流程,
__SYSCALL_CONCAT(__syscall, __SYSCALL_NARGS(SYS_writev, f->fd, iov, iovcnt)) => __SYSCALL_CONCAT(__syscall, 3) => __SYSCALL_CONCAT_X(__syscall, 3) => __syscall3
再回到__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)展開流程,結果應該是:
__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt) => __syscall3(SYS_writev, f->fd, iov, iovcnt)
系統調用函數__syscall3的實現
這些__syscall[1-7]的系統調用包裝宏定義如下:
#ifndef __scc #define __scc(X) ((long) (X)) // 轉為long類型 typedef long syscall_arg_t; #endif #define __syscall1(n,a) __syscall1(n,__scc(a)) #define __syscall2(n,a,b) __syscall2(n,__scc(a),__scc(b)) #define __syscall3(n,a,b,c) __syscall3(n,__scc(a),__scc(b),__scc(c)) // <- 看這里 #define __syscall4(n,a,b,c,d) __syscall4(n,__scc(a),__scc(b),__scc(c),__scc(d)) #define __syscall5(n,a,b,c,d,e) __syscall5(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e)) #define __syscall6(n,a,b,c,d,e,f) __syscall6(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e),__scc(f)) #define __syscall7(n,a,b,c,d,e,f,g) __syscall7(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e),__scc(f),__scc(g))
繼續搜索發現有多出匹配,我們關注arch/arm目錄下的文件,因為ARM Cortext A7是Armv7-A指令集的32位CPU(如果是Armv8-A指令集的64位CPU則對應arch/aarch64下的文件):
static inline long __syscall3(long n, long a, long b, long c)
{
register long r7 __ASM____R7__ = n;
register long r0 __asm__("r0") = a;
register long r1 __asm__("r1") = b;
register long r2 __asm__("r2") = c;
__asm_syscall(R7_OPERAND, "0"(r0), "r"(r1), "r"(r2));
}
這段代碼中還有三個宏,__ASM____R7__、__asm_syscall和R7_OPERAND:
#ifdef __thumb__
#define __ASM____R7__
#define __asm_syscall(...) do { \
__asm__ __volatile__ ( "mov %1,r7 ; mov r7,%2 ; svc 0 ; mov r7,%1" \
: "=r"(r0), "=&r"((int){0}) : __VA_ARGS__ : "memory"); \
return r0; \
} while (0)
#else // __thumb__
#define __ASM____R7__ __asm__("r7")
#define __asm_syscall(...) do { \
__asm__ __volatile__ ( "svc 0" \
: "=r"(r0) : __VA_ARGS__ : "memory"); \
return r0; \
} while (0)
#endif // __thumb__
#ifdef __thumb2__
#define R7_OPERAND "rI"(r7)
#else
#define R7_OPERAND "r"(r7)
#endif
它們有兩個實現版,分別對應于編譯器THUMB選項的開啟和關閉。這兩種選項條件下的代碼流程基本一致,以下僅以未開啟THUMB選項為例進行分析。這兩個宏展開后的__syscall3函數內容為:
static inline long __syscall3(long n, long a, long b, long c)
{
register long r7 __asm__("r7") = n; // 系統調用號
register long r0 __asm__("r0") = a; // 參數0
register long r1 __asm__("r1") = b; // 參數1
register long r2 __asm__("r2") = c; // 參數2
do { \
__asm__ __volatile__ ( "svc 0" \
: "=r"(r0) : "r"(r7), "0"(r0), "r"(r1), "r"(r2) : "memory"); \
return r0; \
} while (0);
}
這里最后的一個內嵌匯編比較復雜,它符合如下格式(具體細節可以查閱gcc內嵌匯編文檔的擴展匯編說明):
asm asm-qualifiers ( AssemblerTemplate
: OutputOperands
[ : InputOperands
[ : Clobbers ] ])
匯編模板為:"svc 0", 輸出參數部分為:"=r"(r0),輸出寄存器為r0輸入參數部分為:"r"(r7), "0"(r0), "r"(r1), "r"(r2),輸入寄存器為r7,r0,r1,r2,("0"的含義是,這個輸入寄存器必須和輸出寄存器第0個位置一樣) Clobber部分為:"memory"
這里我們只需要記住:系統調用號存放在r7寄存器,參數存放在r0,r1,r2,返回值最終會存放在r0中;
SVC指令,ARM Cortex A7手冊 的解釋為:
The SVC instruction causes a Supervisor Call exception. This provides a mechanism for unprivileged software to make a call to the operating system, or other system component that is accessible only at PL1.
翻譯過來就是說
SVC指令會觸發一個“特權調用”異常。這為非特權軟件調用操作系統或其他只能在PL1級別訪問的系統組件提供了一種機制。
詳細的指令說明在
到這里,我們分析了鴻蒙系統上應用程序如何進入內核態,主要分析的是musl libc的實現。
liteos-a內核的系統調用實現分析
既然SVC能夠觸發一個異常,那么我們就要看看liteos-a內核是如何處理這個異常的。
ARM Cortex A7中斷向量表
在ARM架構參考手冊中,可以找到中斷向量表的說明:

可以看到SVC中斷向量的便宜地址是0x08,我們可以在kernel/liteos_a/arch/arm/arm/src/startup目錄的reset_vector_mp.S文件和reset_vector_up.S文件中找到相關匯編代碼:
__exception_handlers:
/*
*Assumption: ROM code has these vectors at the hardware reset address.
*A simple jump removes any address-space dependencies [i.e. safer]
*/
b reset_vector
b _osExceptUndefInstrHdl
b _osExceptSwiHdl
b _osExceptPrefetchAbortHdl
b _osExceptDataAbortHdl
b _osExceptAddrAbortHdl
b OsIrqHandler
b _osExceptFiqHdl
PS:kernel/liteos_a/arch/arm/arm/src/startup目錄有兩個文件reset_vector_mp.S文件和reset_vector_up.S文件分別對應多核和單核編譯選項:
ifeq ($(LOSCFG_KERNEL_SMP), y) LOCAL_SRCS += src/startup/reset_vector_mp.S else LOCAL_SRCS += src/startup/reset_vector_up.S endif
SVC中斷處理函數
上面的匯編代碼中可以看到,_osExceptSwiHdl函數就是SVC異常處理函數,具體實現在kernel/liteos_a/arch/arm/arm/src/los_hw_exc.S文件中:
@ Description: Software interrupt exception handler
_osExceptSwiHdl:
SUB SP, SP, #(4 * 16) @ 棧增長
STMIA SP, {R0-R12} @ 保存R0-R12寄存器到棧上
MRS R3, SPSR @ 移動SPSR寄存器的值到R3
MOV R4, LR
AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode
CMP R1, #CPSR_USER_MODE @ User mode
BNE OsKernelSVCHandler @ Branch if not user mode
@ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr).
@ stmia with ^ will return the user mode registers (provided that r15 is not in the register list).
MOV R0, SP
STMFD SP!, {R3} @ Save the CPSR
ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage
STMFD R3!, {R4} @ Save the CPSR and r15(pc)
STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr)
SUB SP, SP, #4
PUSH_FPU_REGS R1
MOV FP, #0 @ Init frame pointer
CPSIE I @ Interrupt Enable
BLX OsArmA32SyscallHandle
CPSID I @ Interrupt Disable
POP_FPU_REGS R1
ADD SP, SP,#4
LDMFD SP!, {R3} @ Fetch the return SPSR
MSR SPSR_cxsf, R3 @ Set the return mode SPSR
@ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr).
@ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list)
LDMFD SP!, {R0-R12}
LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14
ADD SP, SP, #(2 * 4)
LDMFD SP!, {PC}^ @ Return to user
這段代碼的注釋較為清楚,可以看到,內核模式會繼續調用OsKernelSVCHandler,用戶模式會繼續調用OsArmA32SyscallHandle函數;
OsArmA32SyscallHandle函數
我們這里分析的流程是從用戶模式進入的,所以調用的是OsArmA32SyscallHandle,它的實現位于kernel/liteos_a/syscall/los_syscall.c文件:
/* The SYSCALL ID is in R7 on entry. Parameters follow in R0..R6 */
LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs)
{
UINT32 ret;
UINT8 nArgs;
UINTPTR handle;
UINT32 cmd = regs[REG_R7];
if (cmd >= SYS_CALL_NUM) {
PRINT_ERR("Syscall ID: error %d !!!\n", cmd);
return regs;
}
if (cmd == __NR_sigreturn) {
OsRestorSignalContext(regs);
return regs;
}
handle = g_syscallHandle[cmd]; // 得到實際系統調用處理函數
nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */
nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);
if ((handle == 0) || (nArgs > ARG_NUM_7)) {
PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs);
regs[REG_R0] = -ENOSYS;
return regs;
}
switch (nArgs) { // 以下各個case是實際函數調用
case ARG_NUM_0:
case ARG_NUM_1:
ret = (*(SyscallFun1)handle)(regs[REG_R0]);
break;
case ARG_NUM_2:
case ARG_NUM_3:
ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);
break;
case ARG_NUM_4:
case ARG_NUM_5:
ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3],
regs[REG_R4]);
break;
default:
ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3],
regs[REG_R4], regs[REG_R5], regs[REG_R6]);
}
regs[REG_R0] = ret; // 返回值填入R0
OsSaveSignalContext(regs);
/* Return the last value of curent_regs. This supports context switches on return from the exception.
* That capability is only used with theSYS_context_switch system call.
*/
return regs;
}
這個函數中用到了個全局數組g_syscallHandle和g_syscallNArgs,它們的定義以及初始化函數也在同一個文件中:
static UINTPTR g_syscallHandle[SYS_CALL_NUM] = {0};
static UINT8 g_syscallNArgs[(SYS_CALL_NUM + 1) / NARG_PER_BYTE] = {0};
void SyscallHandleInit(void)
{
#define SYSCALL_HAND_DEF(id, fun, rType, nArg) \
if ((id) < SYS_CALL_NUM) { \
g_syscallHandle[(id)] = (UINTPTR)(fun); \
g_syscallNArgs[(id) / NARG_PER_BYTE] |= \
((id) & 1) ? (nArg) << NARG_BITS : (nArg); \
}
#include "syscall_lookup.h"
#undef SYSCALL_HAND_DEF
}
其中SYSCALL_HAND_DEF宏的對齊格式我做了一點調整。
從g_syscallNArgs成員賦值以及定義的地方,能看出它的每個UINT8成員被用來存放兩個系統調用的參數個數,從而實現更少的內存占用;
syscall_lookup.h文件和los_syscall.c位于同一目錄,它記錄了系統調用函數對照表,我們僅節取一部分:
SYSCALL_HAND_DEF(__NR_read, SysRead, ssize_t, ARG_NUM_3) SYSCALL_HAND_DEF(__NR_write, SysWrite, ssize_t, ARG_NUM_3) // <-- 我們要跟蹤的 write 在這里 SYSCALL_HAND_DEF(__NR_open, SysOpen, int, ARG_NUM_7) SYSCALL_HAND_DEF(__NR_close, SysClose, int, ARG_NUM_1) SYSCALL_HAND_DEF(__NR_creat, SysCreat, int, ARG_NUM_2) SYSCALL_HAND_DEF(__NR_unlink, SysUnlink, int, ARG_NUM_1) #ifdef LOSCFG_KERNEL_DYNLOAD SYSCALL_HAND_DEF(__NR_execve, SysExecve, int, ARG_NUM_3) #endif
看到這里,write系統調用的內核函數終于找到了——SysWrite。
到此,我們已經知道了liteos-a的系統調用機制是如何實現的。
liteos-a內核SysWrite的實現
SysWrite函數的實現位于kernel/liteos_a/syscall/fs_syscall.c文件:
ssize_t SysWrite(int fd, const void *buf, size_t nbytes)
{
int ret;
if (nbytes == 0) {
return 0;
}
if (!LOS_IsUserAddressRange((vaddr_t)(UINTPTR)buf, nbytes)) {
return -EFAULT;
}
/* Process fd convert to system global fd */
fd = GetAssociatedSystemFd(fd);
ret = write(fd, buf, nbytes); // <-- ??似曾相識??
if (ret < 0) {
return -get_errno();
}
return ret;
}
它又調用了write?但是這一次是內核空間的write,不再是 musl libc,經過一番搜索,我們可以找到另一個文件third_party/NuttX/fs/vfs/fs_write.c中的write:
ssize_t write(int fd, FAR const void *buf, size_t nbytes) {
#if CONFIG_NFILE_DESCRIPTORS > 0
FAR struct file *filep;
if ((unsigned int)fd >= CONFIG_NFILE_DESCRIPTORS)
#endif
{ /* Write to a socket descriptor is equivalent to send with flags == 0 */
#if defined(LOSCFG_NET_LWIP_SACK)
FAR const void *bufbak = buf;
ssize_t ret;
if (LOS_IsUserAddress((VADDR_T)(uintptr_t)buf)) {
if (buf != NULL && nbytes > 0) {
buf = malloc(nbytes);
if (buf == NULL) { /* 省略 錯誤處理 代碼 */ }
if (LOS_ArchCopyFromUser((void*)buf, bufbak, nbytes) != 0) {/* 省略 */}
}
}
ret = send(fd, buf, nbytes, 0); // 這個分支是處理socket fd的
if (buf != bufbak) {
free((void*)buf);
}
return ret;
#else
set_errno(EBADF);
return VFS_ERROR;
#endif
}
#if CONFIG_NFILE_DESCRIPTORS > 0
/* The descriptor is in the right range to be a file descriptor... write
* to the file.
*/
if (fd <= STDERR_FILENO && fd >= STDIN_FILENO) { /* fd : [0,2] */
fd = ConsoleUpdateFd();
if (fd < 0) {
set_errno(EBADF);
return VFS_ERROR;
}
}
int ret = fs_getfilep(fd, &filep);
if (ret < 0) {
/* The errno value has already been set */
return VFS_ERROR;
}
if (filep->f_oflags & O_DIRECTORY) {
set_errno(EBADF);
return VFS_ERROR;
}
if (filep->f_oflags & O_APPEND) {
if (file_seek64(filep, 0, SEEK_END) == -1) {
return VFS_ERROR;
}
}
/* Perform the write operation using the file descriptor as an index */
return file_write(filep, buf, nbytes);
#endif
}
找到這段代碼,我們知道了:
liteos-a的vfs是在NuttX基礎上實現的,NuttX是一個開源RTOS項目;
liteos-a的TCP/IP協議棧是基于lwip的,lwip也是一個開源項目;
這段代碼中的write分為兩個分支,socket fd調用lwip的send,另一個分支調用file_write;
至于,file_write如何調用到存儲設備驅動程序,則是更底層的實現了,本文不在繼續分析。
補充說明
本文內容均是基于鴻蒙系統開源項目OpenHarmony源碼靜態分析所整理,沒有進行實際的運行環境調試,實際執行過程可能有所差異,希望發現錯誤的讀者及時指正。文中所有路徑均為整個openharmony源碼樹上的相對路徑(而非liteos源碼相對路徑)
編輯:hfy
-
RTOS
+關注
關注
25文章
866瀏覽量
122979 -
鴻蒙系統
+關注
關注
183文章
2642瀏覽量
69844 -
OpenHarmony
+關注
關注
33文章
3952瀏覽量
21096
發布評論請先 登錄
AT32開源鴻蒙開發環境搭建
開源鴻蒙項目順利孵化畢業
開源鴻蒙項目達成開源孵化目標順利畢業
開源鴻蒙和開源歐拉成為開放原子開源基金會首批畢業項目
【置頂公告】視美泰開源鴻蒙系列產品業務咨詢與商務合作通道正式開啟!
開源鴻蒙6.0 Release版本重磅發布
觸覺智能RK3576開發板OpenHarmony開源鴻蒙系統USB控制傳輸功能示例
開源鴻蒙持續取得顯著成果
華為亮相2025開源鴻蒙開發者大會
請問下,openharmony支持哪一款龍芯的開發板?有沒有開源的龍芯的openharmony源碼?
鴻蒙北向開發OpenHarmony5.0 DevEco Studio開發工具安裝與配置
【貝啟科技BQ3568HM開源鴻蒙開發板深度試用報告】系統鏡像編譯和燒錄
OpenHarmony4.1系統WiFi藍牙打不開時,教你如何排查解決問題
OpenHarmony5.0系統怎么去除鎖屏直接進入界面?教你2步搞定
基于鴻蒙系統開源項目OpenHarmony源碼靜態分析
評論