Golang調(diào)度與MPG

本文轉(zhuǎn)自:https://studygolang.com/articles/01855侠鳄。原文圖碎了膘茎,這里補(bǔ)充上圖片矢炼。

我們都知道Go語(yǔ)言是原生支持語(yǔ)言級(jí)并發(fā)的,這個(gè)并發(fā)的最小邏輯單元就是goroutine。goroutine就是Go語(yǔ)言提供的一種用戶態(tài)線程,當(dāng)然這種用戶態(tài)線程是跑在內(nèi)核級(jí)線程之上的贯莺。當(dāng)我們創(chuàng)建了很多的goroutine,并且它們都是跑在同一個(gè)內(nèi)核線程之上的時(shí)候宁改,就需要一個(gè)調(diào)度器來(lái)維護(hù)這些goroutine缕探,確保所有的goroutine都使用cpu,并且是盡可能公平的使用cpu資源还蹲。

這個(gè)調(diào)度器的原理以及實(shí)現(xiàn)值得我們?nèi)ド钊胙芯恳幌碌摹V握麄€(gè)調(diào)度器的主要有4個(gè)重要結(jié)構(gòu),分別是M谜喊、G潭兽、P、Sched斗遏,前三個(gè)定義在runtime.h中山卦,Sched定義在proc.c中。

  • Sched結(jié)構(gòu)就是調(diào)度器诵次,它維護(hù)有存儲(chǔ)M和G的隊(duì)列以及調(diào)度器的一些狀態(tài)信息等账蓉。
  • M代表內(nèi)核級(jí)線程,一個(gè)M就是一個(gè)線程逾一,goroutine就是跑在M之上的铸本;M是一個(gè)很大的結(jié)構(gòu),里面維護(hù)小對(duì)象內(nèi)存cache(mcache)遵堵、當(dāng)前執(zhí)行的goroutine归敬、隨機(jī)數(shù)發(fā)生器等等非常多的信息。
  • P全稱是Processor鄙早,處理器汪茧,它的主要用途就是用來(lái)執(zhí)行g(shù)oroutine的,所以它也維護(hù)了一個(gè)goroutine隊(duì)列限番,里面存儲(chǔ)了所有需要它來(lái)執(zhí)行的goroutine舱污,這個(gè)P的角色可能有一點(diǎn)讓人迷惑,一開始容易和M沖突弥虐,后面重點(diǎn)聊一下它們的關(guān)系扩灯。
  • G就是goroutine實(shí)現(xiàn)的核心結(jié)構(gòu)了,G維護(hù)了goroutine需要的棧霜瘪、程序計(jì)數(shù)器以及它所在的M等信息珠插。
    理解M、P颖对、G三者的關(guān)系對(duì)理解整個(gè)調(diào)度器非常重要捻撑,我從網(wǎng)絡(luò)上找了一個(gè)圖來(lái)說(shuō)明其三者關(guān)系:


    地鼠

地鼠(gopher)用小車運(yùn)著一堆待加工的磚。M就可以看作圖中的地鼠,P就是小車顾患,G就是小車?yán)镅b的磚番捂。一圖勝千言啊,弄清楚了它們?nèi)叩年P(guān)系江解,下面我們就開始重點(diǎn)聊地鼠是如何在搬運(yùn)磚塊的设预。

啟動(dòng)過(guò)程

在關(guān)心絕大多數(shù)程序的內(nèi)部原理的時(shí)候,我們都試圖去弄明白其啟動(dòng)初始化過(guò)程犁河,弄明白這個(gè)過(guò)程對(duì)后續(xù)的深入分析至關(guān)重要鳖枕。在asm_amd64.s文件中的匯編代碼_rt0_amd64就是整個(gè)啟動(dòng)過(guò)程,核心過(guò)程如下:

CALL    runtime·args(SB)
CALL    runtime·osinit(SB)
CALL    runtime·hashinit(SB)
CALL    runtime·schedinit(SB)

// create a new goroutine to start program
PUSHQ   $runtime·main·f(SB)     // entry
PUSHQ   $0          // arg size
CALL    runtime·newproc(SB)
POPQ    AX
POPQ    AX

// start this M
CALL    runtime·mstart(SB)

