2024年1月

namespace-kind


深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
一文中我们分析了 Docker 是由三大核心技术实现的。

docker-core

今天就一起分析 Docker 三大核心技术之一的 Linux Namespace。

后续文章会演示如何从零实现一个简易的 Docker,这里先简单了解下 Docker 的核心原理。


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【
探索云原生
】即可订阅


当我们通过
docker run -it
启动并进入一个容器之后,会发现不论是进程、网络还是文件系统,好像都被隔离了,就像这样:

[root@docker cpu]# docker run -it busybox
/ # 
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 sh
    7 root      0:00 ps
/ # ip a 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
120: eth0@if121: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
/ # ls
bin    dev    etc    home   lib    lib64  proc   root   sys    tmp    usr    var
  • ps 命令看不到宿主机上的进程
  • ip 命令也只能看到容器内部的网卡
  • ls 命令看到的文件好像也和宿主机不一样

这就是
Docker 核心之一,借助 Linux Namespace 技术实现了视图隔离。

看起来容器和宿主机隔离开了

接下来就让我们一起探索一下 Linux Namespace 以及 Docker 是如何借助该能力来实现隔离的。

1. 什么是 Linux 命名空间(Namespace)?

Namespace 是 Linux 提供的一种内核级别环境隔离的方法。

可以使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。

简单来说:namespace 就是对资源的逻辑隔离

目前,Linux 内核里面实现了 8 种不同类型的 namespace:

namespace-kind

分类 系统调用参数 隔离内容 相关内核版本
Mount namespaces CLONE_NEWNS Mount points Linux 2.4.19
UTS namespaces CLONE_NEWUTS Hostname and NIS domain name Linux 2.6.19
IPC namespaces CLONE_NEWIPC System V IPC, POSIX message queues Linux 2.6.19
PID namespaces CLONE_NEWPID Process IDs Linux 2.6.24
Network namespaces CLONE_NEWNET Network devices, stacks, ports, etc. 始于Linux 2.6.24 完成于 Linux 2.6.29
User namespaces CLONE_NEWUSER User and group IDs 始于 Linux 2.6.23 完成于 Linux 3.8)
Cgroup namespace CLONE_NEWCGROUP Cgroup root directory Linux 4.6
Time namespace CLONE_NEWTIME Boot and monotonic Linux 5.6

前面 6 种是比较常见的,后面两种 Cgroup Namespace 以及 Time Namespace 则是比较少见。

相关API

和 namespace 相关的函数只有四个,这里简单的看一下:

namespace-api

  • clone
  • setns
  • unshare
  • ioctl_ns

clone:创建一个新的进程并把他放到新的 namespace 中。

int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
                 /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
 /*
 flags:
    指定一个或者多个上面的CLONE_NEW*(当然也可以包含跟namespace无关的flags), 
    这样就会创建一个或多个新的不同类型的namespace, 
    并把新创建的子进程加入新创建的这些namespace中。
 */

setns:将当前进程加入到已有的 namespace 中。

int setns(int fd, int nstype);
/*
fd:
    指向/proc/[pid]/ns/目录里相应namespace对应的文件,
    表示要加入哪个namespace
nstype:
    指定namespace的类型(上面的任意一个CLONE_NEW*):
    1. 如果当前进程不能根据fd得到它的类型,如fd由其他进程创建,
    并通过UNIX domain socket传给当前进程,
    那么就需要通过nstype来指定fd指向的namespace的类型
    2. 如果进程能根据fd得到namespace类型,比如这个fd是由当前进程打开的,
    那么nstype设置为0即可
*/

unshare:使当前进程退出指定类型的 namespace,并加入到新创建的 namespace(相当于创建并加入新的 namespace)。

int unshare(int flags);
/*
flags:
    指定一个或者多个上面的CLONE_NEW*,
    这样当前进程就退出了当前指定类型的namespace并加入到新创建的namespace
*/

ioctl_ns:查询 namespace 信息。

new_fd = ioctl(fd, request);
/*
fd: 指向/proc/[pid]/ns/目录里相应namespace对应的文件
request: 
  NS_GET_USERNS: 返回指向拥有用户的文件描述符namespace fd引用的命名空间
    NS_GET_PARENT: 返回引用父级的文件描述符由fd引用的命名空间的命名空间。
*/

看完之后大致可以这样分类:

  • clone、unshare:加入新 namespace
  • setsns:加入已有 namespace
  • ioctl_ns:主要用于查询

clone 和 unshare 都是加入新 namespace,二者有什么区别呢?

二者的功能都是创建并加入新的 namespace, 区别在于:

  • unshare 是使 当前进程 加入新的 namespace
  • clone 是创建一个新的子进程,然后让 子进程 加入新的 namespace,而当前进程保持不变

查看进程所属的 namespaces

系统中的每个进程都有
/proc/[pid]/ns/
这样一个目录,里面包含了这个进程所属 namespace 的信息,里面每个文件的描述符都可以用来作为 setns 函数(后面会介绍)的参数。

#查看当前bash进程所属的namespace
 lixd  ~  ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 lixd lixd 0 Jan  6 19:00 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 lixd lixd 0 Jan  6 19:00 ipc -> 'ipc:[4026532227]'
lrwxrwxrwx 1 lixd lixd 0 Jan  6 19:00 mnt -> 'mnt:[4026532241]'
lrwxrwxrwx 1 lixd lixd 0 Jan  6 19:00 net -> 'net:[4026531992]'
lrwxrwxrwx 1 lixd lixd 0 Jan  6 19:00 pid -> 'pid:[4026532243]'
lrwxrwxrwx 1 lixd lixd 0 Jan  6 19:00 pid_for_children -> 'pid:[4026532243]'
lrwxrwxrwx 1 lixd lixd 0 Jan  6 19:00 user -> 'user:[4026531837]'
lrwxrwxrwx 1 lixd lixd 0 Jan  6 19:00 uts -> 'uts:[4026532242]


