嵌入式linux/鸿蒙开发板(IMX6ULL)开发(三十五)驱动程序基石(上)

简介: 嵌入式linux/鸿蒙开发板(IMX6ULL)开发(三十五)驱动程序基石

1.1 休眠与唤醒


1.1.1 适用场景


在前面引入中断时,我们曾经举过一个例子:

1670924117006.jpg

妈妈怎么知道卧室里小孩醒了?

① 时不时进房间看一下:查询方式

简单,但是累

② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒

不累,但是妈妈干不了活了

③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式

要浪费点时间,但是可以继续干活。

妈妈要么是被小孩吵醒,要么是被闹钟吵醒。

④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知

妈妈、小孩互不耽误


当应用程序必须等待某个事件发生,比如必须等待按键被按下时,可以使用“休眠-唤醒”机制:

① APP调用read等函数试图读取数据,比如读取按键;

② APP进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回;

③ 如果APP在内核态,也就是在驱动程序中发现没有数据,则APP休眠;

④ 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒APP;

⑤ APP继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户空间并马上返回。


驱动中有数据时,下图中红线就是APP1的执行过程,涉及用户态、内核态:

1670924244788.jpg

驱动中没有数据时,APP1在内核态执行到drv_read时会休眠。所谓休眠就是把自己的状态改为非RUNNING,这样内核的调度器就不会让它运行。当按下按键,驱动程序中的中断服务程序被调用,它会记录数据,并唤醒APP1。所以唤醒就是把程序的状态改为RUNNING,这样内核的调度器有合适的时间就会让它运行。当APP1再次运行时,就会继续执行drv_read中剩下的代码,把数据复制回用户空间,返回用户空间。APP1的执行过程如下图的红色实线所示,它被分成了2段:

1670924257303.jpg

值得注意的是,上面2个图中红线部分都属于APP1的“上下文”,或者这样说:红线所涉及的代码,都是APP1调用的。但是按键的中断服务程序,不属于APP1的“上下文”,这是突如其来的,当中断发生时,APP1正在休眠呢。

在APP1的“上下文”,也就是在APP1的执行过程中,它是可以休眠的。

在中断的处理过程中,也就是gpio_key_irq的执行过程中,它不能休眠:“中断”怎么能休眠?“中断”休眠了,谁来调度其他APP啊?

所以,请记住:在中断处理函数中,不能休眠,也就不能调用会导致休眠的函数。


1.1.2 内核函数


1.1.2.1 休眠函数


参考内核源码:include\linux\wait.h。

1670924279066.jpg

比较重要的参数就是:

① wq:waitqueue,等待队列

休眠时除了把程序状态改为非RUNNING之外,还要把进程/进程放入wq中,以后中断服务程序要从wq中把它取出来唤醒。

没有wq的话,茫茫人海中,中断服务程序去哪里找到你?

② condition

这可以是一个变量,也可以是任何表达式。表示“一直等待,直到condition为真”。


1.1.2.2 唤醒函数


参考内核源码:include\linux\wait.h。

1670924287833.jpg


1.1.3 驱动框架


驱动框架如下:

1670924321388.jpg


要休眠的线程,放在wq队列里,中断处理函数从wq队列里把它取出来唤醒。

所以,我们要做这几件事:

① 初始化wq队列

② 在驱动的read函数中,调用wait_event_interruptible:

它本身会判断event是否为FALSE,如果为FASLE表示无数据,则休眠。

当从wait_event_interruptible返回后,把数据复制回用户空间。

③ 在中断服务程序里:

设置event为TRUE,并调用wake_up_interruptible唤醒线程。


1.1.4 编程


使用GIT命令载后,源码位于这个目录下:

01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\source\
06_gpio_irq\
    02_read_key_irq\ 和 03_read_key_irq_circle_buffer


03_read_key_irq_circle_buffer使用了环型缓冲区,可以避免按键丢失。


1.1.4.1 驱动程序关键代码


02_read_key_irq\gpio_key_drv.c中,要先定义“wait queue”:

41 static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

在驱动的读函数里调用wait_event_interruptible:

44 static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
45 {
46      //printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
47      int err;
48
49      wait_event_interruptible(gpio_key_wait, g_key);
50      err = copy_to_user(buf, &g_key, 4);
51      g_key = 0;
52
53      return 4;
54 }


第49行并不一定会进入休眠,它会先判断g_key是否为TRUE。

执行到第50行时,表示要么有了数据(g_key为TRUE),要么有信号等待处理(本节课程不涉及信号)。


假设g_key等于0,那么APP会执行到上述代码第49行时进入休眠状态。它被谁唤醒?被控制的中断服务程序:

64 static irqreturn_t gpio_key_isr(int irq, void *dev_id)
65 {
66      struct gpio_key *gpio_key = dev_id;
67      int val;
68      val = gpiod_get_value(gpio_key->gpiod);
69
70
71      printk("key %d %d\n", gpio_key->gpio, val);
72      g_key = (gpio_key->gpio << 8) | val;
73      wake_up_interruptible(&gpio_key_wait);
74
75      return IRQ_HANDLED;
76 }


上述代码中,第72行确定按键值g_key,g_key也就变为TRUE了。

然后在第73行唤醒gpio_key_wait中的第1个线程。

注意这2个函数,一个没有使用“&”,另一个使用了“&”:

wait_event_interruptible(gpio_key_wait, g_key);
wake_up_interruptible(&gpio_key_wait);


1.1.4.2 应用程序


应用程序并不复杂,调用open、read即可,代码在button_test.c中:

25      /* 2. 打开文件 */
26      fd = open(argv[1], O_RDWR);
27      if (fd == -1)
28      {
29              printf("can not open file %s\n", argv[1]);
30              return -1;
31      }
32
33      while (1)
34      {
35              /* 3. 读文件 */
36              read(fd, &val, 4);
37              printf("get button : 0x%x\n", val);
38      }


在33行~38行的循环中,APP基本上都是休眠状态。你可以执行top命令查看CPU占用率。


1.1.5 上机实验


跟上一节视频类似,需要先修改设备树,请使用上一节视频的设备树文件。

然后安装驱动程序,运行测试程序。

# insmod -f gpio_key_drv.ko
# ls /dev/100ask_gpio_key
/dev/100ask_gpio_key
# ./button_test /dev/100ask_gpio_key  &
# top


1.1.6 使用环形缓冲区改进驱动程序


使用GIT命令载后,源码位于这个目录下:

01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\source\
06_gpio_irq\
    03_read_key_irq_circle_buffer


使用环形缓冲区,可以在一定程序上避免按键数据丢失,关键代码如下:

1670924447886.jpg


使用环形缓冲区之后,休眠函数可以这样写:

86      wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
87      key = get_key();
88      err = copy_to_user(buf, &key, 4);


唤醒函数可以这样写:

111     key = (gpio_key->gpio << 8) | val;
112     put_key(key);
113     wake_up_interruptible(&gpio_key_wait);


1.2 POLL机制


使用GIT命令载后,本节源码位于这个目录下:

01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\source\
06_gpio_irq\
    04_read_key_irq_poll


1.2.1 适用场景


在前面引入中断时,我们曾经举过一个例子:

1670924502269.jpg

妈妈怎么知道卧室里小孩醒了?

① 时不时进房间看一下:查询方式

简单,但是累

② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒

不累,但是妈妈干不了活了

③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式

要浪费点时间,但是可以继续干活。

妈妈要么是被小孩吵醒,要么是被闹钟吵醒。

④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知

妈妈、小孩互不耽误


使用休眠-唤醒的方式等待某个事件发生时,有一个缺点:等待的时间可能很久。我们可以加上一个超时时间,这时就可以使用poll机制。

① APP不知道驱动程序中是否有数据,可以先调用poll函数查询一下,poll函数可以传入超时时间;

② APP进入内核态,调用到驱动程序的poll函数,如果有数据的话立刻返回;

③ 如果发现没有数据时就休眠一段时间;

④ 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒APP;

⑤ 当超时时间到了之后,内核也会唤醒APP;

⑥ APP根据poll函数的返回值就可以知道是否有数据,如果有数据就调用read得到数据


1.2.2 使用流程


