91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何解析Linux 驅動架構

發布時間:2022-01-21 11:01:01 來源:億速云 閱讀:119 作者:柒染 欄目:開發技術

今天就跟大家聊聊有關如何解析Linux 驅動架構,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。

  1. 首先,需要熟悉操作系統的設計與實現,推薦大家看 MINIX作者的那部書,同時把MINIX的kernel代碼研讀一下。 不然,你不知道操作系統都有哪些模塊, 不知道操作系統要做些什么事情,提供什么功能。簡單地說,操作系統首先要驅動 CPU,然后提供那幾大管理(進程,內存,文件),實現一兩百個系統呼叫,提供驅動接口, 用戶態與內核之間進行切換。

  2. 去intel的官網,找一下 ‘Intel® 64 and IA-32 Architectures Software Developer’s Manual’ , 了解一下 CPU的架構,工作模式,底層編碼。否則, 你不知道 gdt,ldt,page table,實地址,保護模式,定時器中斷都是什么東西,為什么操作系統要這樣來設置寄存器。這塊基本上全匯編語言,對CPU的初始化,寄存器設置,手冊上面都有嚴格的時需要求。 哪些操作需要屏蔽中斷,哪些需要在一個指令周期完成等等。有了上面的基礎后,大概知道一個操作系統大概要做些什么事情, 如何驅動底層的 CPU,這個時候閱讀 linux的kernel代碼,事半功倍。

kernel分為兩個模塊:一個是core: cpu, 中斷,進程,內存幾大管理, 提供系統呼叫。另一個是driver。 linux的driver 都是有架構的,不需要從底層做起。各類架構稱為‘子系統’:如,block子系統,net子系統,usb子系統等。 別看操作系統的代碼量大,其實,driver占了估計 80%的代碼量, 這些都是不需要去看的。驅動是否放在內核,就是微內核與宏內核的區別。

閱讀源碼過程中,觀其大略即可,主要了解整個結構,以及程序的流程。如:系統呼叫的調用,追一個就可以了—— 看看操作系統如何捕捉軟中斷, 根據中斷號,dispatch到相應的服務程序,如何保存現場, 完成后,又如何回到用戶態。 系統呼叫調用,核心就是 dispatch的流程。 追完一支系統呼叫,其它的大概就知道怎么回事了。driver 也就一樣的, 找個簡單的驅動看看, 從驅動層一直到驅動的架構,流程清楚就可以了。 如字符設備驅動, 追一下注冊后, 驅動框架如何把該設備放入 list,當有用戶請求的時候,它又如何查找到相應的設備,調用相應的操作函數, 一路追下來,流程大概就清楚了。

不建議一開始就閱讀“linux內核源碼分析” 之類的書, 會讓讀者一頭霧水。 正確的方法應該是, 先了解相應的背景知識后,再來閱讀源碼。Driver 框架的源碼的位于 drivers/base/, 它是整個驅動模式的基礎框架,相當于OO語言的里面的Object對象。 其實,Linux 的驅動框架就是一個OO的結構, core模塊定義數據結構,函數接口,實現各種通用的功能——相當于OO里面的基類。各模塊的設備驅動程序則只需要實現 core模塊里面定義的接口即可。

bus, driver, device 框架

linux的外圍設備驅動,都是通過 bus + driver + device來管理的,其實也好理解 ,外設都是通過總線來與cpu通訊的。kernel會實現各種總線的規范以及設備管理(設備檢測,驅動綁定等),驅動程序只需要注冊自己的驅動,實現對設備的讀寫控制即可。

這類驅動通常是2個層次:總線子系統 + 驅動模塊,它的流程大概是:

  1. bus_register(xx)

kernel里面的各bus子系統(如:serio, usb, pci, …)會使用該函數來注冊自己。

  1. driver_register(xx)

驅動模塊使用它來向總線系統注冊自己,這樣驅動模塊只需要關注相應driver接口的實現。通常,bus子系統會對 driver_register來進行封裝,如:

  • serio 提供serio_register_driver()
  • usb 提供usb_register_driver()
  • pci提供 pci_register_driver()
  1. registe_device(xx)