啟動(dòng)過(guò)程做了調(diào)度器初始化runtime·schedinit后桨螺,調(diào)用runtime·newproc創(chuàng)建出第一個(gè)goroutine宾符,這個(gè)goroutine將執(zhí)行的函數(shù)是runtime·main,這第一個(gè)goroutine也就是所謂的主goroutine彭谁。我們寫的最簡(jiǎn)單的Go程序”hello,world”就是完全跑在這個(gè)goroutine里允扇,當(dāng)然任何一個(gè)Go程序的入口都是從這個(gè)goroutine開始的缠局。最后調(diào)用的runtime·mstart就是真正的執(zhí)行上一步創(chuàng)建的主goroutine。

啟動(dòng)過(guò)程中的調(diào)度器初始化runtime·schedinit函數(shù)主要根據(jù)用戶設(shè)置的GOMAXPROCS值來(lái)創(chuàng)建一批小車(P)考润,不管GOMAXPROCS設(shè)置為多大狭园,最多也只能創(chuàng)建256個(gè)小車(P)。這些小車(p)初始創(chuàng)建好后都是閑置狀態(tài)糊治,也就是還沒(méi)開始使用唱矛,所以它們都放置在調(diào)度器結(jié)構(gòu)(Sched)的pidle字段維護(hù)的鏈表中存儲(chǔ)起來(lái)了,以備后續(xù)之需井辜。

查看runtime·main函數(shù)可以了解到主goroutine開始執(zhí)行后绎谦,做的第一件事情是創(chuàng)建了一個(gè)新的內(nèi)核線程(地鼠M),不過(guò)這個(gè)線程是一個(gè)特殊線程粥脚,它在整個(gè)運(yùn)行期專門負(fù)責(zé)做特定的事情——系統(tǒng)監(jiān)控(sysmon)窃肠。接下來(lái)就是進(jìn)入Go程序的main函數(shù)開始Go程序的執(zhí)行。

至此刷允,Go程序就被啟動(dòng)起來(lái)開始運(yùn)行了冤留。一個(gè)真正干活的Go程序,一定創(chuàng)建有不少的goroutine树灶,所以在Go程序開始運(yùn)行后纤怒,就會(huì)向調(diào)度器添加goroutine,調(diào)度器就要負(fù)責(zé)維護(hù)好這些goroutine的正常執(zhí)行天通。

創(chuàng)建goroutine(G)

在Go程序中泊窘,時(shí)常會(huì)有類似代碼:

go do_something()

go關(guān)鍵字就是用來(lái)創(chuàng)建一個(gè)goroutine的,后面的函數(shù)就是這個(gè)goroutine需要執(zhí)行的代碼邏輯。go關(guān)鍵字對(duì)應(yīng)到調(diào)度器的接口就是runtime·newproc州既。runtime·newproc干的事情很簡(jiǎn)單谜洽,就負(fù)責(zé)制造一塊磚(G),然后將這塊磚(G)放入當(dāng)前這個(gè)地鼠(M)的小車(P)中吴叶。

每個(gè)新的goroutine都需要有一個(gè)自己的棧阐虚,G結(jié)構(gòu)的sched字段維護(hù)了棧地址以及程序計(jì)數(shù)器等信息,這是最基本的調(diào)度信息蚌卤,也就是說(shuō)這個(gè)goroutine放棄cpu的時(shí)候需要保存這些信息实束,待下次重新獲得cpu的時(shí)候,需要將這些信息裝載到對(duì)應(yīng)的cpu寄存器中逊彭。

假設(shè)這個(gè)時(shí)候已經(jīng)創(chuàng)建了大量的goroutine咸灿,就輪到調(diào)度器去維護(hù)這些goroutine了。

創(chuàng)建內(nèi)核線程(M)

Go程序中沒(méi)有語(yǔ)言級(jí)的關(guān)鍵字讓你去創(chuàng)建一個(gè)內(nèi)核線程侮叮,你只能創(chuàng)建goroutine避矢,內(nèi)核線程只能由runtime根據(jù)實(shí)際情況去創(chuàng)建。runtime什么時(shí)候創(chuàng)建線程囊榜?以地鼠運(yùn)磚圖來(lái)講审胸,磚(G)太多了,地鼠(M)又太少了卸勺,實(shí)在忙不過(guò)來(lái)砂沛,剛好還有空閑的小車(P)沒(méi)有使用,那就從別處再借些地鼠(M)過(guò)來(lái)直到把小車(p)用完為止曙求。這里有一個(gè)地鼠(M)不夠用碍庵,從別處借地鼠(M)的過(guò)程,這個(gè)過(guò)程就是創(chuàng)建一個(gè)內(nèi)核線程(M)悟狱。創(chuàng)建M的接口函數(shù)是:

void newm(void (*fn)(void), P *p)

newm函數(shù)的核心行為就是調(diào)用clone系統(tǒng)調(diào)用創(chuàng)建一個(gè)內(nèi)核線程静浴,每個(gè)內(nèi)核線程的開始執(zhí)行位置都是runtime·mstart函數(shù)。參數(shù)p就是一輛空閑的小車(p)挤渐。

每個(gè)創(chuàng)建好的內(nèi)核線程都從runtime·mstart函數(shù)開始執(zhí)行了马绝,它們將用分配給自己小車去搬磚了。

調(diào)度核心

newm接口只是給新創(chuàng)建的M分配了一個(gè)空閑的P挣菲,也就是相當(dāng)于告訴借來(lái)的地鼠(M)——“接下來(lái)的日子富稻,你將使用1號(hào)小車搬磚,記住是1號(hào)小車白胀;待會(huì)自己到停車場(chǎng)拿車椭赋。”或杠,地鼠(M)去拿小車(P)這個(gè)過(guò)程就是acquirep哪怔。runtime·mstart在進(jìn)入schedule之前會(huì)給當(dāng)前M裝配上P,runtime·mstart函數(shù)中的代碼:

} else if(m != &runtime·m0) {
    acquirep(m->nextp);
    m->nextp = nil;
}
schedule();

if分支的內(nèi)容就是為當(dāng)前M裝配上P,nextp就是newm分配的空閑小車(P)认境,只是到這個(gè)時(shí)候才真正拿到手罷了胚委。沒(méi)有P,M是無(wú)法執(zhí)行g(shù)oroutine的叉信,就像地鼠沒(méi)有小車無(wú)法運(yùn)磚一樣的道理亩冬。對(duì)應(yīng)acquirep的動(dòng)作是releasep,把M裝配的P給載掉硼身;活干完了硅急,地鼠需要休息了,就把小車還到停車場(chǎng)佳遂,然后睡覺(jué)去营袜。

地鼠(M)拿到屬于自己的小車(P)后,就進(jìn)入工場(chǎng)開始干活了丑罪,也就是上面的schedule調(diào)用荚板。簡(jiǎn)化schedule的代碼如下:

static void
schedule(void)
{
    G *gp;

    gp = runqget(m->p);
    if(gp == nil)
        gp = findrunnable();

    if (m->p->runqhead != m->p->runqtail &&
        runtime·atomicload(&runtime·sched.nmspinning) == 0 &&
        runtime·atomicload(&runtime·sched.npidle) > 0)  // TODO: fast atomic
        wakep();

    execute(gp);
}

schedule函數(shù)被我簡(jiǎn)化了太多,主要是我不喜歡貼大段大段的代碼吩屹,因此只保留主干代碼了跪另。這里涉及到4大步邏輯:

  1. runqget, 地鼠(M)試圖從自己的小車(P)取出一塊磚(G),當(dāng)然結(jié)果可能失敗祟峦,也就是這個(gè)地鼠的小車已經(jīng)空了罚斗,沒(méi)有磚了徙鱼。
  2. findrunnable, 如果地鼠自己的小車中沒(méi)有磚宅楞,那也不能閑著不干活是吧,所以地鼠就會(huì)試圖跑去工場(chǎng)倉(cāng)庫(kù)取一塊磚來(lái)處理袱吆;工場(chǎng)倉(cāng)庫(kù)也可能沒(méi)磚啊厌衙,出現(xiàn)這種情況的時(shí)候,這個(gè)地鼠也沒(méi)有偷懶停下干活绞绒,而是悄悄跑出去婶希,隨機(jī)盯上一個(gè)小伙伴(地鼠),然后從它的車?yán)镌噲D偷一半磚到自己車?yán)锱詈狻H绻啻螄L試偷磚都失敗了喻杈,那說(shuō)明實(shí)在沒(méi)有磚可搬了,這個(gè)時(shí)候地鼠就會(huì)把小車還回停車場(chǎng)狰晚,然后睡覺(jué)休息了筒饰。如果地鼠睡覺(jué)了,下面的過(guò)程當(dāng)然都停止了壁晒,地鼠睡覺(jué)也就是線程sleep了瓷们。
  3. wakep, 到這個(gè)過(guò)程的時(shí)候,可憐的地鼠發(fā)現(xiàn)自己小車?yán)镉泻枚啻u啊,自己根本處理不過(guò)來(lái)谬晕;再回頭一看停車場(chǎng)居然有閑置的小車碘裕,立馬跑到宿舍一看,你妹攒钳,居然還有小伙伴在睡覺(jué)帮孔,直接給屁股一腳,“你妹夕玩,居然還在睡覺(jué)你弦,老子都快累死了,趕緊起來(lái)干活燎孟,分擔(dān)點(diǎn)工作禽作。”揩页,小伙伴醒了旷偿,拿上自己的小車,乖乖干活去了爆侣。有時(shí)候萍程,可憐的地鼠跑到宿舍卻發(fā)現(xiàn)沒(méi)有在睡覺(jué)的小伙伴,于是會(huì)很失望兔仰,最后只好向工場(chǎng)老板說(shuō)——”停車場(chǎng)還有閑置的車啊茫负,我快干不動(dòng)了,趕緊從別的工場(chǎng)借個(gè)地鼠來(lái)幫忙吧乎赴∪谭ǎ”,最后工場(chǎng)老板就搞來(lái)一個(gè)新的地鼠干活了榕吼。
  4. execute饿序,地鼠拿著磚放入火種歡快的燒練起來(lái)。
    注: “地鼠偷磚”叫work stealing羹蚣,一種調(diào)度算法原探。