妈妈进入房间时,会先看小孩醒没醒,闹钟响之后走出房间之前又会再看小孩醒没醒。

注意:看了2次小孩!

POLL机制也是类似的,流程如下:

1670924520373.jpg

函数执行流程如上图①~⑧所示,重点从③开始看。假设一开始无按键数据:

③ APP调用poll之后,进入内核态;

④ 导致驱动程序的drv_poll被调用:

注意,drv_poll要把自己这个线程挂入等待队列wq中;假设不放入队列里,那以后发生中断时,中断服务程序去哪里找到你嘛?

drv_poll还会判断一下:有没有数据啊?返回这个状态。

⑤ 假设当前没有数据,则休眠一会;

⑥ 在休眠过程中,按下了按键,发生了中断:

在中断服务程序里记录了按键值,并且从wq中把线程唤醒了。

⑦ 线程从休眠中被唤醒,继续执行for循环,再次调用drv_poll:

drv_poll返回数据状态

⑧ 哦,你有数据,那从内核态返回到应用态吧

⑨ APP调用read函数读数据

如果一直没有数据,调用流程也是类似的,重点从③开始看,如下:

③ APP调用poll之后,进入内核态;

④ 导致驱动程序的drv_poll被调用:

注意,drv_poll要把自己这个线程挂入等待队列wq中;假设不放入队列里,那以后发生中断时,中断服务程序去哪里找到你嘛?

drv_poll还会判断一下:有没有数据啊?返回这个状态。

⑤ 假设当前没有数据,则休眠一会;

⑥ 在休眠过程中,一直没有按下了按键,超时时间到:内核把这个线程唤醒;

⑦ 线程从休眠中被唤醒,继续执行for循环,再次调用drv_poll:

drv_poll返回数据状态

⑧ 哦,你还是没有数据,但是超时时间到了,那从内核态返回到应用态吧

⑨ APP不能调用read函数读数据


注意几点:

① drv_poll要把线程挂入队列wq,但是并不是在drv_poll中进入休眠,而是在调用drv_poll之后休眠

② drv_poll要返回数据状态

③ APP调用一次poll,有可能会导致drv_poll被调用2次

④ 线程被唤醒的原因有2:中断发生了去队列wq中把它唤醒,超时时间到了内核把它唤醒

⑤ APP要判断poll返回的原因:有数据,还是超时。有数据时再去调用read函数。


1.2.3 驱动编程


使用poll机制时,驱动程序的核心就是提供对应的drv_poll函数。

在drv_poll函数中要做2件事:

① 把当前线程挂入队列wq:poll_wait

APP调用一次poll,可能导致drv_poll被调用2次,但是我们并不需要把当前线程挂入队列2次。

可以使用内核的函数poll_wait把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。

② 返回设备状态:

APP调用poll函数时,有可能是查询“有没有数据可以读”:POLLIN,也有可能是查询“你有没有空间给我写数据”:POLLOUT。

所以drv_poll要返回自己的当前状态:(POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。

POLLRDNORM等同于POLLIN,为了兼容某些APP把它们一起返回。

POLLWRNORM等同于POLLOUT ,为了兼容某些APP把它们一起返回。


APP调用poll后,很有可能会休眠。对应的,在按键驱动的中断服务程序中,也要有唤醒操作。

驱动程序中poll的代码如下:

static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{
  printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
  poll_wait(fp, &gpio_key_wait, wait);
  return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}


1.2.4 应用编程


注意:APP可以调用poll或select函数,这2个函数的作用是一样的。

poll/select函数可以监测多个文件,可以监测多种事件:

1670924556071.jpg

在调用poll函数时,要指明:

① 你要监测哪一个文件:哪一个fd

② 你想监测这个文件的哪种事件:是POLLIN、还是POLLOUT

最后,在poll函数返回时,要判断状态。


应用程序代码如下:

struct pollfd fds[1];
int timeout_ms = 5000;
int ret;
fds[0].fd = fd;
fds[0].events = POLLIN;
ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN))
{
  read(fd, &val, 4);
  printf("get button : 0x%x\n", val);
}


1.2.5 现场编程


1.2.6 上机实验


1.2.7 POLL机制的内核代码详解


