正点原子驱动开发BUG(一)--SPI无法正常通信

目录

一、问题描述

使用正点的im6ull开发板进行spi通信驱动开发实验的时候,主机无法与从机进行正常通信。就算使用正点的例程,也无法正常通信。读不到从机寄存器中的值。以读取从机ID为例,例子为正点原子的例程基础上添加了几行printk用来打印信息:

c 复制代码
void icm20608_reginit(void)
{
	u8 value = 1;
	u8 test = 1;
	
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x80);		/* 复位 */
	mdelay(50);
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x01);		/* 关闭睡眠 */
	mdelay(50);

	printk("ICM20608 ID = %#X\r\n", value);	
	value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);	 /* 这里读出来的ID不对 */
	printk("ICM20608 ID = %#X\r\n", value);	
	...
}
...
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{
	u8 data = 0;
	icm20608_read_regs(dev, reg, &data, 1);
	return data;
}
...
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{

	int ret = -1;
	unsigned char txdata[1];
	unsigned char * rxdata;
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)dev->private_data;
    
	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
	if(!t) {
		return -ENOMEM;
	}

	rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL);	/* 申请内存 */
	if(!rxdata) {
		goto out1;
	}

	/* 一共发送len+1个字节的数据,第一个字节为
	寄存器首地址,一共要读取len个字节长度的数据,*/
	txdata[0] = reg | 0x80;		/* 写数据的时候首寄存器地址bit8要置1 */			
	t->tx_buf = txdata;			/* 要发送的数据 */
    t->rx_buf = rxdata;			/* 要读取的数据 */
	t->len = len+1;				/* t->len=发送的长度+读取的长度 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */
	if(ret) {
		goto out2;
	}
	
    memcpy(buf , rxdata+1, len);  /* 只需要读取的数据 */

out2:
	kfree(rxdata);					/* 释放内存 */
out1:	
	kfree(t);						/* 释放内存 */
	
	return ret;
}

读ID失败,读出来ID是0,单纯是因为调用icm20608_read_onereg函数时会把返回值初始化为0,也就是根本没读到寄存器中内容

二、讲该问题的解决方案

首先直接说明问题所在:如果使用正点的spi驱动开发例程则设备树中spi适配器设备节点下的 cs-gpios属性不能写成 cs-gpio。当使用的属性名位cs-gpios的时候是由该spi适配器匹配的驱动程序来控制片选。

其实正点原子也在开发指南中说了:

bash 复制代码
第 2 行,设置当前片选数量为 1,因为就只接了一个 ICM20608。
第 3 行,一定要使用 "cs-gpios"属性来描述片选引脚,SPI 主机驱动就会控制片选引脚。
第 5 行,设置 IO 要使用的 pinctrl 子节点,也就是我们在示例代码 62.5.1.1 中新建的
pinctrl_ecspi3。

正点提供的linux源码中的documentation中也提到怎么编写对应设备树了,该文件位于linux源码位置/Documentation/devicetree/bindings/spi中:

bash 复制代码
* Freescale (Enhanced) Configurable Serial Peripheral Interface
  (CSPI/eCSPI) for i.MX

Required properties:
- compatible :
  - "fsl,imx1-cspi" for SPI compatible with the one integrated on i.MX1
  - "fsl,imx21-cspi" for SPI compatible with the one integrated on i.MX21
  - "fsl,imx27-cspi" for SPI compatible with the one integrated on i.MX27
  - "fsl,imx31-cspi" for SPI compatible with the one integrated on i.MX31
  - "fsl,imx35-cspi" for SPI compatible with the one integrated on i.MX35
  - "fsl,imx51-ecspi" for SPI compatible with the one integrated on i.MX51
- reg : Offset and length of the register set for the device
- interrupts : Should contain CSPI/eCSPI interrupt
- fsl,spi-num-chipselects : Contains the number of the chipselect
- cs-gpios : Specifies the gpio pins to be used for chipselects.
- clocks : Clock specifiers for both ipg and per clocks.
- clock-names : Clock names should include both "ipg" and "per"
See the clock consumer binding,
	Documentation/devicetree/bindings/clock/clock-bindings.txt