ipc:[4026532227]
为例,其中 ipc 是 namespace 的类型,4026532227 是 inode number。

如果两个进程的 ipc namespace 的 inode number一样,说明他们属于同一个 namespace。

这条规则对其他类型的 namespace 也同样适用。

namespace 数量限制与回收策略

linux 也限制了 namespace 的数量,不能无限制的创建 namespace,具体限制一般在
/proc/sys/user
目录中。具体如下:

$ tree /proc/sys/user/
/proc/sys/user/
├── max_cgroup_namespaces
├── max_inotify_instances
├── max_inotify_watches
├── max_ipc_namespaces
├── max_mnt_namespaces
├── max_net_namespaces
├── max_pid_namespaces
├── max_user_namespaces
└── max_uts_namespaces

$ cat /proc/sys/user/max_pid_namespaces
6784

以看到,当前系统中 PID namespace 最多可以创建 6784 个。

既然数量有限制,那已经创建的 namespace 什么时候会被销毁回收呢?

规则还是比较好理解的:当一个 namespace 中的所有进程都结束或者移出该 namespace 时,该 namespace 将会被销毁。

这也解释了为什么没有创建 namespace 的 API,因为刚创建的 namespace 没有任何进程,立马就会被回收。

不过也有一些特殊情况,可以再没有进程的时候保留 namespace:

  • 存在打开的 FD,或者对
    /proc/[pid]/ns/*
    执行了 bind mount
  • 存在子 namespace
  • 它是一个拥有一个或多个非用户 namespace 的 namespace。
  • 它是一个 PID namespace,并且有一个进程通过 /proc/[pid]/ns/pid_for_children 符号链接引用了这个 namespace。
  • 它是一个 Time namespace,并且有一个进程通过 /proc/[pid]/ns/time_for_children 符号链接引用了这个 namespace。
  • 它是一个 IPC namespace,并且有一个 mqueue 文件系统的 mount 引用了该 namespace
  • 它是一个 PIDnamespace,并且有一个 proc 文件系统的 mount 引用了该 namespace

一句话描述:
当 namespace 有被使用时就不会被回收,反之则会被回收。

2. 使用 Go 语言操作 Linux 命名空间示例

UTS Namespace

UTS Namespace主要用来隔离nodename和domainname两个系统标识

在UTS Namespace里面,每个Namespace允许有自己的hostname.

以下程序展示了如何在 Go 中切换 UTS Namespace。

// 注: 运行时需要 root 权限。
func main() {
	cmd := exec.Command("bash")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS,
	}

	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		log.Fatalln(err)
	}
}

运行并测试

DESKTOP-9K4GB6E# go run main.go
root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker#

运行后会进入了一个新的 shell 环境。

查看以下是否真的进入了新的 UTS Namespace。

首先使用
pstree
查看进程关系:

root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# pstree -pl
init(1)─┬─init(1272)───init(1273)───server(1274)─┬─{server}(1282)
        │                                        ├─{server}(1283)
        │                                        ├─{server}(1284)
        │                                        ├─{server}(1285)
        │                                        ├─{server}(1286)
        │                                        ├─{server}(1287)
        │                                        ├─{server}(1288)
        │                                        └─{server}(1289)
        ├─init(3701)───init(3702)───zsh(3703)───su(7520)───bash(7521)───zsh(7575)───go(8104)─┬─main(8182)─┬─bash(8187)───pstree(8194)
        │                                                                                    │            ├─{main}(8183)
        │                                                                                    │            ├─{main}(8184)
        │                                                                                    │            ├─{main}(8185)
        │                                                                                    │            └─{main}(8186)
        │                                                                                    ├─{go}(8105)
        │                                                                                    ├─{go}(8106)
        │                                                                                    ├─{go}(8107)
        │                                                                                    ├─{go}(8108)
        │                                                                                    ├─{go}(8109)
        │                                                                                    ├─{go}(8110)
        │                                                                                    ├─{go}(8666666)
        │                                                                                    ├─{go}(8112)
        │                                                                                    ├─{go}(8117)
        │                                                                                    └─{go}(8143)
        ├─init(3763)───init(3764)───zsh(3765)
        ├─init(5171)───init(5172)───fsnotifier-wsl(5173)
        ├─init(7459)───init(7460)───bash(7461)───su(7476)───bash(7477)
        ├─{init}(5)
        └─{init}(6)

主要关注这条:

├─init(3701)───init(3702)───zsh(3703)───su(7520)───bash(7521)───zsh(7575)───go(8104)─┬─main(8182)─┬─bash(8187)

main 程序 pid 为 8182,后续新创建的 bash pid 为 8187,现在查看二者 uts 是否相同即可:

root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# readlink /proc/8182/ns/uts
uts:[4026532242]
root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# readlink /proc/8187/ns/uts
uts:[4026532386]

可以发现二者确实不在一个 UTS Namespace 中。由于 UTS Namespace hostname 做了隔离 所以在这个环境内修改 hostname 应该不影响外部主机, 下面来做 下实验。

在这个新的 bash 环境中修改 hostname

root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# hostname bash
root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# hostname
bash

新开一个在宿主机上查看 hostname:

 lixd  ~ $ hostname
DESKTOP-9K4GB6E

可以看到外部的 hostname 并没有被修改影响,由此可了解 UTS Namespace 的作用。

IPC Namespace

IPC Namespace 用来隔离 sys V IPC和 POSIX message queues

每个 IPC Namespace 都有自己的 Sys V IPC 和 POSIX message queues。

微调一下程序,只是修改了 Cloneflags,新增了 CLONE_NEWIPC,表示同时创建 IPC Namespace。

// 注: 运行时需要 root 权限。
func main() {
	cmd := exec.Command("bash")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		// Cloneflags: syscall.CLONE_NEWUTS,
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
	}

	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		log.Fatalln(err)
	}
}

运行并测试:

# 先查看宿主机上的 ipc message queue
DESKTOP-9K4GB6E# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

# 然后创建一个 
DESKTOP-9K4GB6E# ipcmk -Q
Message queue id: 0
# 再次查看,发现有了
DESKTOP-9K4GB6E# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x70ffd07c 0          root       644        0            0

运行程序进入新的 shell

DESKTOP-9K4GB6E# go run main.go
root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

可以发现,在新的 Namespace 中已经看不到宿主机上的 message queue 了。说明 IPC Namespace 创建成功,IPC 已经被隔离。

PID Namespace

PID Namespace是用来隔离进程ID的

同样一个进程在不同的PID Namespace里可以拥有不同的PID。

这样就可以理解,在docker container 里面,使用ps -ef经常会发现,在容器内,前台运行的那个进程PID是1,但是在容器外,使用ps -ef会发现同样的进程却有不同的PID,这就是PID Namespace做的事情。

再次调整程序,增加 PID flags:

// 注: 运行时需要 root 权限。
func main() {
	cmd := exec.Command("bash")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		// Cloneflags: syscall.CLONE_NEWUTS,
		// Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
	}

	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		log.Fatalln(err)
	}
}

运行并测试:

DESKTOP-9K4GB6E# go run main.go
root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# pstree -pl
init(1)─┬─init(1272)───init(1273)───server(1274)─┬─{server}(1282)
        │                                        ├─{server}(1283)
        │                                        ├─{server}(1284)
        │                                        ├─{server}(1285)
        │                                        ├─{server}(1286)
        │                                        ├─{server}(1287)
        │                                        ├─{server}(1288)
        │                                        └─{server}(1289)
        ├─init(3701)───init(3702)───zsh(3703)───su(7520)───bash(7521)───zsh(7575)───go(9103)─┬─main(9184)─┬─bash(9189)───pstree(9196)
        │                                                                                    │            ├─{main}(9185)
        │                                                                                    │            ├─{main}(9186)
        │                                                                                    │            ├─{main}(9187)
        │                                                                                    │            └─{main}(9188)
        │                                                                                    ├─{go}(9104)
        │                                                                                    ├─{go}(9105)
        │                                                                                    ├─{go}(9106)
        │                                                                                    ├─{go}(9107)
        │                                                                                    ├─{go}(9108)
        │                                                                                    ├─{go}(9109)
        │                                                                                    ├─{go}(9110)
        │                                                                                    ├─{go}(9666666)
        │                                                                                    ├─{go}(9112)
        │                                                                                    └─{go}(9120)
        ├─init(3763)───init(3764)───zsh(3765)
        ├─init(5171)───init(5172)───fsnotifier-wsl(5173)
        ├─init(7459)───init(7460)───bash(7461)───su(7476)───bash(7477)
        ├─init(8201)───init(8202)───zsh(8203)
        ├─{init}(5)
        └─{init}(6)

可以看到 main 函数 pid 为 9184,而新开的 bash pid 为 9189。

然后再新开的 bash 中查看自己的 pid:

这里只能使用
echo $$
命令查看,ps、top 等命令会查看到其他 Namespace 中的信息。

root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# echo $$
1

发现 pid 是1,说明再新开的 PID Namespace 中只有一个 bash 这个进程,而且被伪装成了 1 号进程。

Mount Namespace

Mount Namespace用来隔离各个进程看到的挂载点视图

在不同Namespace的进程中,看到的文件系统层次是不一样的。 在Mount Namespace中调用mount()和umount()仅仅只会影响当前Namespace内的文件系统,而对全局的文件系统是没有影响的。

看到这里,也许就会想到chroot(),它也是将某一个子目录变成根节点。但是,Mount Namespace不仅能实现这个功能,而且能以更加灵活和安全的方式实现。

需要注意的是,Mount Namespace 的 flag 是
CLONE_NEWNS
,直接是 NEWNS 而不是 NEWMOUNT,因为 Mount Namespace 是 Linux 中实现的第一个 Namespace,当时也没想到后续会有很多类型的 Namespace 加入。

再次修改代码,增加 Mount Namespace 的 flag

	cmd.SysProcAttr = &syscall.SysProcAttr{
		// Cloneflags: syscall.CLONE_NEWUTS,
		// Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
		// Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
	}

运行并测试:

首先运行程序并在新的 bash 环境中查看 /proc

DESKTOP-9K4GB6E# go run main.go
root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# ls /proc
1     3764  7476  9476       cmdline    driver       kallsyms     loadavg  net           swaps        vmallocinfo
1272  3765  7477  9557       config.gz  execdomains  kcore        locks    pagetypeinfo  sys          vmstat
1273  5171  7520  9562       consoles   filesystems  key-users    mdstat   partitions    sysvipc      zoneinfo
1274  5172  7521  9569       cpuinfo    fs           keys         meminfo  sched_debug   thread-self
3701  5173  7575  acpi       crypto     interrupts   kmsg         misc     schedstat     timer_list
3702  7459  8201  buddyinfo  devices    iomem        kpagecgroup  modules  self          tty
3703  7460  8202  bus        diskstats  ioports      kpagecount   mounts   softirqs      uptime
3763  7461  8203  cgroups    dma        irq          kpageflags   mtrr     stat          version

可以看到,有一大堆文件,这是因为现在查看到的其实是宿主机上的 /proc 目录。

现在把 proc 目录挂载到当前 Namespace 中来:

root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# mount -t proc proc /proc

root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# ls /proc
1          cmdline    diskstats    interrupts  key-users    loadavg  mounts        schedstat  sysvipc      vmallocinfo
10         config.gz  dma          iomem       keys         locks    mtrr          self       thread-self  vmstat
acpi       consoles   driver       ioports     kmsg         mdstat   net           softirqs   timer_list   zoneinfo
buddyinfo  cpuinfo    execdomains  irq         kpagecgroup  meminfo  pagetypeinfo  stat       tty
bus        crypto     filesystems  kallsyms    kpagecount   misc     partitions    swaps      uptime
cgroups    devices    fs           kcore       kpageflags   modules  sched_debug   sys        version

可以看到,少了一些文件,少的主要是数字命名的目录,因为当前 Namespace 下没有这些进程,自然就看不到对应的信息了。

此时就可以通过 ps 命令来查看了:

root@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 13:13 pts/2    00:00:00 bash
root        11     1  0 13:13 pts/2    00:00:00 ps -ef

可以看到,在当前 Namespace 中 bash 为 1 号进程。

这就说明,
当前 Mount Namespace 中的 mount 和外部是隔离的,mount 操作并没有影响到外部
,Docker volume 也是利用了这个特性。

User Namespace

User Narespace 主要是隔离用户的用户组ID

也就是说,一个进程的 UserID 和GroupID 在不同的 User Namespace 中可以是不同的。

比较常用的是,在宿主机上以一个非 root 用户运行创建一个 User Namespace, 然后在 User Namespace 里面却映射成 root 用户。这意味着,这个进程在 User Namespace 里面有 root 权限,但是在 User Namespace 外面却没有 root 的权限。

再次修改代码,增加 User Namespace 的 flag:

	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
	}

运行并测试:

首先在宿主机上查看一个 user 和 group:

DESKTOP-9K4GB6E# id
uid=0(root) gid=0(root) groups=0(root)

可以看到,此时是 root 用户。

运行程序,进入新的 bash 环境:

DESKTOP-9K4GB6E# go run main.go
nobody@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

可以看到,UID 是不同的,说明 User Namespace 生效了。

Network Namespace

Network Namespace 是用来隔离网络设备、IP 地址端口等网络栈的 Namespace

Network Namespace 可以让每个容器拥有自己独立的(虛拟的)网络设备,而且容器内的应用可以绑定到自己的端口,每个 Namespace 内的端口都不会互相冲突。

在宿主机上搭建网桥后,就能很方便地实现容器之间的通信,而且不同容器上的应用可以使用相同的端口。

再次修改代码,增加 Network Namespace 的 flag:

	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | 
			syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,
	}

运行并测试:

首先看一下宿主机上的网络设备:

DESKTOP-9K4GB6E# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: bond0: <BROADCAST,MULTICAST,MASTER> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 22:2f:1f:8e:f7:72 brd ff:ff:ff:ff:ff:ff
3: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 7e:46:8b:04:23:81 brd ff:ff:ff:ff:ff:ff
4: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
5: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/sit 0.0.0.0 brd 0.0.0.0
6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:6e:f2:65 brd ff:ff:ff:ff:ff:ff
    inet 172.18.167.21/20 brd 172.18.175.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:fe6e:f265/64 scope link
       valid_lft forever preferred_lft forever

有 lo、eth0 等6 个设备。

然后运行程序:

DESKTOP-9K4GB6E# go run main.go
nobody@DESKTOP-9K4GB6E:/home/lixd/projects/docker/mydocker$ ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
3: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/sit 0.0.0.0 brd 0.0.0.0

可以发现,新的 Namespace 中只有3个设备了,说明 Network Namespace 生效了。

3. 小结

  • 0)Docker 使用 namespace 进行隔离。
  • 1)
    Namespace 的本质
    :Linux Namespace 是 Linux 提供的
    一种内核级别环境隔离的方法,本质就是对全局系统资源的一种封装隔离
  • 2)
    Namespace 的使用
    :Namespace API 一共 4个,最常用的就是 clone,而 Go 已经把 clone 调用给封装好了,使用时只需要传入不同参数即可控制创建不同 Namespace。

namespace-kind

namespace-api


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【
探索云原生
】即可订阅


4. 参考

学习 @ConfigurationProperties 之前我们需要一些前置知识点:

@Value是个什么东西

首先明确:
@ConfigurationProperties 是 SpringBoot 注解。

我们在讲 @Value 时知道了如何将配置文件的属性注入到变量中,废话不多说,直接上代码。

这是配置文件配置:

spring.datasource.dynamic.mysql.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.mysql.url=jdbc:mysql://localhost:3306/db1
spring.datasource.dynamic.mysql.username=root
spring.datasource.dynamic.mysql.password=root

麻烦?

没事,有简单的,我们用简单的:

spring:
  datasource:
    dynamic:
      mysql:
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db1
        username: root
        password: root

使用 @Value 获取配置:

@Configuration
public class DynamicDataSourceConfig {
    
    @Value("${spring.datasource.dynamic.mysql.driverClassName}")
    private String driverClassName;
    @Value("${spring.datasource.dynamic.mysql.url}")
    private String url;
    @Value("${spring.datasource.dynamic.mysql.username}")
    private String username;
    @Value("${spring.datasource.dynamic.mysql.password}")
    private String password;    

}

写完之后整整齐齐,帅!

如果有人觉得帅,一定还没成长成一个合格的程序员。

我们程序员追求什么?

优雅。

优雅永不过时。

优雅最重要的一条原则是什么?

不写重复代码!

所以怎么办?

有办法。

下面我们讲一下
@ConfigurationProperties

作用

将标注了 @ConfigurationProperties 注解的Spring容器中的Bean与配置文件中的属性进行一一绑定,用于更加快速、方便的读取配置文件的内容。

使用方式一:@Component

直接上代码:

@Component
@ConfigurationProperties(prefix = "spring.datasource.dynamic.mysql")
@Data
public class DynamicDataSourceConfig {
    
    private String driverClassName;
    private String url;
    private String username;
    private String password;    

}

写完之后清清爽爽,帅!

我们发现了几个关键点:

1.原来的 @Configuration 变成了 @Component

这个不影响,因为 @Configuration 中包含了 @Component,这里主要强调通过@Component注解将其注册成Bean。

2.@ConfigurationProperties(prefix = "")

这是我们的主角,prefix 就是匹配我们属性值的前缀,有了它我们就少写好多代码。

3.多了个@Data

@Data的功能中包含了setter方法,没有setter怎么赋值?!

使用方式二:@EnableConfigurationProperties

WTF is @EnableConfigurationProperties?

@EnableConfigurationProperties 就是:

@ConfigurationProperties,启动!

如果配置类只配置了 @ConfigurationProperties 注解,而没有使用@Component,也就是在IOC容器中是获取不到bean。

那就需要
@EnableConfigurationProperties
把使用 @ConfigurationProperties 的类进行了一次
注入

怎么用呢?

需要与@Configuration一起使用。

需要与@Configuration一起使用。

需要与@Configuration一起使用。

上代码:

@ConfigurationProperties(prefix = "father")
@Data
public class Father {
   private String name;
}
@Configuration
@EnableConfigurationProperties(Father.class)
public class MainConfig {
}

实际开发中可能更多的写在 Application 启动类上。

这时候 A 同学问了:

那启动类上没有 @Configuration 啊?

有的,@SpringBootApplication 也是一个@Configuration。

又有 B 同学问了:

那我有两个配置类怎么写呢?

哎?好办。

@Configuration
@EnableConfigurationProperties(Father.class, Son.class)
public class MainConfig {
}

又有 C 同学问了:

那我有 100 个配置类呢?

聪明的 B 同学说:

我知道我知道!

@Configuration
@EnableConfigurationProperties(Father.class, Son.class, GrandSon.class, GrandgrandSon.class...)
public class MainConfig {
}

...

不愧是你。

那怎么办呢?

也有办法。

使用方式三:@ConfigurationPropertiesScan

SpringBoot 2.2.0版本以后提供了这个注解。

这个注解加到启动类上,注明包路径。

系统就会扫描这个包路径下的所有包含 @ConfigurationProperties 注解的配置类。

如果不写包路径,就默认扫描启动类所在包及其子包。

这样就不用写一百个类了。

@SpringBootApplication
@ConfigurationPropertiesScan
public class DemoAnnotationApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoAnnotationApplication.class, args);
    }
}

差不多了吧?

什么?小明,你有话说?

那既然 @Component 能注册成 Bean,那我使用 @Configuration 加 @Bean 注册又怎么说?

卧槽,还有高手?

使用方式四:@Bean

@Data
public class Animal {
    private String name;
}
@Configuration
public class MainConfig {
    @Bean
    @ConfigurationProperties(prefix = "animal")
    public Animal getAnimalBean() {
        return new Animal();
    }
}
animal.name=Wangcai

还有高手吗?

下课!


联系我:

https://stanezhang.github.io/

哈喽大家好,我是咸鱼。

果然出来工作后地球转速加快是正常现象,不知不觉 2023 年就结束了,我到现在都有种昨天刚过完 2023 年春节的恍惚。

工作时长已有一年多,刚开始接触工作的时候,有许多工作上的东西能够让我去学习,无论是技术方面还是业务方面都进步很快。

随着工作逐渐得心应手,感觉进步的曲线开始逐渐平缓。

即使偶尔逼自己看一些书籍或学习一门技术来提升自己,但是过一段时间之后就偃旗息鼓开始放弃了(例如之前热情高涨地要学 FLask ,买了书学了三分之一写了两篇文章之后就再也没打开过)。

我开始感到焦虑和纠结,不学的时候担心如果每天除了工作外没有额外的学习,自己将无法有所进步,与他人的差距逐渐加大;学的时候发现自己并不享受这个过程,或者每当学习时,内心就会出现一个声音:工作了一天这么累了,别再学了,好好休息刷刷短视频不好吗?

每当纠结时不出意外的话往往最后多半躺在床上玩手机刷短视频,至多做些明天的规划。

大脑

现在我一般通过两种途径去驱动自己学习:

  • 写文章
  • 工作

工作这块就不细说了,写文章是我主动学习的一个有效途径,现在基本能保持一周 2~3 的文章更新速度,现在已经有 169 篇原创了。

前 100 篇以自己的学习笔记居多,100 篇之后的文章更多的是译文。虽然说是译文,但是我觉得写翻译文章也是比较耗费精力的,远不是简单使用翻译软件就能完成的工作。

下面分享一些我觉得比较优质的信息源/个人博客:

今年开始尝试在掘金、阿里云等平台上发布文章

CSDN

自从我把 CSDN 上面的文章改成粉丝可见之后,我的粉丝量嘎嘎涨,后悔没早点意识到这一点哈哈哈。

不出意外的话这个月就能拿到 CSDN 的专家认证。

知乎

接近 50w 的阅读量,有一篇文章还被官方收录进去。

不得不说,知乎在创作者福利这块还是挺好的,每年传统节日都能收到知乎送的礼物。

阿里云

拿了个阿里云专家博主的证书,以及一些小礼品啥的。

小红书

小红书可以说是 2023 年新尝试的赛道了,区别于其他平台,小红书内容更多的是图文形式,而且小到昵称头像,大到个人简介笔记文案都有自己的规则。

我也是在慢慢摸索当中吧,5月份创的号,现在粉丝 1.2 w

阅读

2023 年抽空读了一些书,下面都是我觉得不错的:

  • 《穿越计算机的迷雾》:计算机科学的科普入门书,从电子和硬件角度看的计算机史,大多内容人人都能看懂,里面很多俏皮话,有些人可能觉得啰嗦废话多,但我觉得比教科书有趣。
  • 《褚时健传》:我挺喜欢读人物传记的,这本书详尽地描述了褚时健的一生。
  • 《反脆弱》:修行不够,太难读懂了,硬着头皮读下去的。
  • 《纳瓦尔宝典》:文字精炼简短,读完我感觉我又行了。
  • 《Flask web 开发实战》:希望今年不要再半途而废了。

财务

打工是通过投入时间和体力来赚取劳动报酬(金钱),是许多人获取收入的一种方式。然而,它的缺点显而易见,因为时间和收益之间往往存在线性关系,很难突破这种限制。

2023 年大 A 大家有目共睹,始终硬不起来,好像大家信心都不是很足

今年降低预期,不求跑赢通胀,只求保本(今天回到家,煮了点面吃,一边吃面一边哭,泪水滴落在碗里,没有开灯)

身体

上半年体脂一直维持在 12% 以下,为了维持低体脂而不得不低碳+热量缺口,导致我每天无精打采神情恍惚,下半年果断决定增肌。在健身房认识了很多大佬,也有幸认识了两个健身搭子&好朋友

不是我去健身房,是健身房收留了我。几乎没什么娱乐活动的我工作之余大部分时间都在健身房里,一周五练

一开始是四分化,腿胸背肩,但是后面不打算单独拿出一天来练肩,先沉淀一下大肌群。练胸的那天把中束带一下,练背那天把后束带一下就行了

希望今年卧推能够突破 100 kg

人生可以没有目标

又是一年过去了,人们通常会回顾这一年实现了哪些目标,下一年要设定哪些目标,但是从今年开始,我不打算设立目标了,只做年终回顾

在读阮一峰的科技爱好者周刊时,他推荐了一篇引人深思的文章:
《我从来没有目标》

于是我打算把阮一峰老师对于这篇文章的感想分享出来

内容摘自《科技爱好者周刊(第 283 期):[年终感想] 没有目的地,向前走》
https://www.ruanyifeng.com/blog/2023/12/weekly-issue-283.html

今天,我想分享 BaseCamp 公司创始人 Jason Fried 的一篇短文,题目就叫
《我从来没有目标》

下面就是这篇文章的主要内容。

我不记得自己有什么目标,那种很具体的目标。

有些事情我一直想做,但如果不做,我也能接受。有些事情值得去做,但如果没有做到,我也不觉得很遗憾。

我的目标不是那样。

我做事,我尝试,我建造,我想要取得进步,我想让我做的东西使得自己、使得公司、使得家庭、使得社会变得更好。但我从未设定过目标。这不是我做事的方式。

目标是当你到达时就会消失的东西。一旦你到达了,它就消失了。你总是可以设置另一个,我只是不按这样的步骤行事。

我只是做我正在做的事情,然后就到了现在的地方。今天我继续以同样的方式对待工作和生活。

他的文章到这里就结束了。我很喜欢作者的这种态度。

你可以不给自己设定目标,因为
目标是你做事的副产品
。只要你按照自己的信念,不断地努力去做事、去尝试,就可以了,至于能做到哪一步,顺其自然就好了。

有一句成语"
功不唐捐
",就是这个意思,付出的努力不会白费。

西方人说:"Passion never fails"(激情不会失败),也是同样的意思。

建造那些你想要建造的东西,努力做事,认真生活,人生可以没有目标。

新的一年,我想我也是这样,没有目的地,就是向前走。

上篇文章
使用房车这一年
总结了过去一年并不平淡的生活,然而,这一年的时光不仅有生活,工作也占去了大半,那这篇文章就来聊聊这稳中有进的工作,总结下这一年我们上线的那些新的自动化运维系统,也回顾下去年上线的那些系统有了怎样的改变,这也算是对去年同名文章
这一年我们上线的自动化系统
的递进和补充了

上一年我们已经上线了
任务系统

多云系统

监控系统

作业系统
,构建了一个相对完善的自动化运维平台,也切切实实的提升了小伙伴们的工作效率,尤其是移动端的全面兼容,更是赞不绝口,我深受鼓舞,感谢这群靠谱的伙伴。2023这一年在去年的基础之上,于2月上线了
软件系统
,3月上线了
容器系统
,6月上线了多云安全,8月上了硬件系统,9月上线了
域名系统
,同时于今年结束前完成了大量的系统底层优化,做了平台化改造,同时也完善了系统报表,提供了全面的审计支持

软件系统

软件系统
主要是为了实现公司内部软件的管理,包含订单管理、软件分配等等功能。公司内部购买了许多软件,这些软件的订单及分配情况、过期时间等等基础信息一直都是通过Excel来管理的,久而久之难免混乱。入职新公司之后,领导给了个额外的任务,希望我能带领下团队内做传统运维的小伙伴学习运维自动化开发技能,于是软件系统应运而生,大家伙以此为实战项目,边学边做,顺利完成

软件系统本身并不复杂,但它却为伙伴们打开了应用开发的大门,当系统上线真的投入生产使用,它的意义已经远超软件管理本身。这也为8月我们硬件系统的上线打下了坚实的基础

容器系统

容器系统
容器主要用来集中管理Kubernetes集群,实现自建以及云上容器的统一管理。可以完成容器的创建、伸缩、删除、监控、日志查看、终端登录等全生命周期管理。通过云平台API实现了Kubernetes集群的统一管理,包括监控查看,节点扩容等,通过Kubernetes集群API实现了集群资源的统一管理,包括资源创建、副本伸缩等,通过WebSocket打通了容器的SSH协议,实现容器登录,可以网页端在线操作容器

同时扩展了任务系统,集成Jenkins,支持原本基于Jenkins发布部署的容器项目低成本迁移至我们的任务系统,实现完善且高效的发布部署任务流。文章
任务系统之Jenkins子任务
就介绍了这一部分的技术实现及渊源,我们不可能设计出完美的系统,但可以在不断的使用中使系统变得逐渐完善,这就是一个实际的例子,这一年对于已经上线的系统像这样的更新还有很多很多

多云安全

多云安全主要实现了云上安全的统一管理,包括基线安全、漏洞管理、文件查杀、异常登陆、密码破解等,支持用户自助发起检测。多云安全与我们基础资源数据打通,可以快速找到受影响资源所属项目及业务,从而及时通知资源owner进行后续处理

对于基线和漏洞,还会有相应的检测项说明以及修复方法参考,实现从检测到修复的全面支持

硬件系统

硬件系统是为了实现公司内部IT硬件资产从采购到分配到回收最后报废等过程全生命周期的管理,包括台式电脑、笔记本、显示器、手绘板、投影仪、打印机等等IT硬件资产。硬件系统是继软件系统之后,团队小伙伴的又一个实践项目,我们完成了从需求收集开始、到技术设计、再到编写代码、最后测试上线的整个流程

硬件系统的上线完全替代了原本低效的Excel管理模式,同时也弥补了资源流转过程缺失的问题,使得资产管理更专业和高效。同时也是团队小伙伴的另一个里程碑

域名系统

域名系统
主要用来实现域名从申请到变更再到下线的全生命周期管理,域名系统整体上包含三个部分,分别是查询、变更、巡检,除了最基本的域名变更操作外,我们还收集了域名的全部基础数据例如注册厂商、注册时间、到期时间、解析记录等等,以及有效关联信息例如指向资源、资源类型、资源状态、申请用户等等,从而构建了完整的域名基础数据库,这个数据库不仅能支持完善的域名信息查询,还能基于此做很多的自动化巡检工作,例如域名临期提醒、域名解析绑定资源下线提醒、域名证书扫描及临期提醒等,完善了整个域名管理体系

在基础数据较为完善的前提下,还做了
全链路自动化拓扑
方向的探索,充分发挥数据的价值,让数据不再枯燥,使资源更加立体,也为后续基于架构/拓扑的可观测平台提供基础支撑

优化升级

这一年除了上线了几个新系统之外,还对整个基础架构做了诸多的优化升级,其中最重要的就是平台化的改造。当系统功能不断增加,菜单也随之增多,大量的菜单带来了使用的不便,尽管可以通过多级菜单来优化,但仍不够清晰明了,参考各大公有云平台,做了平台化的改造,将每个不同功能的服务也就是我们前文所提到的这些不同的系统进行了平台内的相对独立,不同服务拥有独立的菜单,不同服务之间可以便捷的切换

优化了权限管理,综合考虑授权粒度和授权方式,完善了
多租户隔离授权
,权限下放,租户owner可以自助分配租户成员的权限,不需要再通过系统管理员,使得权限管理更加灵活和高效

完善了审计功能,除了所有操作通过中间件自动记录日志外,还单独添加了登陆/变更等核心操作的二次审计记录。引入了流程管理,重要操作进行审批确认,确保操作前有审批,操作后有记录。增加审计报表,系统使用情况十分清晰

除此之外还有其他的一些优化更新,例如通过
Celery多队列解决任务优先级问题
、通过
迁移不同APP之间Model
使Model组织更合理等等

系统更新

除了新的系统以及底层优化外,过去一年还对2022年已经上线的系统做了诸多的升级,多云和监控系统支持了更多厂商更多类型的资源,实现了更加全面的统一管理和监控,目前已经纳管了超过10个云厂商的30多个服务,几乎涵盖了公司项目用到的所有服务类型

作业系统支持了动态参数,脚本管理等。任务系统也支持了更多类型的子任务,例如监控类型的子任务可以实现上线过程中监控的自动启停,前文说过的
Jenkins类型子任务
可以直接接入Jenkins,同时也将作业系统作为新的子任务类型接入了任务系统,这样可以让任务系统批量操作目标终端,实现更广泛的应用场景

一点思考

两年前
从工作了十年的公司离职
来到这里,一个重要的理由就是想要试试换个场景落地之前几年沉淀的devops工具和方法,转眼已经两年,有过欣喜也有过气馁,整体上的落地效果至少我个人还是认可的,也并没有因此而后悔。但这两年也深刻的意识到,技术飞速发展,尤其是今年AI大模型的横空出世,更是给我自己带来了不小的震撼,这是之前几次VR/AR/元宇宙等技术革新所完全没有的

时代发展太快,之前认知的一些壁垒似乎也没有那么坚固,想到的事情要尽快去做,关注落地

前言

驱动作为桥梁,用户层调用预定义名称的系统函数与系统内核交互,而用户层与系统层不能直接进行数据传递,进行本篇主要就是理解清楚驱动如何让用户编程来实现与内核的数据交互传递。


温故知新

  • 设备节点是应用层(用户层)与内核层交互;
  • 使用预先的结构体进行操作,如系统open函数对应了驱动中文件操作及的open指针结构体:struct file_operations;
  • 文件操作集结构体,填充结构体对应指针,填充自己使用到的就行了,多余的可以不填充,调用也不会崩溃或返回错误,会返回0;
    在这里插入图片描述

那么如何将应用层的输入写入进去可用,如何将内核层的数据通过read返回出来,就是本篇学习了。


驱动模板准备

首先复制之前的testFileOpts的驱动,改个名字为:testFileOpts:

cd ~/work/drive/
ls
cp -arf 003_testFileOpts 004_testReadWrite
cd 004_testReadWrite/
make clean
ls
mv testFileOpts.c testReadWrite.c
vi Makefile
ls

在这里插入图片描述

其中修改makefile里面的模块名称(obj-m模块名称),模板准备好了

gedit Makefile 

在这里插入图片描述

下面基于testReadWrite.c文件进行注册杂项设备,修改.c文件:

gedit testReadWrite.c

在这里插入图片描述

在这里插入图片描述

#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>

// int (*open) (struct inode *, struct file *);
int misc_open(struct inode * pInode, struct file * pFile)
{
printk("int misc_open(struct inode * pInode, struct file * pFile)\n");
return 0;
}

// int (*release) (struct inode *, struct file *);
int misc_release(struct inode * pInde, struct file * pFile)
{
printk("int misc_release(struct inode * pInde, struct file * pFile)\n");
return 0;
}

// ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t misc_read(struct file * pFile, char __user * pUser, size_t size, loff_t *pLofft)
{
printk("ssize_t misc_read(struct file * pFile, char __user * pUser, size_t size, loff_t *pLofft)\n");
return 0;
}

// ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t misc_write(struct file * pFile, const char __user * pUser, size_t size, loff_t *pLofft)
{
printk("ssize_t misc_write(struct file * pFile, const char __user * pUser, size_t size, loff_t *pLofft)\n");
return 0;
}

struct file_operations misc_fops = {
.owner = THIS_MODULE,
.open = misc_open,
.release = misc_release,
.read = misc_read,
.write = misc_write,
};

struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR, // 这个宏是动态分配次设备号,避免冲突
.name = "register_hongPangZi_testReadWrite", // 设备节点名称
.fops = &misc_fops, // 这个变量记住,自己起的,步骤二使用
};

static int registerMiscDev_init(void)
{
int ret;
// 在内核里面无法使用基础c库printf,需要使用内核库printk
printk("Hello, I’m hongPangZi, registeraMiscDev_init\n");
ret = misc_register(&misc_dev);
if(ret < 0)
{
printk("Failed to misc_register(&misc_dev)\n");
return -1;
}
return 0;
}

static void registerMiscDev_exit(void)
{
misc_deregister(&misc_dev);
printk("bye-bye!!!\n");
}

MODULE_LICENSE("GPL");
module_init(registerMiscDev_init);
module_exit(registerMiscDev_exit);


概述

内核层和用户层不能中是不能直接与用户数据交互,需要使用内核函数copy_to_user和copy_from_user。
在内核中可以使用printk,memset,memcpy,strlen等函数。


内核函数

头文件是:linux/uaccess.h(我们这是ubuntu,不是arm)
可以在内核根目录下搜索下:

find . -type f -exec grep -l "copy_to_user(void" {} \;

在这里插入图片描述

在这里插入图片描述


copy_from_user函数:从用户层复制到内核层

static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)

简化下:

static unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)

参数分别是,复制到的地址(内核空间),从什么地址复制(用户空间),复制长度;


copy_to_user函数:从内核层复制到用户层

static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)

简化下:

static unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)

参数分别是,复制到的地址(用户空间),从什么地址复制(内核空间),复制长度;


杂项设备驱动添加数据传递函数Demo


步骤一:加入头文件和定义static缓存区

在这里插入图片描述

#include <linux/uaccess.h>      // Demo_004 add
static char kBuf[256] = {0x00}; // Demo_004 add


步骤二:初始化缓存区

在这里插入图片描述

// int (*open) (struct inode *, struct file *);
int misc_open(struct inode * pInode, struct file * pFile)
{
printk("int misc_open(struct inode * pInode, struct file * pFile)\n");
memcpy(kBuf, "init kBuf", sizeof("init kBuf"));
printk("kBuf = %s\n", kBuf);

return 0;
}


步骤三:在驱动函数read中,添加从内核层到用户层的函数

在这里插入图片描述

// ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t misc_read(struct file * pFile, char __user * pUser, size_t size, loff_t *pLofft)
{
printk("ssize_t misc_read(struct file * pFile, char __user * pUser, size_t size, loff_t *pLofft)\n");
if(copy_to_user(pUser, kBuf, strlen(kBuf)) != 0)
{
printk("Failed to copy_to_user(pUser, kBuf, strlen(kBuf)\n");
return -1;
}
return 0;
}


步骤四:在驱动函数wirte中,添加从用户层到内核层的函数

在这里插入图片描述

// ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t misc_write(struct file * pFile, const char __user * pUser, size_t size, loff_t *pLofft)
{
printk("ssize_t misc_write(struct file * pFile, const char __user * pUser, size_t size, loff_t *pLofft)\n");
if(copy_from_user(kBuf, pUser, size) != 0)
{
printk("Failed to copy_from_user(kBuf, pUser, size)\n");
return -1;
}
return 0;
}


步骤五:在程序中读取、写入、再读取

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char **argv)
{
int fd = -1;
char buf[32] = {0};
int ret = -1;

const char devPath[] = "/dev/register_hongPangZi_testReadWrite";
fd = open(devPath, O_RDWR);
if(fd < 0)
{
printf("Failed to open %s\n", devPath);
return -1;
}else{
printf("Succeed to open %s\n", devPath);
}
// 读取
ret = read(fd, buf, sizeof(buf) < 0);
if(ret < 0)
{
printf("Failed to read %s\n", devPath);
close(fd);
return 0;
}else{
printf("Succeed to read [%s]\n", buf);
}
// 修改内容
memset(buf, 0x00, sizeof(buf));
memcpy(buf, "Get you content", strlen("Get you content"));
// 写入
ret = write(fd, buf, sizeof(buf));
if(ret < 0)
{
printf("Failed to write %s\n", devPath);
close(fd);
return 0;
}else{
printf("Succeed to write [%s]\n", buf);
}
// 读取
ret = read(fd, buf, sizeof(buf) < 0);
if(ret < 0)
{
printf("Failed to read %s\n", devPath);
close(fd);
return 0;
}else{
printf("Succeed to read [%s]\n", buf);
}

close(fd);

printf("exit\n");

fd = -1;


return 0;
}


步骤六:编译加载驱动

在这里插入图片描述

make
sudo insmod testReadWrite.ko


步骤七:编译程序运行结果

gcc test.c
sudo ./a.out

在这里插入图片描述

测试结果与预期相同


入坑


入坑一:测试程序读取与预期不同


问题

在这里插入图片描述


原因

在这里插入图片描述


解决

在这里插入图片描述