Linux APP系统调用,基本都可以在它的名字前加上“sys_”前缀,这就是它在内核中对应的函数。比如系统调用open、read、write、poll,与之对应的内核函数为:sys_open、sys_read、sys_write、sys_poll。

对于系统调用poll或select,它们对应的内核函数都是sys_poll。分析sys_poll,即可理解poll机制。


1.2.7.1 sys_poll函数


sys_poll位于fs/select.c文件中,代码如下:

SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds,
  int, timeout_msecs)
{
  struct timespec64 end_time, *to = NULL;
  int ret;
  if (timeout_msecs >= 0) {
  to = &end_time;
  poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC,
    NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
  }
  ret = do_sys_poll(ufds, nfds, to);
……


SYSCALL_DEFINE3是一个宏,它定义于include/linux/syscalls.h,展开后就有sys_poll函数。

sys_poll对超时参数稍作处理后,直接调用do_sys_poll。


1.2.7.2 do_sys_poll函数


do_sys_poll位于fs/select.c文件中,我们忽略其他代码,只看关键部分:

int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
  struct timespec64 *end_time)
{
……
  poll_initwait(&table);
  fdcount = do_poll(head, &table, end_time);
  poll_freewait(&table);
……
}


poll_initwait函数非常简单,它初始化一个poll_wqueues变量table:

poll_initwait
init_poll_funcptr(&pwq->pt, __pollwait);
pt->qproc = qproc


即table->pt->qproc = __pollwait,__pollwait将在驱动的poll函数里用到。

do_poll函数才是核心,继续看代码。


1.2.7.3 do_poll函数


do_poll函数位于fs/select.c文件中,这是POLL机制中最核心的代码,贴图如下:

1670924644234.jpg

① 从这里开始,将会导致驱动程序的poll函数被第一次调用。

沿着②③④⑤,你可以看到:驱动程序里的poll_wait会调用__pollwait函数把线程放入某个队列。

当执行完①之后,在⑥或⑦处,pt->_qproc被设置为NULL,所以第二次调用驱动程序的poll时,不会再次把线程放入某个队列里。

⑧ 如果驱动程序的poll返回有效值,则count非0,跳出循环;

⑨ 否则休眠一段时间;当休眠时间到,或是被中断唤醒时,会再次循环、再次调用驱动程序的poll。


回顾APP的代码,APP可以指定“想等待某些事件”,poll函数返回后,可以知道“发生了哪些事件”:

1670924654599.jpg

驱动程序里怎么体现呢?在上上一个图中,看②位置处,细说如下:

1670924666372.jpg


1.3 异步通知


使用GIT命令载后,本节源码位于这个目录下:

01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\source\
06_gpio_irq\
    05_read_key_irq_poll_fasync


1.3.1 适用场景


在前面引入中断时,我们曾经举过一个例子:

1670924701663.jpg

妈妈怎么知道卧室里小孩醒了?

① 时不时进房间看一下:查询方式

简单,但是累

② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒

不累,但是妈妈干不了活了

③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式

要浪费点时间,但是可以继续干活。

妈妈要么是被小孩吵醒,要么是被闹钟吵醒。

④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知

妈妈、小孩互不耽误

使用休眠-唤醒、POLL机制时,都需要休眠等待某个事件发生时,它们的差别在于后者可以指定休眠的时长。

在现实生活中:妈妈可以不陪小孩睡觉,小孩醒了之后可以主动通知妈妈。

如果APP不想休眠怎么办?也有类似的方法:驱动程序有数据时主动通知APP,APP收到信号后执行信息处理函数。


什么叫“异步通知”?

你去买奶茶:

你在旁边等着,眼睛盯着店员,生怕别人插队,他一做好你就知道:你是主动等待他做好,这叫“同步”。

你付钱后就去玩手机了,店员做好后他会打电话告诉你:你是被动获得结果,这叫“异步”。


1.3.2 使用流程


驱动程序怎么通知APP:发信号,这只有3个字,却可以引发很多问题:

① 谁发:驱动程序发

② 发什么:信号

③ 发什么信号:SIGIO

④ 怎么发:内核里提供有函数