- dmas: DMA specifiers for tx and rx dma. See the DMA client binding,
		Documentation/devicetree/bindings/dma/dma.txt
- dma-names: DMA request names should include "tx" and "rx" if present.

Example:

ecspi@70010000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx51-ecspi";
	reg = <0x70010000 0x4000>;
	interrupts = <36>;
	fsl,spi-num-chipselects = <2>;
	cs-gpios = <&gpio3 24 0>, /* GPIO3_24 */
		   <&gpio3 25 0>; /* GPIO3_25 */
	dmas = <&sdma 3 7 1>, <&sdma 4 7 2>;
	dma-names = "rx", "tx";
};

但是我太信任正点提供的linux源码了,直接用了正点提供的出厂linux源码,这个出厂linux源码中的READ_ME解释如下:

bash 复制代码
说明:
1、使用此uboot和linux源码可以编译得到正点原子I.MX6U出货时的uboot和Linux固件。兼容正点原子所有RGB屏,板子上所有功能也相应调试好。直接按【正点原子】I.MX6U用户快速体验Vx.x.pdf文档编译出来使用!
2、驱动指南也提及过编译这里的源码,只是教学编译体验。

问题答疑:
1、看到1、例程源码\10、开发板教程对应的uboot和linux源码这里还有一份正点原子的uboot和linux源码,为什么会有两份源码?
答:1、例程源码\10、开发板教程对应的uboot和linux源码是正点原子驱动指南做驱动实验、移植uboot和linux所用的源码。而出厂源码则是用于出货所使用,客户无需再调试。直接编译使用!

2、为什么要和出厂源码分开?
答:由于多种原因,出厂使用的源码不能与驱动指南所使用的源码同时进行。出厂源码会随时修复bug或者添加新的驱动以兼容正点原子的其他模块。

3、那我可不可以这么理解:出厂源码是给客户直接用在产品上使用,而教程源码则是用于初学者用于学习驱动和移植uboot和Linux上使用?
答:恩,可以这么理解。因为出厂源码是几乎无需再调试了,适用正点原子的ALPHA和Mini开发板,给一些快速上手的客户使用!而教程源码则是用于学习或者自己开发使用!

他说的是出厂源码是几乎无需再调试了,适用于正点原子的ALPHA开发板可以直接上手用,我就直接用了。但其实他的设备树是这么写的:

bash 复制代码
&ecspi3 {
        fsl,spi-num-chipselects = <1>;
        cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>;		/* 必须使用cs-gpios属性名来描述片选引脚(如果你想让spi适配器驱动程序来设置片选的话) */
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_ecspi3>;	/* 难不成是因为这里是pinctrl-0,这个0正好对应这第0通道,即icm20608 */
        status = "okay";

	spidev: icm20608@0 {
	compatible = "alientek,icm20608";
        spi-max-frequency = <8000000>;
        reg = <0>;
    };
};

打眼一看内容跟正点的驱动指南中提到的都一样,所以我就觉得没啥问题,但是实际上可以看到,有一个属性名写成了cs-gpio而不是cs-gpios,这是与正点的spi驱动开发例程不匹配的。为什么不匹配?这得分析分析适配器的驱动程序

三、imx6ull的spi适配器驱动程序控制片选分析

3.1 设备icm20608的驱动程序分析

首先说明问题:问题出现在厂商编写的spi适配器驱动程序上。但在此之前,我们要先来看看我们的设备驱动,也就是正点使用的设备icm20608的驱动程序中的一个函数:spi_setup()

c 复制代码
static int icm20608_probe(struct spi_device *spi)
{
	...
	/*初始化spi_device */
	spi->mode = SPI_MODE_0;	/*MODE0,CPOL=0,CPHA=0*/
	spi_setup(spi);
	icm20608dev.private_data = spi; /* 设置私有数据 */

	/* 初始化ICM20608内部寄存器 */
	icm20608_reginit();		
	return 0;
}

该函数传入一个struct spi_device类型的变量。正点驱动对这个函数一带而过,只是说这个函数必须得有。其实就是这个函数中出了问题。该函数位于drivers/spi/spi.c中,我只列出源码中与错误相关的部分:

c 复制代码
int spi_setup(struct spi_device *spi)
{
	unsigned	bad_bits, ugly_bits;
	int		status = 0;
	...
	spi_set_cs(spi, false);

	if (spi->master->setup)
		status = spi->master->setup(spi);
	...
	return status;
}
...
...
static void spi_set_cs(struct spi_device *spi, bool enable)
{
	if (spi->mode & SPI_CS_HIGH)
		enable = !enable;

	if (spi->cs_gpio >= 0)
		gpio_set_value(spi->cs_gpio, !enable);
	else if (spi->master->set_cs)
		spi->master->set_cs(spi, !enable);
}

spi_setup调用的spi_set_cs函数中可以看到,如果spi->cs_gpio >= 0,函数调用gpio_set_value(spi->cs_gpio, !enable)来设置某个goio口的电平,比如在正点的教程中如果要选中icm20608外设,那么就要设置gpio1_20这个gpio口的电平,所以应该出现的情况是spi->cs_gpio = 20。如果没满足第一个if,就会判断第二个else if,这个master->set_cs根本没设置,所以也用不到。

那么spi->cs_gpio是在哪里设置的呢?在spi.c文件中有这么一个函数spi_add_device(),截取出相关的内容:

c 复制代码
int spi_add_device(struct spi_device *spi)
{
	static DEFINE_MUTEX(spi_add_lock);
	struct spi_master *master = spi->master;
	struct device *dev = master->dev.parent;
	int status;

	/* Chipselects are numbered 0..max; validate. */
	if (spi->chip_select >= master->num_chipselect) {
		dev_err(dev, "cs%d >= max %d\n",
			spi->chip_select,
			master->num_chipselect);
		return -EINVAL;
	}
	...
	if (master->cs_gpios)
		spi->cs_gpio = master->cs_gpios[spi->chip_select];
	...
}

可以看到在最后一个if中,如果master->cs_gpios不为0,那么就会设置spi->cs_gpio = master->cs_gpios[spi->chip_select]。这个spi->chip_select相当于master->cs_gpios这个数组的索引(master->cs_gpioscs_gpios的是struct spi_master结构体下的一个成员变量,为一个int类型的指针,其实就是一个数组),这个会在下一部分提到。

spi->chip_select又是在哪设置的呢?看一下spi.c文件中的of_register_spi_device()函数:

c 复制代码
#if defined(CONFIG_OF)
static struct spi_device *
of_register_spi_device(struct spi_master *master, struct device_node *nc)
{
	struct spi_device *spi;
	int rc;
	u32 value;

	/* Alloc an spi_device */
	spi = spi_alloc_device(master);
	if (!spi) {
		dev_err(&master->dev, "spi_device alloc error for %s\n",
			nc->full_name);
		rc = -ENOMEM;
		goto err_out;
	}

	/* Select device driver */
	rc = of_modalias_node(nc, spi->modalias,
				sizeof(spi->modalias));
	if (rc < 0) {
		dev_err(&master->dev, "cannot find modalias for %s\n",
			nc->full_name);
		goto err_out;
	}

	/* Device address */
	rc = of_property_read_u32(nc, "reg", &value);
	if (rc) {
		dev_err(&master->dev, "%s has no valid 'reg' property (%d)\n",
			nc->full_name, rc);
		goto err_out;
	}
	spi->chip_select = value;
	...
	/* Register the new device */
	rc = spi_add_device(spi);
	if (rc) {
		dev_err(&master->dev, "spi_device register error %s\n",
			nc->full_name);
		goto err_out;
	}

	return spi;
}

该函数会调用of_property_read_u32()函数解析设备树中的spi设备的reg属性值,并赋值给变量value而在设备树中这个值中填入的正是片选值,然后设置spi->chip_select = value,并调用刚刚提到的spi_add_device()函数。该函数会在of_register_spi_devices()函数中调用:

c 复制代码
static void of_register_spi_devices(struct spi_master *master)
{
	struct spi_device *spi;
	struct device_node *nc;

	if (!master->dev.of_node)
		return;

	for_each_available_child_of_node(master->dev.of_node, nc) {
		spi = of_register_spi_device(master, nc);
		if (IS_ERR(spi))
			dev_warn(&master->dev, "Failed to create SPI device for %s\n",
				nc->full_name);
	}
}

就相当于可能会注册很多个spi设备,所以用个for循环来一个一个注册。这个注册很多个spi设备的函数会在spi_register_master()函数中被调用,而这个函数是跟spi适配器有关。

可以看到,最终的最终,spi->cs_gpio其实是跟master->cs_gpios有关系,并且上述函数的调用的源头也是spi_register_master()函数。所以必须得去看spi适配器的驱动函数了。但在此之前先来看一下当设备树中写的属性为cs-gpio时出现的情况,我们在驱动程序中编写如下测试代码:

c 复制代码
static int icm20608_probe(struct spi_device *spi)
{
	...
	/*初始化spi_device */
	spi->mode = SPI_MODE_0;	/*MODE0,CPOL=0,CPHA=0*/
	spi_setup(spi);
	printk("chip select:%d\r\n", spi->chip_select);
	printk("cs gpio:%d\r\n", spi->cs_gpio);
	icm20608dev.private_data = spi; /* 设置私有数据 */
	...
	return 0;
}

注册编译出来的ko模块的时候结果如下:

可以看到,spi->chip_select正常获取到了设备树中spi设备reg属性的值,但是spi->cs_gpio却没有获取到我们想要获取的gpio号20吗,而是-2。至于为什么,需要看下一部分。

3.2 imx的spi适配器的驱动程序分析

在第二部分中列出的设备树代码中只是列出了引用ecspi3时的一些修改,该设备真正定义在imx6ull.dtsi下:

bash 复制代码
ecspi3: ecspi@02010000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
	reg = <0x02010000 0x4000>;
	interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_ECSPI3>,
			<&clks IMX6UL_CLK_ECSPI3>;
	clock-names = "ipg", "per";
	dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
	dma-names = "rx", "tx";
	status = "disabled";
};

其中的属性compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";,根据这个去找对应的驱动程序,找到的驱动程序为drivers/spi/spi-imx.c,找到其.probe函数,并列出其与问题相关的部分:

c 复制代码
static int spi_imx_probe(struct platform_device *pdev)
{
	struct device_node *np = pdev->dev.of_node;
	const struct of_device_id *of_id =
			of_match_device(spi_imx_dt_ids, &pdev->dev);
	struct spi_imx_master *mxc_platform_info =
			dev_get_platdata(&pdev->dev);
	struct spi_master *master;
	struct spi_imx_data *spi_imx;
	struct resource *res;
	int i, ret, num_cs, irq;

	if (!np && !mxc_platform_info) {
		dev_err(&pdev->dev, "can't get the platform data\n");
		return -EINVAL;
	}

	ret = of_property_read_u32(np, "fsl,spi-num-chipselects", &num_cs);	/* 获取到num_cs为1 */
	if (ret < 0) {
		if (mxc_platform_info)
			num_cs = mxc_platform_info->num_chipselect;
		else
			return ret;
	}

	master = spi_alloc_master(&pdev->dev,
			sizeof(struct spi_imx_data) + sizeof(int) * num_cs);	/* 这个函数会把num_chipselect初始化为1 */
	if (!master)
		return -ENOMEM;

	...
	spi_imx->bitbang.master = master;

	for (i = 0; i < master->num_chipselect; i++) {
		int cs_gpio = of_get_named_gpio(np, "cs-gpios", i);
		if (!gpio_is_valid(cs_gpio) && mxc_platform_info)	/* gpio_is_valid: asm-generic/gpio.h return 1 or 0*/
			cs_gpio = mxc_platform_info->chipselect[i];

		spi_imx->chipselect[i] = cs_gpio;
		if (!gpio_is_valid(cs_gpio))
			continue;

		ret = devm_gpio_request(&pdev->dev, spi_imx->chipselect[i],
					DRIVER_NAME);
		if (ret) {
			dev_err(&pdev->dev, "can't get cs gpios\n");
			goto out_master_put;
		}
	}

	spi_imx->bitbang.chipselect = spi_imx_chipselect;
	spi_imx->bitbang.setup_transfer = spi_imx_setupxfer;
	spi_imx->bitbang.txrx_bufs = spi_imx_transfer;
	spi_imx->bitbang.master->setup = spi_imx_setup;
	spi_imx->bitbang.master->cleanup = spi_imx_cleanup;
	spi_imx->bitbang.master->prepare_message = spi_imx_prepare_message;
	spi_imx->bitbang.master->unprepare_message = spi_imx_unprepare_message;
	spi_imx->bitbang.master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;
	...
	master->dev.of_node = pdev->dev.of_node;
	ret = spi_bitbang_start(&spi_imx->bitbang);
	...
	return ret;
}