各總線除了管理driver外,還管理device,通常會提供一支API來添加設備,如: input_register_device, serio_add_port.實現上都是通過一個鏈表對設備進行管理,通常是在初始化或者probe的時候, 添加設備。

設備(device)指的是具體實現總線協議的物理設備,如對serio總線而言,i8042就是它的一個設備,而該總線連接的設備(鼠標,鍵盤)則是一個serio driver。

注冊

bus.c 和 driver.c 分別對 bus,driver和device進行管理,提供注冊bus, driver和查找 device 功能。

bus_register(*bus) 這個函數會生成兩個list,用來保存設備和驅動。

INIT_LIST_HEAD(&priv->interfaces);
klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
klist_init(&priv->klist_drivers, NULL, NULL);

* priv是 struct subsys_private定義在 driver/base/base.h

driver_register(*drv) 實際上就是調用 bus_add_driver(*drv) 把 drv 添加到 klist_drivers:

klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);

同理注冊device,也是通過 bus_add_device(*dev),添加到 klist_devices:

klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices);

以 hid_bus_type為例,執行 bus_register(&hid_bus_type) 后, hid_bus_type->p->klist_devices 和 hid_bus_type->p->klist_klist_drivers 這兩個list 會被初始化,為后面的 driver和 device 注冊做準備,driver數據結構如下:

       static struct hid_driver tpkbd_driver = {
        .name = "lenovo_tpkbd",
        .id_table = tpkbd_devices,
        .input_mapping = tpkbd_input_mapping,
        .probe = tpkbd_probe,
        .remove = tpkbd_remove,
    };

注冊driver時,它先經過 __hid_register_driver(&tpkbd_driver),設置一些基本參數。

hdrv->driver.bus = &hid_bus_type;
.....    
driver_register(&hdrv->driver);

設置’driver.bus’字段后,driver和bus的對應關系就建立起來了。 然后, 經過 driver_register 后,hid_bus_type->p->list_drivers 保存了 tpkbd_driver.

Q: driver模塊是不知道 hid_driver 這個數據結構的,它如何能把它的指針放到list里面呢?

答案是”不能”, list_drivers 是不能保存 hid_driver 指針的。driver模塊提供了一個接口: ‘struct device_driver’ , hid_driver 這個結構里面需要包含該結構。

      struct hid_driver {
              const struct hid_device_id *id_table;
                /* private: */  
                 struct device_driver driver;
      }

注冊的時候,取的是 driver 字段的地址,也就是 hid_driver.driver 的指針, driver_register(&hdrv->driver); 當從 driver模塊 callback 到 hid-core模塊的時候, 如

 static int hid_bus_match(struct device *dev, struct device_driver *drv)
 {
         struct hid_driver *hdrv = container_of(drv, struct hid_driver, driver);
         struct hid_device *hdev = container_of(dev, struct hid_device, dev);

         return hid_match_device(hdev, hdrv) != NULL;
 }

使用 container_of 就把 hid_driver.driver 的指針轉換成了hid_driver 的指針--這個方法類似 OO編程里面使用基類指針指向派生類對象。Linux普通使用這個方法,來構建框架。

device和driver綁定

當增加新device的時候,bus 會輪循它的驅動列表來找到一個匹配的驅動,它們是通過device id和 driver的id_table來進行 ”匹配”的,主要是在 driver_match_device()[drivers/base/base.h] 通過 bus->match() 這個callback來讓驅動判斷是否支持該設備,一旦匹配成功,device的driver字段會被設置成相應的driver指針 :

really_probe()
{
    dev->driver = drv;
    if (dev->bus->probe) {
        ret = dev->bus->probe(dev);
        ...
    } else if (drv->probe) {
        ret = drv->probe(dev);
        ...
    }
}

然后 callback 該 driver 的 probe 或者 connect 函數,進行一些初始化操作。

同理,當增加新的driver時,bus也會執行相同的動作,為驅動查找設備。因此,綁定發生在兩個階段:

1: 驅動找設備,發生在driver向bus系統注冊自己時候,函數調用鏈是:

driver_register –> bus_add_driver –> driver_attach() [dd.c] -- 將輪循device鏈表,查找匹配的device。

