Linux的GPIO驅(qū)動(dòng)程序使用詳解
讓我們看看 Linux 中 GPIO 驅(qū)動(dòng)程序的具體結(jié)構(gòu),以及為什么要這樣做。這樣我們就能理解為什么在這個(gè)操作系統(tǒng)中,僅僅為了讓 LED 閃爍就需要經(jīng)過 N 層抽象。
上述內(nèi)容大部分也適用于其他驅(qū)動(dòng)程序。GPIO 被選為最簡(jiǎn)單的外圍設(shè)備示例。
我們的文章結(jié)構(gòu)如下:
- 我們先來看看硬件層面的GPIO控制;
- 我們來研究一下操作系統(tǒng)給我們?cè)黾恿四男┮螅?/li>
- 讓我們考慮一下這些要求通常是如何實(shí)現(xiàn)的;
- 讓我們看看 Linux 中如何為不同的 SoC 實(shí)現(xiàn)真正的 GPIO 驅(qū)動(dòng)程序;
- 讓我們?cè)俅魏?jiǎn)單總結(jié)一下研究結(jié)果。
硬件級(jí)別
在硬件層面,一切都非常簡(jiǎn)單。處理器有一個(gè) GPIO 引腳控制器,從程序員的角度來看,它就是映射到通用地址空間的十幾個(gè)寄存器。
要點(diǎn)亮 LED,我們首先需要初始化所需的引腳(將其配置為輸出),然后設(shè)置所需的值。
基本上,寄存器中有兩個(gè)條目。一個(gè)條目位于確定輸出方向的寄存器中。第二個(gè)條目位于確定輸出值的寄存器中。這可以用十幾條 CPU 指令完成。
操作系統(tǒng)限制
當(dāng)我們開始在操作系統(tǒng)中實(shí)現(xiàn)相同的操作時(shí),我們會(huì)產(chǎn)生額外的需求。
這些需求會(huì)導(dǎo)致額外的代碼,但與此同時(shí),這些需求的實(shí)現(xiàn)正是確保操作系統(tǒng)便捷運(yùn)行的關(guān)鍵因素。
從內(nèi)核模式和用戶模式訪問外設(shè)
在用戶空間運(yùn)行的應(yīng)用程序無法訪問外設(shè)寄存器和其他服務(wù)內(nèi)存區(qū)域。
這是像 Linux 這樣的操作系統(tǒng)的基本要求之一——用戶進(jìn)程不應(yīng)該破壞其他進(jìn)程的內(nèi)存,尤其是內(nèi)核內(nèi)存。
這意味著需要一個(gè)在內(nèi)核空間運(yùn)行并為用戶模式應(yīng)用程序提供 API 的驅(qū)動(dòng)程序。
GPIO 引腳必須由內(nèi)核控制(例如,來自鄰近的驅(qū)動(dòng)程序),也就是說,內(nèi)核模式也需要一個(gè) API。
訪問共享
多個(gè)用戶空間進(jìn)程可能想要管理同一個(gè)輸出。必須提供一種機(jī)制來防止多個(gè)進(jìn)程同時(shí)訪問該輸出時(shí)出現(xiàn)錯(cuò)誤。
多個(gè)內(nèi)核空間進(jìn)程可能想要使用相同的輸出。這些情況也需要處理,但需要排除錯(cuò)誤。
標(biāo)準(zhǔn) API
許多 SoC 中集成了多種 GPIO 端口實(shí)現(xiàn)。無論具體實(shí)現(xiàn)如何,驅(qū)動(dòng)程序都必須提供相同的一組“旋鈕”(換句話說,相同的 API)。原因如下:
- 用戶空間應(yīng)用程序獲得了額外的平臺(tái) 獨(dú)立性——在任何 Linux 平臺(tái)上,GPIO 引腳都以相同的方式控制;
- 對(duì)于在工作中使用 GPIO 引腳的驅(qū)動(dòng)程序,也賦予了類似的自由度(例如,觸摸控制器或 Wi-Fi 模塊的驅(qū)動(dòng)程序可以使用 GPIO 引腳進(jìn)行電源管理)。
此外,大多數(shù) SoC 可以使用 GPIO 引腳接收外部中斷。Linux 也解決了這個(gè)問題,但 Linux 中的中斷有其自身的抽象,值得單獨(dú)研究。因此,雖然我們?cè)谠u(píng)測(cè)過程中會(huì)遇到處理中斷的代碼,但本文不會(huì)討論它。
實(shí)現(xiàn)通用 GPIO 驅(qū)動(dòng)程序要求
從內(nèi)核和用戶空間訪問外設(shè)
一切都很簡(jiǎn)單。驅(qū)動(dòng)程序是一個(gè)常規(guī)的 Linux 內(nèi)核模塊,因此它在內(nèi)核空間中執(zhí)行,并且可以訪問 CPU 的整個(gè)地址空間。
對(duì)于用戶級(jí)應(yīng)用程序,sysfs中提供了特殊文件。
自定義應(yīng)用程序的 API
這是 Linux 系統(tǒng)的標(biāo)準(zhǔn)機(jī)制 - 通過文件/sys/class/gpio。
當(dāng)我們找到系統(tǒng)中所需的輸出編號(hào)(它通常與物理輸出的編號(hào)不一致)時(shí),我們將該編號(hào)寫入文件/sys/class/gpio/export:
num=416 echo $num > /sys/class/gpio/export
此后,文件系統(tǒng)中會(huì)出現(xiàn)一個(gè)目錄/sys/class/gpio/gpio$num。該目錄下有文件direction和value。
控制輸出方向:
echo out > /sys/class/gpio/gpio$num/direction echo in > /sys/class/gpio/gpio$num/direction
控制輸出狀態(tài):
echo 1 > /sys/class/gpio/gpio$num/value echo 0 > /sys/class/gpio/gpio$num/value
讀取輸入狀態(tài):
cat /sys/class/gpio/gpio$num/value
如何實(shí)現(xiàn)這一點(diǎn),我們稍后會(huì)考慮。
用戶應(yīng)用程序的訪問分離
如果多個(gè)應(yīng)用程序訪問同一個(gè)“句柄”,系統(tǒng)將自行解決沖突。就像多個(gè)應(yīng)用程序同時(shí)訪問同一個(gè)文件時(shí)一樣。這些沖突不需要在驅(qū)動(dòng)程序?qū)用孢M(jìn)行處理。
內(nèi)核 API 和內(nèi)核訪問共享
本節(jié)主要介紹 Linux 內(nèi)核中與平臺(tái)無關(guān)的代碼。代碼以存儲(chǔ)庫https://github.com/torvalds/linux的主分支為例,即本文發(fā)布時(shí)對(duì)應(yīng)的內(nèi)核版本 6.x。具體的驅(qū)動(dòng)程序?qū)崿F(xiàn)將以內(nèi)核 4.x 和 5.x 為例進(jìn)行進(jìn)一步討論。內(nèi)核版本之間存在差異,但總體概念沒有變化。
Linux 內(nèi)核有一個(gè)獨(dú)立于平臺(tái)的抽象結(jié)構(gòu)體 gpio_chip,它是一個(gè)包含一組特定 GPIO 引腳描述的結(jié)構(gòu)體。沒有單獨(dú)的結(jié)構(gòu)體來描述單個(gè)引腳并包含管理該引腳的函數(shù)。
題外話,這與硬件層面的關(guān)系如何。
- 在硬件層面,芯片(SoC - 片上系統(tǒng))通常具有多個(gè) GPIO 塊,每個(gè)塊包含數(shù)十個(gè)引腳。通常使用諸如 GPIOA、GPIOB 等名稱。因此,引腳的指定格式為 GPIOA_31..GPIOA_0 等。
- 根據(jù)芯片和驅(qū)動(dòng)程序的具體實(shí)現(xiàn),可以實(shí)現(xiàn)不同的方法??梢詾槊總€(gè)塊啟動(dòng)一個(gè)單獨(dú)的 gpio_chip 結(jié)構(gòu)。但也可以實(shí)現(xiàn)另一種方法。
- 例如,可以按電源域劃分 GPIO 塊。通常,GPIOA..GPIOC 位于一個(gè)域中,而 GPIOD..GPIOF 位于另一個(gè)域中。
- 在這種情況下,供應(yīng)商在提供的 SDK 中為一個(gè)域的塊形成一個(gè) gpio_chip,為另一個(gè)域的塊形成第二個(gè) gpio_chip。
總的來說,從技術(shù)角度來看,gpio_chip 的分區(qū)并不重要。基本上,這里的一切都取決于特定驅(qū)動(dòng)程序作者的審美。
無論如何,在 gpio_chip 結(jié)構(gòu)中,除其他內(nèi)容外,還有指向特定 GPIO 塊的驅(qū)動(dòng)程序中實(shí)現(xiàn)的平臺(tái)相關(guān)函數(shù)的指針:
int (*request)(struct gpio_chip *chip, unsigned offset); void (*free)(struct gpio_chip *chip, unsigned offset); int (*get_direction)(struct gpio_chip *chip, unsigned offset); int (*direction_input)(struct gpio_chip *chip, unsigned offset); int (*direction_output)(struct gpio_chip *chip, unsigned offset, int value); … void (*set)(struct gpio_chip *chip, unsigned offset, int value);
除了 gpio_chip 結(jié)構(gòu)體之外,內(nèi)核還提供了與平臺(tái)無關(guān)的 gpio 函數(shù)。它們分別位于 drivers/gpio/gliolib.c 和 drivers/gpio/gpiolib_sysfs.c 文件中。我們來看一下這兩個(gè)文件。
第一個(gè)包含內(nèi)核函數(shù)。有很多函數(shù),我們重點(diǎn)介紹兩個(gè):
int gpiod_request(struct gpio_desc *desc, const char *label); void gpiod_set_value(struct gpio_desc *desc, int value);
這是允許我們占用所需 GPIO 并設(shè)置其值(如果將其配置為輸出)的最小集合。其余函數(shù)基于相同的原則構(gòu)建,對(duì)它們的額外考慮不會(huì)給我們帶來任何根本性的新東西。
訪問共享機(jī)制是在 gpiod_request() 函數(shù)內(nèi)部實(shí)現(xiàn)的。當(dāng)然,它不是直接在函數(shù)內(nèi)部實(shí)現(xiàn)的,而是按照慣例,通過函數(shù)“下方”的幾個(gè)層級(jí)來實(shí)現(xiàn)的:
int gpiod_request(struct gpio_desc *desc, const char *label)
{
int ret = -EPROBE_DEFER;
VALIDATE_DESC(desc);
if (try_module_get(desc->gdev->owner)) {
ret = gpiod_request_commit(desc, label);
if (ret)
module_put(desc->gdev->owner);
else
gpio_device_get(desc->gdev);
}
if (ret)
gpiod_dbg(desc, "%s: status %d\n", __func__, ret);
return ret;
}static int gpiod_request_commit(struct gpio_desc *desc, const char *label)
{
struct gpio_chip *gc = desc->gdev->chip;
unsigned long flags;
unsigned int offset;
int ret;
if (label) {
label = kstrdup_const(label, GFP_KERNEL);
if (!label)
return -ENOMEM;
}
spin_lock_irqsave(&gpio_lock, flags);
/* NOTE: gpio_request() can be called in early boot,
* before IRQs are enabled, for non-sleeping (SOC) GPIOs.
*/
if (test_and_set_bit(FLAG_REQUESTED, &desc->flags) == 0) {
desc_set_label(desc, label ? : "?");
} else {
ret = -EBUSY;
goto out_free_unlock;
}
if (gc->request) {
/* gc->request may sleep */
spin_unlock_irqrestore(&gpio_lock, flags);
offset = gpio_chip_hwgpio(desc);
if (gpiochip_line_is_valid(gc, offset))
ret = gc->request(gc, offset);
else
ret = -EINVAL;
spin_lock_irqsave(&gpio_lock, flags);
if (ret) {
desc_set_label(desc, NULL);
clear_bit(FLAG_REQUESTED, &desc->flags);
goto out_free_unlock;
}
}
if (gc->get_direction) {
/* gc->get_direction may sleep */
spin_unlock_irqrestore(&gpio_lock, flags);
gpiod_get_direction(desc);
spin_lock_irqsave(&gpio_lock, flags);
}
spin_unlock_irqrestore(&gpio_lock, flags);
return 0;
out_free_unlock:
spin_unlock_irqrestore(&gpio_lock, flags);
kfree_const(label);
return ret;
}要點(diǎn)如下:
1)將其包裝在自旋鎖中并查看此輸出是否已處于請(qǐng)求的狀態(tài):
spin_lock_irqsave(&gpio_lock, flags);
/* NOTE: gpio_request() can be called in early boot,
* before IRQs are enabled, for non-sleeping (SOC) GPIOs.
*/
if (test_and_set_bit(FLAG_REQUESTED, &desc->flags) == 0) {
desc_set_label(desc, label ? : "?");
} else {
ret = -EBUSY;
goto out_free_unlock;
}2)如果輸出不忙,那么我們檢查它是否有自己的平臺(tái)相關(guān)函數(shù) request(),并調(diào)用它:
if (gc->request) {
/* gc->request may sleep */
spin_unlock_irqrestore(&gpio_lock, flags);
offset = gpio_chip_hwgpio(desc);
if (gpiochip_line_is_valid(gc, offset))
ret = gc->request(gc, offset);
else
ret = -EINVAL;
spin_lock_irqsave(&gpio_lock, flags);顯然,我們?cè)谧孕i解鎖的情況下調(diào)用此函數(shù)。雖然很難保證 100%,但看起來我們已經(jīng)解鎖了自旋鎖,因?yàn)檫@里我們已經(jīng)排除了對(duì)特定 GPIO 的同時(shí)訪問,并且建議盡可能縮短自旋鎖的鎖定時(shí)間,以最大程度地減少其他進(jìn)程/線程的主動(dòng)等待。
現(xiàn)在讓我們看看設(shè)置給定值是什么樣子的:
void gpiod_set_value(struct gpio_desc *desc, int value)
{
VALIDATE_DESC_VOID(desc);
/* Should be using gpiod_set_value_cansleep() */
WARN_ON(desc->gdev->chip->can_sleep);
gpiod_set_value_nocheck(desc, value);
}
EXPORT_SYMBOL_GPL(gpiod_set_value);static void gpiod_set_value_nocheck(struct gpio_desc *desc, int value)
{
if (test_bit(FLAG_ACTIVE_LOW, &desc->flags))
value = !value;
if (test_bit(FLAG_OPEN_DRAIN, &desc->flags))
gpio_set_open_drain_value_commit(desc, value);
else (test_bit(FLAG_OPEN_SOURCE, &desc->flags))
gpio_set_open_source_value_commit(desc, value);
}static void gpiod_set_raw_value_commit(struct gpio_desc *desc, bool value)
{
struct gpio_chip *gc;
gc = desc->gdev->chip;
trace_gpio_value(desc_to_gpio(desc), 0, value);
gc->set(gc, gpio_chip_hwgpio(desc), value);
}可以看出,最終調(diào)用的是平臺(tái)特定函數(shù)set()。set 函數(shù)不再使用任何訪問共享機(jī)制。如果某個(gè)內(nèi)核代碼想要使用某個(gè) GPIO 引腳,但在調(diào)用 時(shí)出錯(cuò)gpiod_request(),則不應(yīng)再嘗試使用該 GPIO。
現(xiàn)在讓我們看看通過 sysfs 從用戶空間調(diào)用的實(shí)現(xiàn)。處理寫入文件的函數(shù)如下所示/sys/class/gpio/export:
static ssize_t export_store(const struct class *class,
const struct class_attribute *attr,
const char *buf, size_t len)
{
struct gpio_desc *desc;
struct gpio_chip *gc;
int status, offset;
long gpio;
status = kstrtol(buf, 0, &gpio);
if (status < 0)
goto done;
desc = gpio_to_desc(gpio);
/* reject invalid GPIOs */
if (!desc) {
pr_warn("%s: invalid GPIO %ld\n", __func__, gpio);
return -EINVAL;
}
gc = desc->gdev->chip;
offset = gpio_chip_hwgpio(desc);
if (!gpiochip_line_is_valid(gc, offset)) {
pr_warn("%s: GPIO %ld masked\n", __func__, gpio);
return -EINVAL;
}
/* No extra locking here; FLAG_SYSFS just signifies that the
* request and export were done by on behalf of userspace, so
* they may be undone on its behalf too.
*/
status = gpiod_request_user(desc, "sysfs");
if (status)
goto done;
status = gpiod_set_transitory(desc, false);
if (status) {
gpiod_free(desc);
goto done;
}
status = gpiod_export(desc, true);
if (status < 0)
gpiod_free(desc);
else
set_bit(FLAG_SYSFS, &desc->flags);
done:
if (status)
pr_debug("%s: status %d\n", __func__, status);
return status ? : len;
}
static CLASS_ATTR_WO(export);這里最重要的是挑戰(zhàn)gpiod_request_user()。如果你搜索 master 分支,你會(huì)發(fā)現(xiàn)
static inline int gpiod_request_user(struct gpio_desc *desc, const char *label)
{
int ret;
ret = gpiod_request(desc, label);
if (ret == -EPROBE_DEFER)
ret = -ENODEV;
return ret;
}在內(nèi)核 4.x,5.x 中沒有gpiod_request_user(),直接使用常規(guī) . gpiod_request()。
static ssize_t value_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t size)
{
struct gpiod_data *data = dev_get_drvdata(dev);
struct gpio_desc *desc = data->desc;
ssize_t status;
long value;
status = kstrtol(buf, 0, &value);
mutex_lock(&data->mutex);
if (!test_bit(FLAG_IS_OUT, &desc->flags)) {
status = -EPERM;
} else if (status == 0) {
gpiod_set_value_cansleep(desc, value);
status = size;
}
mutex_unlock(&data->mutex);
return status;
}
static DEVICE_ATTR_PREALLOC(value, S_IWUSR | S_IRUGO, value_show, value_store);void gpiod_set_value_cansleep(struct gpio_desc *desc, int value)
{
might_sleep();
VALIDATE_DESC_VOID(desc);
gpiod_set_value_nocheck(desc, value);
}
EXPORT_SYMBOL_GPL(gpiod_set_value_cansleep);static void gpiod_set_value_nocheck(struct gpio_desc *desc, int value)
{
if (test_bit(FLAG_ACTIVE_LOW, &desc->flags))
value = !value;
if (test_bit(FLAG_OPEN_DRAIN, &desc->flags))
gpio_set_open_drain_value_commit(desc, value);
else if (test_bit(FLAG_OPEN_SOURCE, &desc->flags))
gpio_set_open_source_value_commit(desc, value);
else
gpiod_set_raw_value_commit(desc, value);
}我們知道,這將導(dǎo)致已經(jīng)熟悉的依賴于平臺(tái)的 set() 函數(shù)。
具體驅(qū)動(dòng)程序的實(shí)現(xiàn)
讓我們看看以上所有內(nèi)容在真實(shí)硬件上是如何實(shí)現(xiàn)的。
研究設(shè)置
我們使用了 BeagleBone Black(SoC TI AM335)和 Orange Pi Zero(SoC Allwinner H2)開發(fā)板。Buildroot 2021.02 被用作內(nèi)核的“包裝器”。這個(gè)設(shè)置沒有什么特別之處,只是手頭上的東西,以及作者或多或少習(xí)慣使用的東西。我們使用了兩塊開發(fā)板來測(cè)試 TI 和 Allwinner 兩家供應(yīng)商的 GPIO 驅(qū)動(dòng)程序的實(shí)現(xiàn)。我們本來想考慮第三塊開發(fā)板,但手頭上沒有其他可用的。
BeagleBone 的 Buildroot 使用內(nèi)核版本 4.19.79。OrangePi Zero 的 Buildroot 使用內(nèi)核版本 5.10.10。如上所述,現(xiàn)代內(nèi)核版本與 4.19 和 5.10 在細(xì)節(jié)上有所不同,但總體方法沒有改變。
BeagleBone 黑色 (TI AM335)
我們啟動(dòng)組裝好的 Buildroot 鏡像并保存 UART 輸出。我們以以下行作為搜索的起點(diǎn):
[ 0.268291] OMAP GPIO hardware version 0.1
我們搜索產(chǎn)生此消息的代碼(該路徑是相對(duì)于內(nèi)核源的根目錄給出的):
驅(qū)動(dòng)程序/gpio/gpio-omap.c
static void omap_gpio_show_rev(struct gpio_bank *bank)
{
static bool called;
u32 rev;
if (called || bank->regs->revision == USHRT_MAX)
return;
rev = readw_relaxed(bank->base + bank->regs->revision);
pr_info("OMAP GPIO hardware version %d.%d\n",
(rev >> 4) & 0x0f, rev & 0x0f);
called = true;
}太好了,我們知道了源代碼在哪里。讓我們開始研究它。首先,讓我們了解一下這個(gè)驅(qū)動(dòng)程序的 DTS 中應(yīng)該包含哪些內(nèi)容。記住,內(nèi)核在獲悉系統(tǒng)存在相應(yīng)的設(shè)備后,會(huì)運(yùn)行驅(qū)動(dòng)程序的probe()函數(shù)。
GPIO控制器是芯片內(nèi)部的一個(gè)模塊,通過 AXI 類型的總線連接。那里沒有即插即用機(jī)制。這意味著內(nèi)核根據(jù)硬件的靜態(tài)描述來確定這一點(diǎn)。設(shè)備樹(俗稱“DTS”或“DTB”——盡管從技術(shù)上講這并不完全正確。
DTS 和 DTB 是設(shè)備樹的兩種呈現(xiàn)形式。
DTS 指的是“設(shè)備樹源代碼”,DTB 指的是“設(shè)備樹二進(jìn)制文件”)。
驅(qū)動(dòng)程序包含必須在 DTS 中描述的兼容字段:
static const struct of_device_id omap_gpio_match[] = {
{
.compatible = "ti,omap4-gpio",
.data = &omap4_pdata,
},
{
.compatible = "ti,omap3-gpio",
.data = &omap3_pdata,
},
{
.compatible = "ti,omap2-gpio",
.data = &omap2_pdata,
},
{ },
};
MODULE_DEVICE_TABLE(of, omap_gpio_match);驅(qū)動(dòng)程序本身注冊(cè)為平臺(tái)驅(qū)動(dòng)程序:
static struct platform_driver omap_gpio_driver = {
.probe = omap_gpio_probe,
.remove = omap_gpio_remove,
.driver = {
.name = "omap_gpio",
.pm = &gpio_pm_ops,
.of_match_table = of_match_ptr(omap_gpio_match),
},
};
/*
* gpio driver register needs to be done before
* machine_init functions access gpio APIs.
* Hence omap_gpio_drv_reg() is a postcore_initcall.
*/
static int __init omap_gpio_drv_reg(void)
{
return platform_driver_register(&omap_gpio_driver);
}
postcore_initcall(omap_gpio_drv_reg);這里我們重新回顧一下 注釋和說明——GPIO 驅(qū)動(dòng)程序必須在使用這些 GPIO 的驅(qū)動(dòng)程序初始化之前注冊(cè)。為了實(shí)現(xiàn)這一點(diǎn),驅(qū)動(dòng)程序在 postcore_initcall 階段初始化,這個(gè)階段比 device_initcall 階段要早得多。
同時(shí),從兼容列表中我們了解到該驅(qū)動(dòng)程序支持三個(gè)版本的GPIO模塊。要了解使用的是哪一個(gè),我們?nèi)匀恍枰D(zhuǎn)到DTS:
架構(gòu)/arm/boot/dts/am33xx.dts
gpio0: gpio@44e07000 {
compatible = "ti,omap4-gpio";
ti,hwmods = "gpio1";
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
reg = <0x44e07000 0x1000>;
interrupts = <96>;
};還有類似的塊,名稱分別為 gpio1、gpio2、gpio3。
結(jié)論:
- gpio 塊至少有三個(gè)受支持的版本(omap2-gpio、omap3-gpio、omap4-gpio)。
- 在DTS中我們可以看到這些塊在芯片內(nèi)部的基地址。
- 這些 GPIO 模塊可以產(chǎn)生中斷。通常,這些信息可以從數(shù)據(jù)手冊(cè)中獲取,但是:
- 原則上,Linux 程序員打開數(shù)據(jù)表的頻率比微控制器程序員要低得多。
- 對(duì)于許多處理器,您可能仍然可以在公共領(lǐng)域找到數(shù)據(jù)表。因此,一般來說,這些知識(shí)并非毫無用處。不過,在這個(gè)特定情況下,它確實(shí)毫無用處。
以下是 omap_gpio_probe() 中發(fā)生的情況:
驅(qū)動(dòng)程序/gpio/gpio-omap.c
static int omap_gpio_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *node = dev->of_node;
const struct of_device_id *match;
const struct omap_gpio_platform_data *pdata;
struct resource *res;
struct gpio_bank *bank;
struct irq_chip *irqc;
...
if (bank->is_mpuio)
omap_mpuio_init(bank);
omap_gpio_mod_init(bank);
ret = omap_gpio_chip_init(bank, irqc);
if (ret) {
pm_runtime_put_sync(dev);
pm_runtime_disable(dev);
if (bank->dbck_flag)
clk_unprepare(bank->dbck);
return ret;
}
omap_gpio_show_rev(bank);讓我們仔細(xì)看看:
if (bank->is_mpuio) omap_mpuio_init(bank);
從代碼上看不清楚它是什么。如果你在搜索引擎中輸入“mpuio”,你可以在TI數(shù)據(jù)表中找到它:

互聯(lián)網(wǎng)上的TI處理器數(shù)據(jù)表
也許這足以得出以下暫時(shí)的結(jié)論:
- 這是來自 TI 的一個(gè)具體事物。
- 這是 GPIO 端口的一些特殊操作模式,為此有一個(gè)驅(qū)動(dòng)程序。
- 并非所有 TI GPIO 端口都支持此模式。
- 在這種情況下,這種模式的存在與否并不會(huì)對(duì)文章主題的考慮產(chǎn)生很大的影響。
接下來我們有這個(gè)函數(shù):
static void omap_gpio_mod_init(struct gpio_bank *bank)
{
void __iomem *base = bank->base;
u32 l = 0xffffffff;
if (bank->width == 16)
l = 0xffff;
if (bank->is_mpuio) {
writel_relaxed(l, bank->base + bank->regs->irqenable);
return;
}
omap_gpio_rmw(base, bank->regs->irqenable, l,
bank->regs->irqenable_inv);
omap_gpio_rmw(base, bank->regs->irqstatus, l,
!bank->regs->irqenable_inv);
if (bank->regs->debounce_en)
writel_relaxed(0, base + bank->regs->debounce_en);
/* Save OE default value (0xffffffff) in the context */
bank->context.oe = readl_relaxed(bank->base + bank->regs->direction);
/* Initialize interface clk ungated, module enabled */
if (bank->regs->ctrl)
writel_relaxed(0, base + bank->regs->ctrl);
}這些是對(duì) GPIO 塊寄存器的可靠寫入。這里沒有特定于 Linux 內(nèi)核的操作??偟膩碚f,對(duì)于我們討論的主題來說,這也不是一個(gè)非常重要的功能。
但下一個(gè)函數(shù)正好符合目標(biāo):
static int omap_gpio_chip_init(struct gpio_bank *bank, struct irq_chip *irqc)
{
struct gpio_irq_chip *irq;
static int gpio;
const char *label;
int irq_base = 0;
int ret;
/*
* REVISIT eventually switch from OMAP-specific gpio structs
* over to the generic ones
*/
bank->chip.request = omap_gpio_request;
bank->chip.free = omap_gpio_free;
bank->chip.get_direction = omap_gpio_get_direction;
bank->chip.direction_input = omap_gpio_input;
bank->chip.get = omap_gpio_get;
bank->chip.direction_output = omap_gpio_output;
bank->chip.set_config = omap_gpio_set_config;
bank->chip.set = omap_gpio_set;
if (bank->is_mpuio) {
bank->chip.label = "mpuio";
if (bank->regs->wkup_en)
bank->chip.parent = &omap_mpuio_device.dev;
bank->chip.base = OMAP_MPUIO(0);
} else {
label = devm_kasprintf(bank->chip.parent, GFP_KERNEL, "gpio-%d-%d",
gpio, gpio + bank->width - 1);
if (!label)
return -ENOMEM;
bank->chip.label = label;
bank->chip.base = gpio;
}
bank->chip.ngpio = bank->width;
#ifdef CONFIG_ARCH_OMAP1
/*
* REVISIT: Once we have OMAP1 supporting SPARSE_IRQ, we can drop
* irq_alloc_descs() since a base IRQ offset will no longer be needed.
*/
irq_base = devm_irq_alloc_descs(bank->chip.parent,
-1, 0, bank->width, 0);
if (irq_base < 0) {
dev_err(bank->chip.parent, "Couldn't allocate IRQ numbers\n");
return -ENODEV;
}
#endif
/* MPUIO is a bit different, reading IRQ status clears it */
if (bank->is_mpuio) {
irqc->irq_ack = dummy_irq_chip.irq_ack;
if (!bank->regs->wkup_en)
irqc->irq_set_wake = NULL;
}
irq = &bank->chip.irq;
irq->chip = irqc;
irq->handler = handle_bad_irq;
irq->default_type = IRQ_TYPE_NONE;
irq->num_parents = 1;
irq->parents = &bank->irq;
irq->first = irq_base;
ret = gpiochip_add_data(&bank->chip, bank);
if (ret) {
dev_err(bank->chip.parent,
"Could not register gpio chip %d\n", ret);
return ret;
}
ret = devm_request_irq(bank->chip.parent, bank->irq,
omap_gpio_irq_handler,
0, dev_name(bank->chip.parent), bank);
if (ret)
gpiochip_remove(&bank->chip);
if (!bank->is_mpuio)
gpio += bank->width;原則上,一條注釋就足以理解——這里我們從“OMAP 專用”結(jié)構(gòu)體(struct gpio_bank)過渡到“系統(tǒng)級(jí)”結(jié)構(gòu)體(struct gpiochip)。實(shí)際上,我們將系統(tǒng)級(jí)結(jié)構(gòu)體作為 OMAP 專用結(jié)構(gòu)體中的一個(gè)實(shí)例。這里有一些與中斷相關(guān)的操作,但我們暫時(shí)不討論這個(gè)話題。
對(duì)于我們來說現(xiàn)在最重要的部分是:
bank->chip.request = omap_gpio_request; bank->chip.free = omap_gpio_free; bank->chip.get_direction = omap_gpio_get_direction; bank->chip.direction_input = omap_gpio_input; bank->chip.get = omap_gpio_get; bank->chip.direction_output = omap_gpio_output; bank->chip.set_config = omap_gpio_set_config; bank->chip.set = omap_gpio_set;
這里,我們實(shí)現(xiàn)了從通用 API 到其平臺(tái)相關(guān)代碼的轉(zhuǎn)換。當(dāng)某些代碼調(diào)用平臺(tái)無關(guān)函數(shù)(例如 gpiod_request())時(shí),系統(tǒng)會(huì)調(diào)用 gpio_chip.request()。如果這種情況發(fā)生在 AM335x 處理器上,我們就會(huì)調(diào)用 omap_gpio_request()。
好了,填充 gpio_chip 結(jié)構(gòu)體之后,最重要的事情就是在系統(tǒng)中注冊(cè)這個(gè)結(jié)構(gòu)體。從這一刻起,系統(tǒng)就知道了這些 GPIO 線的存在。
讓我們來看看兩個(gè)函數(shù)的實(shí)現(xiàn)——GPIO 請(qǐng)求和 GPIO 值設(shè)置:
static int omap_gpio_request(struct gpio_chip *chip, unsigned offset)
{
struct gpio_bank *bank = gpiochip_get_data(chip);
unsigned long flags;
/*
* If this is the first gpio_request for the bank,
* enable the bank module.
*/
if (!BANK_USED(bank))
pm_runtime_get_sync(chip->parent);
raw_spin_lock_irqsave(&bank->lock, flags);
omap_enable_gpio_module(bank, offset);
bank->mod_usage |= BIT(offset);
raw_spin_unlock_irqrestore(&bank->lock, flags);
return 0;
}這里最重要的是自旋鎖 (spinlock) 封裝的內(nèi)容:?jiǎn)⒂?GPIO 模塊并寫入內(nèi)部 bank.mod_usage 結(jié)構(gòu)體,這實(shí)際上復(fù)制了關(guān)于引腳“忙”狀態(tài)的條目。以下是其中最重要的部分:
- 事實(shí)上,使用自旋鎖可以消除碰撞。
- 雖然這里存在一個(gè)函數(shù) omap_enable_gpio_module(),但通常不需要對(duì)硬件進(jìn)行任何特殊操作。GPIO 請(qǐng)求只是在系統(tǒng)中記錄“此 GPIO 已被占用”的一種方式。嚴(yán)格來說,平臺(tái)相關(guān)的 request() 函數(shù)根本沒有必要存在。
設(shè)置GPIO輸出值的函數(shù):
static void omap_gpio_set(struct gpio_chip *chip, unsigned offset, int value)
{
struct gpio_bank *bank;
unsigned long flags;
bank = gpiochip_get_data(chip);
raw_spin_lock_irqsave(&bank->lock, flags);
bank->set_dataout(bank, offset, value);
raw_spin_unlock_irqrestore(&bank->lock, flags);
}set_dataout() 函數(shù)是在驅(qū)動(dòng)程序初始化期間在probe()函數(shù)中指定的:
if (bank->regs->set_dataout && bank->regs->clr_dataout) {
bank->set_dataout = omap_set_gpio_dataout_reg;
bank->set_dataout_multiple = omap_set_gpio_dataout_reg_multiple;
} else {
bank->set_dataout = omap_set_gpio_dataout_mask;
bank->set_dataout_multiple =
omap_set_gpio_dataout_mask_multiple;
}顯然,這為驅(qū)動(dòng)程序開發(fā)人員提供了額外的自由度。無論如何,如果我們查看 omap_set_gpio_dataout_reg(),我們會(huì)發(fā)現(xiàn)它已經(jīng)直接與 GPIO 塊寄存器進(jìn)行了交互:
/* set data out value using dedicate set/clear register */
static void omap_set_gpio_dataout_reg(struct gpio_bank *bank, unsigned offset,
int enable)
{
void __iomem *reg = bank->base;
u32 l = BIT(offset);
if (enable) {
reg += bank->regs->set_dataout;
bank->context.dataout |= l;
} else {
reg += bank->regs->clr_dataout;
bank->context.dataout &= ~l;
}
writel_relaxed(l, reg);
}本質(zhì)上,我們?cè)谶@里看到的是一個(gè)簡(jiǎn)單的注冊(cè)表?xiàng)l目,與我們?cè)谖恼麻_頭討論的相同。
現(xiàn)在,讓我們嘗試驗(yàn)證之前通過閱讀源代碼獲得的理論計(jì)算。確保在通過內(nèi)核空間和用戶空間訪問時(shí),我們確實(shí)會(huì)經(jīng)歷這一系列調(diào)用。讓我們用最原始的方式做到這一點(diǎn)——在
printk(“%s: entered\n”, __func__);
我們討論的函數(shù)中添加一行代碼。然后,我們將刪除日志。
首先,讓我們看一下UART啟動(dòng)日志,我們會(huì)立即看到我們干預(yù)的痕跡:
[ 0.571306] pinctrl-single 44e10800.pinmux: 142 pins, size 568 [ 0.576824] gpiod_request_commit: entered [ 0.576852] omap_gpio_request: entered [ 0.576992] gpiod_direction_output_raw_commit: entered [ 0.577051] omap_set_gpio_direction: entered [ 0.580063] Serial: 8250/16550 driver, 6 ports, IRQ sharing enabled ... [ 1.535627] sdhci: Secure Digital Host Controller Interface driver [ 1.541871] sdhci: Copyright(c) Pierre Ossman [ 1.547696] gpiod_request_commit: entered [ 1.551745] omap_gpio_request: entered [ 1.555787] omap_set_gpio_direction: entered [ 1.560134] omap_gpio 44e07000.gpio: Could not set line 6 debounce to 200000) [ 1.568968] omap_hsmmc 48060000.mmc: Got CD GPIO
很明顯,驅(qū)動(dòng)程序在加載過程中使用了兩個(gè) GPIO。而且,它們的執(zhí)行路徑與之前提到的完全一致——首先調(diào)用與平臺(tái)無關(guān)的 gpiod_request() 函數(shù),然后調(diào)用與平臺(tái)相關(guān)的 omap_gpio_request() 函數(shù)。
現(xiàn)在讓我們看看如果我們從內(nèi)核空間控制 GPIO 會(huì)發(fā)生什么:
# cd /sys/class/gpio # ls export gpiochip0 gpiochip32 gpiochip64 gpiochip96 unexport # echo 15 > export [ 803.092438] gpiod_request_commit: entered [ 803.097022] omap_gpio_request: entered # ls export gpiochip0 gpiochip64 unexport gpio15 gpiochip32 gpiochip96 # cd gpio15 # echo out > direction [ 815.830522] gpiod_direction_output_raw_commit: entered [ 815.836210] omap_set_gpio_direction: entered # echo 1 > value [ 820.621108] value_store: entered [ 820.624881] gpiod_set_value_cansleep: entered [ 820.629404] gpiod_set_raw_value_commit: entered [ 820.634088] omap_gpio_set: entered
顯然,一切確實(shí)都朝著我們所討論的相同功能發(fā)展。
OrangePi Zero(全志 H2)
這里的啟動(dòng)日志并沒有用GPIO相關(guān)的字眼來破壞我們的印象。這些行看起來像是我們要找的內(nèi)容:
[ 0.089512] sun8i-h3-pinctrl 1c20800.pinctrl: initialized sunXi PIO driver [ 0.091141] sun8i-h3-r-pinctrl 1f02c00.pinctrl: initialized sunXi PIO driver
“sun8i-h3”這個(gè)名字很容易讓人混淆,因?yàn)槲覀冎靼迳系奶幚砥魃嫌幸粋€(gè)大大的“H2”字樣。搜索可用的DTS,找不到任何類似“sun8i-h2”的版本。不過,匯編代碼可以運(yùn)行,這意味著H2和H3之間的區(qū)別并不大。
這意味著一切都有效,我們可以進(jìn)一步研究代碼。
讓我們看一下顯示這些行的代碼。它位于此處:
驅(qū)動(dòng)程序/pinctrl/sunxi/pinctrl-sunxi.c
int sunxi_pinctrl_init_with_variant(struct platform_device *pdev,
const struct sunxi_pinctrl_desc *desc,
unsigned long variant)
{
...
pctl->chip->owner = THIS_MODULE;
pctl->chip->request = gpiochip_generic_request;
pctl->chip->free = gpiochip_generic_free;
pctl->chip->set_config = gpiochip_generic_config;
pctl->chip->direction_input = sunxi_pinctrl_gpio_direction_input;
pctl->chip->direction_output = sunxi_pinctrl_gpio_direction_output;
pctl->chip->get = sunxi_pinctrl_gpio_get;
pctl->chip->set = sunxi_pinctrl_gpio_set;
pctl->chip->of_xlate = sunxi_pinctrl_gpio_of_xlate;
pctl->chip->to_irq = sunxi_pinctrl_gpio_to_irq;
pctl->chip->of_gpio_n_cells = 3;
pctl->chip->can_sleep = false;
pctl->chip->ngpio = round_up(last_pin, PINS_PER_BANK) -
pctl->desc->pin_base;
pctl->chip->label = dev_name(&pdev->dev);
pctl->chip->parent = &pdev->dev;
pctl->chip->base = pctl->desc->pin_base;
...他們這樣稱呼它:
驅(qū)動(dòng)程序/pinctrl/sunxi/pinctrl-sun8i-v3s.c
static int sun8i_v3s_pinctrl_probe(struct platform_device *pdev)
{
unsigned long variant = (unsigned long)of_device_get_match_data(&pdev->dev);
return sunxi_pinctrl_init_with_variant(pdev, &sun8i_v3s_pinctrl_data,
variant);
}
static const struct of_device_id sun8i_v3s_pinctrl_match[] = {
{
.compatible = "allwinner,sun8i-v3-pinctrl",
.data = (void *)PINCTRL_SUN8I_V3
},
{
.compatible = "allwinner,sun8i-v3s-pinctrl",
.data = (void *)PINCTRL_SUN8I_V3S
},
{ },
};
static struct platform_driver sun8i_v3s_pinctrl_driver = {
.probe = sun8i_v3s_pinctrl_probe,
.driver = {
.name = "sun8i-v3s-pinctrl",
.of_match_table = sun8i_v3s_pinctrl_match,
},
};
builtin_platform_driver(sun8i_v3s_pinctrl_driver);與 TI 類似,這是一個(gè)平臺(tái)驅(qū)動(dòng)程序。實(shí)際上,很難用其他方式實(shí)現(xiàn)。
但明顯的區(qū)別是,驅(qū)動(dòng)程序不在 GPIO 目錄中,而是在 pinctrl 目錄中。
pinctrl 驅(qū)動(dòng)程序還控制每個(gè)特定引腳的分配。通常,現(xiàn)代 SoC 上的 GPIO 引腳與其他 I2C/SPI/UART 級(jí)接口復(fù)用。pinctrl 驅(qū)動(dòng)程序包含控制這些復(fù)用器的功能。例如,TI AM335x 上也存在 pinctrl 驅(qū)動(dòng)程序,但該實(shí)體與 GPIO 驅(qū)動(dòng)程序是分開的。在這里,他們決定這樣做。
讓我們回到 GPIO 上。無論如何,此驅(qū)動(dòng)程序中的關(guān)鍵操作已經(jīng)完成——創(chuàng)建、填充并在系統(tǒng)中注冊(cè) gpio_chip 結(jié)構(gòu)的實(shí)例。讓我們仔細(xì)看看這一點(diǎn)。
首先,你可以看到函數(shù) request() 用于實(shí)現(xiàn)gpiochip_generic_request():
驅(qū)動(dòng)程序/gpio/gpiolib.c
int gpiochip_generic_request(struct gpio_chip *gc, unsigned offset)
{
#ifdef CONFIG_PINCTRL
if (list_empty(&gc->gpiodev->pin_ranges))
return 0;
#endif
return pinctrl_gpio_request(gc->gpiodev->base + offset);
}
EXPORT_SYMBOL_GPL(gpiochip_generic_request);她在引擎蓋下喊道:
驅(qū)動(dòng)程序/pinctrl/core.c
/**
* pinctrl_gpio_request() - request a single pin to be used as GPIO
* @gpio: the GPIO pin number from the GPIO subsystem number space
*
* This function should *ONLY* be used from gpiolib-based GPIO drivers,
* as part of their gpio_request() semantics, platforms and individual drivers
* shall *NOT* request GPIO pins to be muxed in.
*/
int pinctrl_gpio_request(unsigned gpio)
{
struct pinctrl_dev *pctldev;
struct pinctrl_gpio_range *range;
int ret;
int pin;
ret = pinctrl_get_device_gpio_range(gpio, &pctldev, &range);
if (ret) {
if (pinctrl_ready_for_gpio_range(gpio))
ret = 0;
return ret;
}
mutex_lock(&pctldev->mutex);
/* Convert to the pin controllers number space */
pin = gpio_to_pin(range, gpio);
ret = pinmux_request_gpio(pctldev, range, pin, gpio);
mutex_unlock(&pctldev->mutex);
return ret;
}
EXPORT_SYMBOL_GPL(pinctrl_gpio_request);無需詳細(xì)了解 pinctrl 和 pinmux 的實(shí)現(xiàn),我們嘗試說明:此功能不僅保留了 GPIO,而且將其固定為 GPIO,而不是 I2C/SPI/UART/等輸出。
我們來看看設(shè)置GPIO輸出值的函數(shù)是什么樣的:
驅(qū)動(dòng)程序/sunxi/pinctrl-sunxi.c
static void sunxi_pinctrl_gpio_set(struct gpio_chip *chip,
unsigned offset, int value)
{
struct sunxi_pinctrl *pctl = gpiochip_get_data(chip);
u32 reg = sunxi_data_reg(offset);
u8 index = sunxi_data_offset(offset);
unsigned long flags;
u32 regval;
raw_spin_lock_irqsave(&pctl->lock, flags);
regval = readl(pctl->membase + reg);
if (value)
regval |= BIT(index);
else
regval &= ~(BIT(index));
writel(regval, pctl->membase + reg);
raw_spin_unlock_irqrestore(&pctl->lock, flags);正如預(yù)期的那樣,這里所有操作也都?xì)w結(jié)為寫入外設(shè)寄存器。有趣的是,這里對(duì)寄存器的操作被封裝在自旋鎖中。這可能是因?yàn)榧拇嫫鞑僮鳑]有原子操作,操作分為三個(gè)階段:讀取寄存器、更改指定位以及將更改后的值寫入寄存器。引入自旋鎖可以消除潛在的沖突。
為了確保我們理解正確,讓我們通過添加調(diào)試輸出來實(shí)現(xiàn)類似的路線。
確實(shí),當(dāng)添加此輸出并使用此固件運(yùn)行時(shí),我們會(huì)陷入無限循環(huán)的消息中:
[ 20.884895] gpiod_set_value_nocheck: entered [ 20.889190] gpiod_set_raw_value_commit: entered [ 20.893720] sunxi_pinctrl_gpio_set: entered
但在日志的開頭我們看到:
[ 1.623498] gpiod_request: entered [ 1.630993] gpiod_request_commit: entered [ 1.635007] gpiochip_generic_request: entered [ 1.639378] pinctrl_gpio_request: entered [ 1.643401] pin_request: entered
這基本上證實(shí)了上面的結(jié)論,但在某種程度上干擾了檢查通過 sysfs 訪問的過程?;旧?,這是一個(gè)非常典型的 Linux 調(diào)試故事——你只需要稍微深入挖掘一下,就會(huì)發(fā)現(xiàn)一些完全無法理解的行為,雖然這并不那么嚴(yán)重(因?yàn)橐恢币詠矶际沁@樣),但忽略它似乎也不妥。我們將把此行為的調(diào)試放在稍后的劇透部分,在這里我們將完成確保 sysfs 正常工作的工作。讓我們刪除 gpiod_set_...() 的額外輸出,重建、重啟,然后查看:
# cd /sys/class/gpio # ls export gpiochip0 gpiochip352 unexport # echo 15 > export [ 1035.505141] export_store: entered [ 1035.508561] gpiod_request: entered [ 1035.511968] gpiod_request_commit: entered [ 1035.515979] gpiochip_generic_request: entered [ 1035.520397] pinctrl_gpio_request: entered [ 1035.524426] pin_request: entered # cd gpio15 # echo out > direction # echo 1 > value [ 1057.037344] value_store: entered
顯然,這正是我們所期望的。
不定期調(diào)試
誰在拉動(dòng) GPIO?調(diào)試過程大大縮短了,搜索和嘗試次數(shù)也顯著增加。最終,結(jié)果如下:
1) 值設(shè)置功能增加調(diào)試輸出:
printk("%s: gpiochip->base=%d, offset=%d\n", __func__, chip->base, offset);我們收到了以下日志:
[ 59.314332] gpiod_set_value_cansleep: entered [ 59.318718] gpiod_set_value_nocheck: entered [ 59.322989] sunxi_pinctrl_gpio_set: entered [ 59.327174] sunxi_pinctrl_gpio_set: gpiochip->base=352, offset=6
2)關(guān)閉日志并查看:
# cat /sys/class/gpio/gpiochip352/label 1f02c00.pinctrl
3) 我們查看了處理器的數(shù)據(jù)手冊(cè)。我們發(fā)現(xiàn) R_PIO 塊位于地址 0x1F0_2C00。實(shí)際上,我們當(dāng)然可以通過 DTS 找到這一點(diǎn)。第二個(gè)引腳塊稱為 PIO。目前還不清楚它們之間有什么區(qū)別。
4) 好的,讓我們進(jìn)入開發(fā)板上的DTS。搜索“r_pio”并找到:
架構(gòu)/arm/boot/dts/sun8i-h2-plus-orangepi-zero.dts
reg_vdd_cpux: vdd-cpux-regulator {
compatible = "regulator-gpio";
regulator-name = "vdd-cpux";
regulator-type = "voltage";
regulator-boot-on;
regulator-always-on;
regulator-min-microvolt = <1100000>;
regulator-max-microvolt = <1300000>;
regulator-ramp-delay = <50>; /* 4ms */
gpios = <&r_pio 0 6 GPIO_ACTIVE_HIGH>; /* PL6 */
enable-active-high;
gpios-states = <1>;
states = <1100000 0>, <1300000 1>;
};注釋里寫著PL6。看起來很像物理引腳標(biāo)識(shí)。
5)打開電路板圖
我們看到 PL6 引腳確實(shí)連接到名為 CPUX-VSET 的鏈:

這個(gè)鏈實(shí)際上控制著某種電源:

這對(duì)我們來說并沒有什么用,但至少讓我們清楚了它到底是什么??雌饋?Linux 在運(yùn)行時(shí)正在積極地管理 CPU 功耗。
6)對(duì)DTS節(jié)點(diǎn)進(jìn)行注釋:
&cpu0 {
cpu-supply = <®_vdd_cpux>;
};7) 重建鏡像,刷入——確保垃圾信息消失。案件告破。這種引腳抖動(dòng)現(xiàn)象的原因很有趣,但這超出了本文的討論范圍。
總結(jié)
總而言之,我們可以將所說的一切簡(jiǎn)化為一個(gè)簡(jiǎn)單的結(jié)構(gòu)圖:

千言萬語值得,但是,借助流程圖并研究源代碼,您可以實(shí)現(xiàn)比僅借助流程圖更多的東西,所以我們不要急于拋棄前面的所有文字。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Linux磁盤空間耗盡導(dǎo)致系統(tǒng)故障問題及說明
文章主要內(nèi)容是Linux系統(tǒng)磁盤耗盡導(dǎo)致報(bào)錯(cuò)的排障和維護(hù)思路,包括確認(rèn)故障、查找占用空間的文件或目錄、清理文件以及預(yù)防措施,還介紹了ncdu工具的使用方法和日志管理、臨時(shí)文件管理、軟鏈接和硬鏈接、磁盤配額等高級(jí)維護(hù)技巧2026-01-01
linux mount報(bào)錯(cuò):you must specify the filesystem type的解決方法
這篇文章主要介紹了linux mount報(bào)錯(cuò):you must specify the filesystem type的解決方法,文中給出了詳細(xì)的解決方法示例,對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來一起看看吧。2017-03-03
CentOS7將Nginx添加系統(tǒng)服務(wù)的方法步驟
這篇文章主要介紹了CentOS7將Nginx添加系統(tǒng)服務(wù)的方法步驟,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-03-03
CentOS7 Grub引導(dǎo)故障恢復(fù)全過程
文章介紹了當(dāng)CentOS7的/boot/grub2/grub.cfg文件丟失時(shí)的恢復(fù)步驟,包括手動(dòng)引導(dǎo)進(jìn)入系統(tǒng)、永久修復(fù)GRUB配置以及使用救援模式進(jìn)行修復(fù)2026-01-01
Ubuntu Server 10.04修改Apache的默認(rèn)目錄的方法
這篇文章主要為大家分享下Ubuntu Server 10.04修改Apache的默認(rèn)目錄的方法,需要的朋友可以參考下2013-12-12
Linux學(xué)習(xí)第一天——ssh登錄和軟件安裝詳解
這篇文章主要介紹了Linux學(xué)習(xí)第一天——ssh登錄和軟件安裝詳解 ,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-12-12
linux vps服務(wù)器進(jìn)程kswapd0與events/0消耗大量CPU的問題
使用了阿里云的vps服務(wù)器網(wǎng)站宕了兩次機(jī),發(fā)工單給阿里云,發(fā)現(xiàn)原因是服務(wù)器的CPU 100%了,這也是vps的弊端,內(nèi)容給的相對(duì)小2014-03-03