并会在spi_bitbang_start函数中调用了spi_register_master()函数,也就是上一部分提到的很多函数的源头,向内核注册spi_master设备,该函数位于drivers/spi/spi-bitbang.c中,列出相关部分:

c 复制代码
int spi_bitbang_start(struct spi_bitbang *bitbang)
{
	struct spi_master *master = bitbang->master;
	int ret;

	if (!master || !bitbang->chipselect)
		return -EINVAL;

	spin_lock_init(&bitbang->lock);
	...
	ret = spi_register_master(spi_master_get(master));
	if (ret)
		spi_master_put(master);

	return 0;
}

接下来我们就可以看一下spi_register_master()这个函数了。上一部分讲的函数主要作用是帮助注册spi设备,而这个函数的作用是向内核注册spi适配器设备,该函数同样位于spi.c中,只列出与我们的bug相关的代码:

c 复制代码
int spi_register_master(struct spi_master *master)
{
	static atomic_t		dyn_bus_id = ATOMIC_INIT((1<<15) - 1);
	struct device		*dev = master->dev.parent;
	struct boardinfo	*bi;
	int			status = -ENODEV;
	int			dynamic = 0;

	if (!dev)
		return -ENODEV;

	status = of_spi_register_master(master);
	...
	/* Register devices from the device tree and ACPI */
	of_register_spi_devices(master);		/* 这里会调用of_register_spi_device */
	acpi_register_spi_devices(master);
done:
	return status;
}

关键函数就是of_spi_register_master()这个函数,该函数同样定义在spi.c中,其源码如下:

c 复制代码
#ifdef CONFIG_OF
static int of_spi_register_master(struct spi_master *master)
{
	int nb, i, *cs;
	struct device_node *np = master->dev.of_node;
	printk("in of_spi_register_master?\r\n");  /* 我自己加的test,判断一下CONFIG_OF这个宏定义是否开启了 */
	if (!np)
		return 0;

	nb = of_gpio_named_count(np, "cs-gpios");
	master->num_chipselect = max_t(int, nb, master->num_chipselect);

	/* Return error only for an incorrectly formed cs-gpios property */
	if (nb == 0 || nb == -ENOENT)
		return 0;
	else if (nb < 0)
		return nb;

	cs = devm_kzalloc(&master->dev,
			  sizeof(int) * master->num_chipselect,
			  GFP_KERNEL);
	master->cs_gpios = cs;

	if (!master->cs_gpios)
		return -ENOMEM;

	for (i = 0; i < master->num_chipselect; i++)
		cs[i] = -ENOENT;

	for (i = 0; i < nb; i++)
		cs[i] = of_get_named_gpio(np, "cs-gpios", i);

	return 0;
}

从这个函数中可以看出,该函数首先调用of_gpio_named_count来统计cs-gpios这个属性中设置的gpio的个数。很不幸,我们在设备树中把属性写成了cs-gpio,所以该函数找不到节点,就会返回负值 。然后该函数将master->num_chipselect设定为了master->num_chipselectnb这两个变量中的最大值,master->num_chipselect这个变量会在.probe函数中通过读取设备树中spi适配器设备节点中的"fsl,spi-num-chipselects"属性来初始化。根据设备树来看,该属性的值被设为了1。

前文提到,master->cs_gpios是一个int类型的指针,所以在该函数中将他指向了一段使用devm_kzalloc()函数分配的内存,可以理解为现在master->cs_gpios就是一个数组了,里面有num_chipselect个数据