2: 設備查找驅動,發生在設備增加到總線的的時候,函數調用鏈是:

device_add –> bus_probe_device –> device_initial_probe –> device_attach -- 將輪循driver鏈表,查找匹配的driver。

匹配成功后,系統繼續調用 driver_probe_device() 來 callback ‘drv->probe(dev)’ 或者 ‘bus->probe(dev) –>drv->connect(),在probe或者connect函數里面,驅動開始實際的初始化操作。因此,probe() 或者 connect() 是真正的驅動’入口’。

對驅動開發者而言,最基本是兩個步驟:

  • 定義device id table.
  • probe()或connect()開始具體的初始化工作。
如何解析Linux 驅動架構

(driver和device注冊流程圖)

實例分析:atkbd鍵盤驅動

Serio Bus 主要是支持 PS/2,串口等串行設備協議,物理上可以通過i8042控制芯片來連接PS/2的鼠標或鍵盤,它的架構是:

  • serio.c 實現總線框架。
  • serio_register_port 注冊底層讀寫設備-- port就是 serio的底層通訊設備, 它執行serio總線的底層讀寫。
  • serio_register_driver 注冊驅動,與port進行綁定,利用port進行底層的讀寫通訊。

atkbd 驅動注冊的時侯,需指定它支持的port類型。

serio->id.type        = SERIO_8042; //表明驅動需要8042的支持。

serio_register_driver()     // 注冊自己, 根據設備id綁定相應的 port(本例中是8042)。 

作為serio的port, i8042 通過 serio_register_port 來注冊,生成serio對象,這樣驅動程序就可以通過 serio->wirte/read 來調用i8042進行底層的通訊。 數據流程框圖如下:

如何解析Linux 驅動架構

driver 通過 bus 匹配 port,通過port與外設通訊。

我們可以在sys接口 [/sys/bus/serio/] 目錄下找到設備和驅動的相關信息,里面的內容可以通過 DEVICE_ATTR_XX 系列宏定義來添加。 port 的命令規則是serio0, serio1, serioN 是自動增加的。

dev_set_name(&serio->dev, "serio%ld", (long)atomic_inc_return(&serio_no) - 1);

atkbd注冊serio驅動后,還需要注冊input設備,它需要實現input子系統的接口,作為一個input設備工作。input.c 定義了callback 和公用接口,子模塊實現相應接口。

keybord:  drivers/input/keyboard/atkbd.c

注冊設備
atkbd.c: atkbd_connect  → input_register_device()

Input子系統通過event 字符設備來與應用程序進行通訊。

evdev.c: evdev_init → input_register_handler → ....
cdev_init(xx, fops) // 處理  /dev/input/eventX的讀寫操作。

atkdb通過注冊Serio Bus 驅動和Input device來打通從應用程序到外設通訊鏈路:

application –> /dev/input/eventX –> Input 子系統 –> atkdb驅動 –> serio bus –> i8042 port –> 物理鍵盤。

我們可以看到字符設備主要是用來提供應用層接口,Bus框架則用來管理外設驅動。

Input提供了proc文件接口,可以查看相應的信息。

cat /proc/bus/input/devices : 可以得到某個設備的event number.
cat /proc/bus/input/handlers

通過cat eventX可以得到按鍵產生input_event,查看 event里面的原始數據

sudo cat /dev/input/eventXX | hexdump
XX: event number.

USB

以usb serial的代碼為例來說明一下usb總線驅動的基本工作流程。

注冊

    struct usb_driver *udriver = kzalloc(sizeof(*udriver), GFP_KERNEL);
    udriver->name = "usb_ftdi";
    udriver->probe = usb_ftdi_probe;
    rc = usb_register(udriver);
    udriver->id_table = id_table;
    rc = driver_attach(&udriver->drvwrap.driver);

先創建一個udriver --

  • probe 函數用來做硬件初始化
  • id_table用來標識驅動支持的芯片類型,用于匹配外設。
  • usb_register 注冊到usb總線
  • driver_attach 插入驅動。