schedule

到這里,貌似整個(gè)工場(chǎng)都正常的運(yùn)轉(zhuǎn)起來(lái)了顽素,無(wú)懈可擊的樣子咽弦。不對(duì),還有一個(gè)疑點(diǎn)沒(méi)解決啊胁出,假設(shè)地鼠的車?yán)镉泻芏啻u型型,它把一塊磚放入火爐中后,何時(shí)把它取出來(lái)划鸽,放入第二塊磚呢输莺?難道要一直把第一塊磚燒練好戚哎,才取出來(lái)嗎?那估計(jì)后面的磚真的是等得花兒都要謝了嫂用。這里就是要真正解決goroutine的調(diào)度型凳,上下文切換問(wèn)題。

調(diào)度點(diǎn)

當(dāng)我們翻看channel的實(shí)現(xiàn)代碼可以發(fā)現(xiàn)嘱函,對(duì)channel讀寫操作的時(shí)候會(huì)觸發(fā)調(diào)用runtime·park函數(shù)甘畅。goroutine調(diào)用park后,這個(gè)goroutine就會(huì)被設(shè)置為waiting狀態(tài)往弓,放棄cpu疏唾。被park的goroutine處于waiting狀態(tài),并且這個(gè)goroutine不在小車(P)中函似,如果不對(duì)其調(diào)用runtime·ready槐脏,它是永遠(yuǎn)不會(huì)再被執(zhí)行的。除了channel操作外撇寞,定時(shí)器中顿天,網(wǎng)絡(luò)poll等都有可能park goroutine。

除了park可以放棄cpu外蔑担,調(diào)用runtime·gosched函數(shù)也可以讓當(dāng)前goroutine放棄cpu牌废,但和park完全不同;gosched是將goroutine設(shè)置為runnable狀態(tài)啤握,然后放入到調(diào)度器全局等待隊(duì)列(也就是上面提到的工場(chǎng)倉(cāng)庫(kù)鸟缕,這下就明白為何工場(chǎng)倉(cāng)庫(kù)會(huì)有磚塊(G)了吧)。

除此之外排抬,就輪到系統(tǒng)調(diào)用了懂从,有些系統(tǒng)調(diào)用也會(huì)觸發(fā)重新調(diào)度。Go語(yǔ)言完全是自己封裝的系統(tǒng)調(diào)用畜埋,所以在封裝系統(tǒng)調(diào)用的時(shí)候莫绣,可以做不少手腳畴蒲,也就是進(jìn)入系統(tǒng)調(diào)用的時(shí)候執(zhí)行entersyscall悠鞍,退出后又執(zhí)行exitsyscall函數(shù)。 也只有封裝了entersyscall的系統(tǒng)調(diào)用才有可能觸發(fā)重新調(diào)度模燥,它將改變小車(P)的狀態(tài)為syscall咖祭。還記一開始提到的sysmon線程嗎?這個(gè)系統(tǒng)監(jiān)控線程會(huì)掃描所有的小車(P)蔫骂,發(fā)現(xiàn)一個(gè)小車(P)處于了syscall的狀態(tài)么翰,就知道這個(gè)小車(P)遇到了goroutine在做系統(tǒng)調(diào)用,于是系統(tǒng)監(jiān)控線程就會(huì)創(chuàng)建一個(gè)新的地鼠(M)去把這個(gè)處于syscall的小車給搶過(guò)來(lái)辽旋,開始干活浩嫌,這樣這個(gè)小車中的所有磚塊(G)就可以繞過(guò)之前系統(tǒng)調(diào)用的等待了檐迟。被搶走小車的地鼠等系統(tǒng)調(diào)用返回后,發(fā)現(xiàn)自己的車沒(méi)码耐,不能繼續(xù)干活了追迟,于是只能把執(zhí)行系統(tǒng)調(diào)用的goroutine放回到工場(chǎng)倉(cāng)庫(kù),自己睡覺(jué)去了骚腥。