然后该函数调用两个for循环,来对master->cs_gpios这个数组进行初始化,因为master->num_chipselect为1,所以第一个for循环正常执行,csp[i] = -ENOENT而这个宏定义ENOENT恰恰就是2 。并且因为变量nb是个负数,所以第二个for不会执行,就算执行这个循环他也根本找不到"cs-gpios"这个属性(我写的设备树少了一个s)。

所以综上,master->cs_gpios全被初始化为了-2,导致第一部分提到的spi设备结构体中的变量(即spi->cs_gpio)也为-2,从而导致在调用gpio_set_value()函数来对spi->cs_gpio这个gpio号的时候没有正确设置。

四、BUG修复测试

前面已经说明了,把设备树中的属性改为"cs-gpios"应该就行了:

bash 复制代码
&ecspi3 {
        fsl,spi-num-chipselects = <1>;
        cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;		/* 必须使用cs-gpio属性来描述片选引脚,可能spi适配器驱动里用的就是这个名称"cs-gpio" */
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_ecspi3>;	/* 难不成是因为这里是pinctrl-0,这个0正好对应这第0通道,即icm20608 */
        status = "okay";

	spidev: icm20608@0 {
	compatible = "alientek,icm20608";
        spi-max-frequency = <8000000>;
        reg = <0>;
    };
};

然后重新编译设备树,测试结果如下:

gpio口获取也对了,也能正确读到icm20608的设备id了。

那如果我就是想用cs-gpio这个名呢?那也有办法,就是在你自己写的设备驱动程序中,使用of_get_named_gpio()函数来手动获取gpio号,但是传入该函数的属性名就不要是cs-gpios了,而应该是cs-gpio(如果用这样的方法相当于跳过spi框架和spi适配器驱动函数,那么你取啥名都可以,我把属性写成本gpios-cs也没问题)。然后再使用gpio_set_value()函数来设置这个gpio口即可

五、其他问题

其实有个问题不知道有没有注意到,就是在分析驱动程序的时候出现了一个#ifdef CONFIG_OF,上述很多函数都依赖于这个宏必须得被定义了才能起作用。这个宏我根本没找到在哪里定义的,如果没定义这个宏,那之前的分析就白费了,所以找了一个依赖于这个宏的函数进行测试:

c 复制代码
#ifdef CONFIG_OF
static int of_spi_register_master(struct spi_master *master)
{
	int nb, i, *cs;
	struct device_node *np = master->dev.of_node;
	printk("in of_spi_register_master?\r\n");  /* 我自己加的test,判断一下CONFIG_OF这个宏定义是否开启了 */
	...
	return 0;
}

然后重新编译内核,再次启动内核,启动过程中打印结果如下:

看来这个宏确实是定义了,但是定义在哪个位置我真没找到,有知道的老哥可以教一下。

相关推荐
C++忠实粉丝34 分钟前
Linux环境基础开发工具使用(2)
linux·运维·服务器
康熙38bdc1 小时前
Linux 环境变量
linux·运维·服务器
存储服务专家StorageExpert1 小时前
DELL SC compellent存储的四种访问方式
运维·服务器·存储维护·emc存储
hakesashou2 小时前
python如何比较字符串
linux·开发语言·python
Ljubim.te2 小时前
Linux基于CentOS学习【进程状态】【进程优先级】【调度与切换】【进程挂起】【进程饥饿】
linux·学习·centos
cooldream20092 小时前
Linux性能调优技巧
linux
大G哥2 小时前
记一次K8S 环境应用nginx stable-alpine 解析内部域名失败排查思路
运维·nginx·云原生·容器·kubernetes
醉颜凉2 小时前
银河麒麟桌面操作系统修改默认Shell为Bash
运维·服务器·开发语言·bash·kylin·国产化·银河麒麟操作系统
QMCY_jason3 小时前
Ubuntu 安装RUST
linux·ubuntu·rust
慕雪华年3 小时前
【WSL】wsl中ubuntu无法通过useradd添加用户
linux·ubuntu·elasticsearch