插入驅動后,將會與usb的底層設備(hub)進行綁定。hub在初始化的時侯,會開啟一個任務來檢測端口的變化,并使用缺省的”endpoint”來枚舉外設,得到它的“接口描述符”,與驅動綁定后,會把該外設的信息通過 probe函數回傳給驅動,通過id比對后,找到對應的外設驅動,在_usb_ftdi_probe_ 函數進行具體的驅動初始化。

通訊

USB設備框架是 device –> interface –> endpoint。如某個設備擁有音頻和存儲兩個功能,那么它有”音頻接口“和“存儲接口”,每個接口又包含幾個通訊“端點”,用來同主機通訊。其中“endpoint 0” 是缺省的通訊端點,主機通過它來讀取設備的各種信息--在usb的規范里稱為“描述符”,如:設備描述符,接口描述符,端點描述符,每個設備都要提供一套usb的“標志描述符”供主機來枚舉它。

_usb_ftdi_probe_首先分析“接口描述符”,得到它的端口信息--

for (i = 0; i desc.bNumEndpoints; ++i) {
    epd = &iface_desc->endpoint[i].desc;
    if (usb_endpoint_is_bulk_in(epd)) {
            ...
    } else if (usb_endpoint_is_bulk_out(epd)) {
            ...
    } else if (usb_endpoint_is_int_in(epd)) {
            ...
    } else if (usb_endpoint_is_int_out(epd)) {
            ...
    }
}

usb共有4類“端點”--

  1. bulk : 用于大量的數據傳輸,如:U盤。
  2. Control : 控制信息傳輸。
  3. Interrupt : 低頻低延時數據傳輸。
  4. Isochronal: 周期性數據傳輸。

驅動需要在probe函數分析“端點描述符”來創建相應的端點,每個“端點”都有IN/OUT兩個方向--

  • IN : Host 讀取 設備數據。
  • OUT : Host 寫數據到設備。

控制管道

endpoint 0是usb協議的標準控制端點,主機通過這個端點來枚舉設備,讀取它的信息。usb_control_msg 這個函數通過”控制管道”來讀寫外設的’寄存器’,核心參數是 “Request”, “Request Type”,”wWalue”和”wIndex”。具體參數值需要向設備制造查詢,如果是通用設備可以直接查詢相應的規范。

以FTDI_SIO_GET_MODEM_STATUS 為例:

/*
 *   BmRequestType:   1100 0000b
 *   bRequest:       5
 *   wValue:          zero
 *   wIndex:          Port
 *   wLength:         1
 *   Data:            Status
 */
    usb_control_msg(dev,  usb_rcvctrlpipe(dev, 0),
                    5, 0xC0, 0, priv->port,
                    buf, 1, WDR_TIMEOUT);

根據設備的定義,我們得到的參數如下:

  • request -- 6
  • requesttype -- 0xC0
  • wValue -- 0
  • wIndex -- Port (在設備的描述符里找回)

usb_rcvctrlpipe(dev, 0),創建一個讀的“管道”,使用的是默認的控制端點0。

下面是一個設置”波特率”的例子:

#define FTDI_SIO_SET_BAUDRATE_REQUEST_TYPE 0x40
#define FTDI_SIO_SET_BAUDRATE_REQUEST 3
/*
 * BmRequestType:  0100 0000B
 * bRequest:       FTDI_SIO_SET_BAUDRATE
 * wValue:         BaudDivisor value - see below
 * wIndex:         Port
 * wLength:        0
 * Data:           None
*/

具體調用如下:

    usb_control_msg(port->serial->dev,
                usb_sndctrlpipe(port->serial->dev, 0),
                FTDI_SIO_SET_BAUDRATE_REQUEST,
                FTDI_SIO_SET_BAUDRATE_REQUEST_TYPE,
                1, port,
                NULL, 0, WDR_SHORT_TIMEOUT);

usb_sndctrlpipe(dev, 0),創建默認(端點0)的寫控制“管道”,由于只是改命令無 附加數據,buf參數是’NULL’,長度是’0’,其它的核心參數,根據協議規范寫入即可。

Bulk 管道