從goroutine的調(diào)度點(diǎn)可以看出敦间,調(diào)度器還是挺粗暴的,調(diào)度粒度有點(diǎn)過(guò)大束铭,公平性也沒(méi)有想想的那么好廓块。總之契沫,這個(gè)調(diào)度器還是比較簡(jiǎn)單的带猴。

現(xiàn)場(chǎng)處理

goroutine在cpu上換入換出,不斷上下文切換的時(shí)候懈万,必須要保證的事情就是保存現(xiàn)場(chǎng)和恢復(fù)現(xiàn)場(chǎng)浓利,保存現(xiàn)場(chǎng)就是在goroutine放棄cpu的時(shí)候,將相關(guān)寄存器的值給保存到內(nèi)存中钞速;恢復(fù)現(xiàn)場(chǎng)就是在goroutine重新獲得cpu的時(shí)候贷掖,需要從內(nèi)存把之前的寄存器信息全部放回到相應(yīng)寄存器中去。

goroutine在主動(dòng)放棄cpu的時(shí)候(park/gosched)渴语,都會(huì)涉及到調(diào)用runtime·mcall函數(shù)苹威,此函數(shù)也是匯編實(shí)現(xiàn),主要將goroutine的棧地址和程序計(jì)數(shù)器保存到G結(jié)構(gòu)的sched字段中驾凶,mcall就完成了現(xiàn)場(chǎng)保存牙甫。恢復(fù)現(xiàn)場(chǎng)的函數(shù)是runtime·gogocall调违,這個(gè)函數(shù)主要在execute中調(diào)用窟哺,就是在執(zhí)行g(shù)oroutine前,需要重新裝載相應(yīng)的寄存器技肩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末且轨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子虚婿,更是在濱河造成了極大的恐慌旋奢,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件然痊,死亡現(xiàn)場(chǎng)離奇詭異至朗,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)剧浸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門锹引,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)矗钟,“玉大人,你說(shuō)我怎么就攤上這事嫌变≌嬷伲” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵初澎,是天一觀的道長(zhǎng)秸应。 經(jīng)常有香客問(wèn)我,道長(zhǎng)碑宴,這世上最難降的妖魔是什么软啼? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮延柠,結(jié)果婚禮上祸挪,老公的妹妹穿的比我還像新娘。我一直安慰自己贞间,他們只是感情好贿条,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著增热,像睡著了一般整以。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上峻仇,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天公黑,我揣著相機(jī)與錄音,去河邊找鬼摄咆。 笑死凡蚜,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吭从。 我是一名探鬼主播朝蜘,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼涩金!你這毒婦竟也來(lái)了谱醇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鸭廷,失蹤者是張志新(化名)和其女友劉穎枣抱,沒(méi)想到半個(gè)月后熔吗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辆床,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年桅狠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了讼载。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片轿秧。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖咨堤,靈堂內(nèi)的尸體忽然破棺而出菇篡,到底是詐尸還是另有隱情,我是刑警寧澤一喘,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布驱还,位于F島的核電站,受9級(jí)特大地震影響凸克,放射性物質(zhì)發(fā)生泄漏议蟆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一萎战、第九天 我趴在偏房一處隱蔽的房頂上張望咐容。 院中可真熱鬧,春花似錦蚂维、人聲如沸戳粒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蔚约。三九已至,卻和暖如春涂籽,著一層夾襖步出監(jiān)牢的瞬間炊琉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工又活, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苔咪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓柳骄,卻偏偏與公主長(zhǎng)得像团赏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子耐薯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354