⑤ 发给谁:APP,APP要把自己告诉驱动

⑥ APP收到后做什么:执行信号处理函数

⑦ 信号处理函数和信号,之间怎么挂钩:APP注册信号处理函数


小孩通知妈妈的事情有很多:饿了、渴了、想找人玩。

Linux系统中也有很多信号,在Linux内核源文件include\uapi\asm-generic\signal.h中,有很多信号的宏定义:

1670924716928.jpg

就APP而言,你想处理SIGIO信息,那么需要提供信号处理函数,并且要跟SIGIO挂钩。这可以通过一个signal函数来“给某个信号注册处理函数”,用法如下:

1670924726188.jpg

APP还要做什么事?想想这几个问题:

① 内核里有那么多驱动,你想让哪一个驱动给你发SIGIO信号?

APP要打开驱动程序的设备节点。

② 驱动程序怎么知道要发信号给你而不是别人?

APP要把自己的进程ID告诉驱动程序。

③ APP有时候想收到信号,有时候又不想收到信号:

应该可以把APP的意愿告诉驱动。


驱动程序要做什么?发信号。

① APP设置进程ID时,驱动程序要记录下进程ID;

② APP还要使能驱动程序的异步通知功能,驱动中有对应的函数:

APP打开驱动程序时,内核会创建对应的file结构体,file中有f_flags;

f_flags中有一个FASYNC位,它被设置为1时表示使能异步通知功能。

当f_flags中的FASYNC位发生变化时,驱动程序的fasync函数被调用。

③ 发生中断时,有数据时,驱动程序调用内核辅助函数发信号。

这个辅助函数名为kill_fasync。


完美!

APP收到信号后,是怎么执行信号处理函数的?

这个,很难,有兴趣的话就看本节最后的文档。初学者没必要看。


综上所述,使用异步通知,也就是使用信号的流程如下图所示:

1670924738661.jpg

重点从②开始:

② APP给SIGIO这个信号注册信号处理函数func,以后APP收到SIGIO信号时,这个函数会被自动调用;

③ 把APP的PID(进程ID)告诉驱动程序,这个调用不涉及驱动程序,在内核的文件系统层次记录PID;

④ 读取驱动程序文件Flag;

⑤ 设置Flag里面的FASYNC位为1:当FASYNC位发生变化时,会导致驱动程序的fasync被调用;

⑥⑦ 调用faync_helper,它会根据FAYSNC的值决定是否设置button_async->fa_file=驱动文件filp:

驱动文件filp结构体里面含有之前设置的PID。

⑧ APP可以做其他事;

⑨⑩ 按下按键,发生中断,驱动程序的中断服务程序被调用,里面调用kill_fasync发信号;

⑪⑫⑬ APP收到信号后,它的信号处理函数被自动调用,可以在里面调用read函数读取按键。


1.3.3 驱动编程


使用异步通知时,驱动程序的核心有2:

① 提供对应的drv_fasync函数;

② 并在合适的时机发信号。


drv_fasync函数很简单,调用fasync_helper函数就可以,如下:

static struct fasync_struct *button_async;
static int drv_fasync (int fd, struct file *filp, int on)
{
  return fasync_helper (fd, filp, on, &button_async);
}


fasync_helper函数会分配、构造一个fasync_struct结构体button_async:

① 驱动文件的flag被设置为FAYNC时:

button_async->fa_file = filp;  // filp表示驱动程序文件,里面含有之前设置的PID

② 驱动文件被设置为非FASYNC时:

button_async->fa_file = NULL;


以后想发送信号时,使用button_async作为参数就可以,它里面“可能”含有PID。


什么时候发信号呢?在本例中,在GPIO中断服务程序中发信号。

怎么发信号呢?代码如下:

kill_fasync (&button_async, SIGIO, POLL_IN);


第1个参数:button_async->fa_file非空时,可以从中得到PID,表示发给哪一个APP;

第2个参数表示发什么信号:SIGIO;

第3个参数表示为什么发信号:POLL_IN,有数据可以读了。(APP用不到这个参数)