bulk 用于大量的數據傳輸,數據封裝在urb里,urb 全稱為”USB Request Block” ,類似于網絡的IP包,它由Host調度,通過層次的hub傳遞到設備。USB2是主機對從機的單向通訊,讀寫數據都由主機來發動。

Host寫數據到設備

usb_fill_bulk_urb(port->write_urbs[i], udev,
            usb_sndbulkpipe(udev, epd->bEndpointAddress),
            port->bulk_out_buffers[i], buffer_size,
            write_bulk_callback, port);

usb_submit_urb(urb, mem_flags);  // 啟動

void  write_bulk_callback(struct urb *urb) {
    
    count = port->serial->type->prepare_write_buffer(
        port, urb->transfer_buffer, port->bulk_out_size);
    urb->transfer_buffer_length = count;

    result = usb_submit_urb(urb, mem_flags);
}

如上圖的示例代碼,它是一個連續傳輸數據的流程,

首先,usb_fill_bulk_urb --

  • usb_sndbulkpipe 定義一個主機輸出的管道。
  • port->bulk_out_buffers[i]和buffer_size 提供數據緩沖和大小。
  • write_bulk_callback, urb傳輸完成后的callback。

其次, usb_submit_urb -- 啟動”寫數據”,提交urb到主機去調度。

最后,定義 write_bulk_callback 函數 --

  • 更新數據緩沖區。
  • 再次usb_submit_urb ,輪循調用這個callback,直到數據傳輸完畢,如果數據可以一次性傳輸完成的,就不需要定義這個 callback了。

Host 從設備讀取數據

usb_fill_bulk_urb(port->read_urbs[i], udev,
            usb_rcvbulkpipe(udev, epd->bEndpointAddress),
            port->bulk_in_buffers[i], buffer_size,
            type->read_bulk_callback, port);

usb_submit_urb(port->read_urbs[index], mem_flags); // 啟動


static void read_bulk_callback(struct urb *urb) {
    for (i = 0; i actual_length; i++) 
        printk(KERN_CONT " 0x%x", urb->transfer_buffer[i]);
     usb_submit_urb(port->read_urbs[urbinx], GFP_ATOMIC);
}

上面的代碼則是一個連續讀取數據的流程,

首先,usb_fill_bulk_urb --

  • usb_rcvbulkpipe 定義一個主機輸出的管道。
  • port->bulk_in_buffers[i]和buffer_size 提供數據緩沖和大小。
  • read_bulk_callback, urb傳輸完成后的callback。

其次, usb_submit_urb -- 啟動”讀數據”,提交urb到主機去調度。

最后,定義 read_bulk_callback 函數 --

  • 提取緩沖區的數據。
  • 再次usb_submit_urb ,輪循調用這個callback,直到數據傳輸完畢,如果不需要再讀取數據,就不要再提交 urb。

從上面例子可以看出,bulk的讀寫流程都是先創建urb,然后使用 _usb_submit_urb_來啟動讀寫,在callback里面處理數據,如果需要繼續讀寫,則在callback函數里,再次調用 usb_submit_urb

Interrupt 管道

通訊流程同bulk,只是 usb_fill_int_urb 多了一個 ‘interval’ 參數,用來設置它的調度時間,可以進行一些時序控制。

Isochronal 管道

用來傳輸一定速率的數據,如:音視頻流。速率通過 ‘interval’ 參數來設置,驅動通過循環檢測 URB的狀態,來持續寫入/讀取數據。

塊設備

塊設備指的是存儲設備,塊設備驅動就是存儲驅動如:HD,SSD。它不同于外設驅動,Linux 用 Block 子系統對它們進行管理,把應用層的IO讀寫請求,轉變為Request ,傳給相應的塊設備驅動。

那么文件系統與Block子系統是什么關系呢?

Block子系統主要是提供最底層的數據讀寫,也就是raw io,文件系統使用它進行IO操作,文件系統只專注于文件系統格式,以讀取分區為例: 文件系統在初始化階段,會call blkdev_get, 如果塊設備還沒有初始化,blkdev_get 則 調用塊設備的 rescan_partitions, 去掃描分區,初始化設備。

如何解析Linux 驅動架構

注冊

#define FR_MAJOR  310
#define DEVICE_NAME "fooram"
注冊塊設備(主設備號)
register_blkdev(FR_MAJOR, DEVICE_NAME);
unregister_blkdev(FR_MAJOR,DEVICE_NAME);

注冊設備(MAJOR, MINOR)
blk_register_region(MKDEV(FR_MAJOR, 0),1, ....);
blk_unregister_region(MKDEV(FR_MAJOR, 0),1, ...);

添加磁盤

dev->gd = alloc_disk(1);
dev->queue =blk_mq_init_queue(&dev→tag_set);  //初始化請求隊列
dev->gd->major = FR_MAJOR;
dev->gd->first_minor = 0;
dev->gd->fops = &fr_fops;
dev->gd->queue = dev->queue;
dev->gd->private_data = dev;
sprintf (dev->gd->disk_name, "frd0");
set_capacity(dev->gd, size);
add_disk(dev->gd);

初始化請求隊列

struct blk_mq_tag_set  tag_set;

dev->tag_set.ops = &fr_mq_ops;
dev->tag_set.nr_hw_queues = 1;
dev->tag_set.queue_depth = 16;
dev->tag_set.numa_node = NUMA_NO_NODE;
dev->tag_set.flags = BLK_MQ_F_SHOULD_MERGE;
dev->tag_set.driver_data = dev;
err = blk_mq_alloc_tag_set(&dev->tag_set);
dev->queue = blk_mq_init_queue(&dev→tag_set);

static struct blk_mq_ops fr_mq_ops = {
    .queue_rq       =  fr_queue_rq,
    .map_queue   =  blk_mq_map_queue,
};

初始化請求隊列是添加磁盤的核心操作,塊設備驅動的核心就是圍繞著“請求隊列”來展開,它的核心任務就是優化這個隊列讀寫請求。當_add_disk_把磁盤加入系統后,請求隊列就隨時可以調度了。

處理設備請求

*BIO 數據結構*

如何解析Linux 驅動架構

*Request Queue*

如何解析Linux 驅動架構

請求隊列的每個”請求”(struct request)都包含著一個bio隊列,bio結構里面就是設備的尋址(扇區,長度)和對應的內存空間。 “讀”請求就是把設備對應的扇區寫到相應的內存空間,“寫”請求則剛好相反,把內存的數據寫入相應的扇區。最簡單的驅動就是用一個循環,依次處理各個請求,進行底層的IO讀寫,如:

*Loop處理請求 (fr_queue_rq)*

rq_for_each_segment (bvec, rq, iter) {
       char *buffer = kmap_atomic(bvec.bv_page) + bvec.bv_offset;
        unsigned nsecs = bvec.bv_len >> 9;
        fr_transfer(dev, sector, nsecs, buffer, write);
        sector += nsecs;
        kunmap_atomic(buffer);
 }

設備讀寫數據(fr_transfer)

這個函數就是具體存儲設備的底層讀寫操作,PCI設備可以用_PCI_WRITE來寫數據,USB設備也可以使用 BULK管道來傳輸數據。下面的示例代碼沒有物理設備是一個ram disk,簡單地用memcpy_ 來傳輸數據。

    loff_t  pos = sector     loff_t  len = nsecs 
    //底層IO 如,PCI_WRITE(IOBase, register add,);
    if (write)
        memcpy(dev->data + pos, buffer, len);
    else
        memcpy(buffer, dev->data + pos, len);

看完上述內容,你們對如何解析Linux 驅動架構有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注億速云行業資訊頻道,感謝大家的支持。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

邢台市| 松溪县| 七台河市| 鄂托克旗| 萍乡市| 广饶县| 永泰县| 洛宁县| 永修县| 观塘区| 黄平县| 慈利县| 阜阳市| 荔波县| 婺源县| 安新县| 新津县| 文水县| 桐柏县| 昌江| 奉新县| 化州市| 外汇| 赫章县| 沁源县| 池州市| 东阳市| 潮安县| 萨迦县| 灵台县| 黄平县| 毕节市| 荔浦县| 庆云县| 闽侯县| 西峡县| 抚宁县| 息烽县| 乐平市| 开鲁县| 翁源县|