相关文章
|
22天前
|
编解码 Linux
FFmpeg开发笔记(二十八)Linux环境给FFmpeg集成libxvid
XviD是开源的MPEG-4视频编解码器,曾与DivX一起用于早期MP4视频编码,但现在已被H.264取代。要集成XviD到Linux上的FFmpeg,首先下载源码,解压后配置并编译安装libxvid。接着,在FFmpeg源码目录中,重新配置FFmpeg以启用libxvid,然后编译并安装。成功后,通过`ffmpeg -version`检查是否启用libxvid。详细步骤包括下载、解压libxvid,使用`configure`和`make`命令安装,以及更新FFmpeg配置并安装。
38 2
FFmpeg开发笔记(二十八)Linux环境给FFmpeg集成libxvid
|
2天前
|
存储 网络协议 Ubuntu
【Linux开发实战指南】基于UDP协议的即时聊天室:快速构建登陆、聊天与退出功能
UDP 是一种无连接的、不可靠的传输层协议,位于IP协议之上。它提供了最基本的数据传输服务,不保证数据包的顺序、可靠到达或无重复。与TCP(传输控制协议)相比,UDP具有较低的传输延迟,因为省去了建立连接和确认接收等过程,适用于对实时性要求较高、但能容忍一定数据丢失的场景,如在线视频、语音通话、DNS查询等。 链表 链表是一种动态数据结构,用于存储一系列元素(节点),每个节点包含数据字段和指向下一个节点的引用(指针)。链表分为单向链表、双向链表和循环链表等类型。与数组相比,链表在插入和删除操作上更为高效,因为它不需要移动元素,只需修改节点间的指针即可。但访问链表中的元素不如数组直接,通常需要从
|
2天前
|
SQL 自然语言处理 网络协议
【Linux开发实战指南】基于TCP、进程数据结构与SQL数据库:构建在线云词典系统(含注册、登录、查询、历史记录管理功能及源码分享)
TCP(Transmission Control Protocol)连接是互联网上最常用的一种面向连接、可靠的、基于字节流的传输层通信协议。建立TCP连接需要经过著名的“三次握手”过程: 1. SYN(同步序列编号):客户端发送一个SYN包给服务器,并进入SYN_SEND状态,等待服务器确认。 2. SYN-ACK:服务器收到SYN包后,回应一个SYN-ACK(SYN+ACKnowledgment)包,告诉客户端其接收到了请求,并同意建立连接,此时服务器进入SYN_RECV状态。 3. ACK(确认字符):客户端收到服务器的SYN-ACK包后,发送一个ACK包给服务器,确认收到了服务器的确
|
20天前
|
Linux 程序员 芯片
【Linux驱动】普通字符设备驱动程序框架
【Linux驱动】普通字符设备驱动程序框架
|
22天前
|
Linux Windows 虚拟化
【Linux环境搭建实战手册】:打造高效开发空间的秘籍
【Linux环境搭建实战手册】:打造高效开发空间的秘籍
|
2天前
|
运维 监控 大数据
部署-Linux01,后端开发,运维开发,大数据开发,测试开发,后端软件,大数据系统,运维监控,测试程序,网页服务都要在Linux中进行部署
部署-Linux01,后端开发,运维开发,大数据开发,测试开发,后端软件,大数据系统,运维监控,测试程序,网页服务都要在Linux中进行部署
|
6天前
|
NoSQL Linux 开发工具
【linux】在linux操作系统下快速熟悉开发环境并上手开发工具——体验不一样的开发之旅
【linux】在linux操作系统下快速熟悉开发环境并上手开发工具——体验不一样的开发之旅
|
10天前
|
移动开发 程序员 Linux
老程序员分享:linux驱动开发笔记_ioctl函数
老程序员分享:linux驱动开发笔记_ioctl函数
|
11月前
|
IDE JavaScript API
HarmonyOS开发第一步,熟知开发工具DevEco Studio
本文主要以常见的功能点作为概述希望可以帮助到学习HarmonyOS的开发者。
294 0
|
2月前
|
JavaScript API
鸿蒙开发接口UI界面:【@ohos.mediaquery (媒体查询)】
鸿蒙开发接口UI界面:【@ohos.mediaquery (媒体查询)】
29 1