当前位置: 首页 > backend >正文

【Linux】系统部分——软硬链接动静态库的使用

18.软硬链接_动静态库的使用

文章目录

  • 18.软硬链接_动静态库的使用
      • 软硬链接
        • 软链接
        • 硬链接
        • 软硬链接的作用
      • 静态库
        • 创建一个静态库
        • 静态库的使用
          • 1.将库安装到系统目录下
          • 2.直接在当前目录下使用
          • 3.使用指定路径下的库
      • 动态库
        • 制作一个动态库
        • 动态库的使用
          • 1.将库安装到系统目录下
          • 2.直接在当前目录下使用
          • 3.使用指定路径下的库(此操作前需要先把系统中安装的动态库删除)
          • 总结一下
          • 总结一下

软硬链接

软链接
[user@iZ7xvdsb1wn2io90klvtwlZ lession20]$ touch file.txt
[user@iZ7xvdsb1wn2io90klvtwlZ lession20]$ ln -s file.txt file-soft.link #对file.txt创建软链接
[user@iZ7xvdsb1wn2io90klvtwlZ lession20]$ ls -li
total 0
665991 lrwxrwxrwx 1 user user 8 Jul 18 13:54 file-soft.link -> file.txt
665990 -rw-rw-r-- 1 user user 0 Jul 18 13:53 file.txt
[user@iZ7xvdsb1wn2io90klvtwlZ lession20]$ echo hello linux > file.txt 
[user@iZ7xvdsb1wn2io90klvtwlZ lession20]$ cat file.txt 
hello linux
[user@iZ7xvdsb1wn2io90klvtwlZ lession20]$ cat file-soft.link 
hello linux
  1. 软链接本质上是一个独立文件,他有自己的inode

  2. 在用户层使用软链接等同于使用目标文件

  3. 软链接的内容上,保存的是目标文件的路径,类似于Windows上的快捷方式

  4. 删除软链接有两种方法:rmulink

硬链接

我们看到,真正找到磁盘上⽂件的并不是⽂件名,⽽是inode。其实在linux中可以让多个⽂件名对应于同⼀个inode 。

[user@iZ7xvdsb1wn2io90klvtwlZ lession20]$ ln file.txt file-hard.link
[user@iZ7xvdsb1wn2io90klvtwlZ lession20]$ ls -li
total 8
665990 -rw-rw-r-- 2 user user 12 Jul 18 14:03 file-hard.link
665991 lrwxrwxrwx 1 user user  8 Jul 18 13:54 file-soft.link -> file.txt
665990 -rw-rw-r-- 2 user user 12 Jul 18 14:03 file.txt
[user@iZ7xvdsb1wn2io90klvtwlZ lession20]$ cat file.txt 
hello linux
[user@iZ7xvdsb1wn2io90klvtwlZ lession20]$ cat file-hard.link 
hello linux
  1. 硬链接与软链接不同,不是独立文件,硬链接没有属于自己的独立inode号,与原文件的inode相同

  2. 当一个文件存在一个硬链接的时候,会发现执行ls -l 命令后有一个数字从1变成的2,这个数字就是引用计数(硬链接数)

    存在硬链接之前:665990 -rw-rw-r-- 1 user user 0 Jul 18 13:53 file.txt

    存在硬链接之后:665990 -rw-rw-r-- 2 user user 12 Jul 18 14:03 file.txt

  3. 硬链接本质上就是一组文件名和已经存在的文件的映射关系

  4. 通过之前的学习,我们已经知道了,文件系统中的inode不保存文件名,文件名与inode的映射关系是由目录文件保存的,当没有一个文件名指向某一个inode的时候,这个inode对应的文件才会被删除,所以要想知道现在是否有文件名指向某个文件(某个文件是否可以被删除),inode中存在一个叫引用计数的东西,记录当前有多少文件名指向这个ionde,这也就是上面第二点提到的内容

  5. 当创建一个空目录的时候,这个空目录的引用计数默认是2,因为创建一个空目录时,在空目录中会自动创建两个隐藏目录**./../这两个文件就分别是当前目录和上级目录的硬链接**,所以不仅当前空目录的引用计数是2,上级目录的引用计数还要+1

  6. Linux中不允许对目录创建硬链接,防止创建环状路径导致一系列问题

当我们删除被软硬连接的原文件之后:

在这里插入图片描述

  1. 硬链接file-hard.link并没有受到影响,但是软链接file-soft.link失效
  2. 重新生成file.txt文件之后,软链接恢复,指向新建的file.txt文件,但是file-hard.link不再是新建的file.txt的硬链接了
软硬链接的作用

硬链接:文件备份,以及每一个文件目录中的/.和/…都是硬链接

软链接:快捷方式

静态库

简单来说,库本质上是⼀种可执⾏代码的⼆进制形式,可以被操作系统载⼊内存执⾏。库有两种:动态库和静态库,分别有不同的后缀

  • 静态库:.a[linux]、.lib[windows]
  • 动态库:.so[linux]、.dll[windows]
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2151672 Jul  3  2019 /lib64/libc-2.17.so
[whb@bite-alicloud ~]$ ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5105516 Jun 4 23:05 /lib64/libc.a
  • 程序在编译链接的时候把库的代码链接到可执⾏⽂件中,程序运⾏的时候将不再需要静态库。
  • ⼀个可执⾏程序可能⽤到许多的库,这些库运⾏有的是静态库,有的是动态库,⽽我们的编译默认为动态链接库,只有在该库下找不到动态.so的时候才会采⽤同名静态库。我们也可以使⽤ gcc 的 -static 强转设置链接静态库。
创建一个静态库
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls
main.c  my_stdio.c  my_stdio.h  my_string.c  my_string.h
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ gcc -c my_stdio.c
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ gcc -c my_string.c
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls
main.c  my_stdio.c  my_stdio.h  my_stdio.o  my_string.c  my_string.h  my_string.o
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ vim my_string.c
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ar -rc libmystdio.a m
main.c       my_stdio.c   my_stdio.h   my_stdio.o   my_string.c  my_string.h  my_string.o  
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ar -rc libmystdio.a my_stdio.o my_string.o
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls
libmystdio.a  main.c  my_stdio.c  my_stdio.h  my_stdio.o  my_string.c  my_string.h  my_string.o

把准备好的源文件编译为.o文件(gcc -c编译为同名.o文件),使用ac -rc创建静态库。

注意一下静态库的命名规范:前缀为lib+名称+后缀为.a

[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls -li
total 36
666002 -rw-rw-r-- 1 user user 4374 Aug 22 15:12 libmystdio.a
665998 -rw-rw-r-- 1 user user  488 Aug 21 11:06 main.c
665995 -rw-rw-r-- 1 user user 1496 Aug 21 11:06 my_stdio.c
665997 -rw-rw-r-- 1 user user  447 Aug 21 11:06 my_stdio.h
665996 -rw-rw-r-- 1 user user 2848 Aug 22 15:11 my_stdio.o
665989 -rw-rw-r-- 1 user user  133 Aug 21 11:13 my_string.c
665999 -rw-rw-r-- 1 user user   44 Aug 21 11:14 my_string.h
666000 -rw-rw-r-- 1 user user 1272 Aug 22 15:11 my_string.o
  • ar在man中的解释为:创建、修改和从归档文件中提取内容。ar 是 gnu 归档⼯具, rc 表⽰ (replace and create)

  • t: 列出静态库中的⽂件、v:verbose 详细信息

    [user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ar -tv libmystdio.a 
    rw-rw-r-- 1001/1001   2848 Aug 22 15:11 2025 my_stdio.o
    rw-rw-r-- 1001/1001   1272 Aug 22 15:11 2025 my_string.o
    
静态库的使用
1.将库安装到系统目录下
  1. 我们可以把前面打包好的静态库安装到系统中,这样就可以在使用vim编写代码的时候直接使用我们自己的库了。如果要把库安装到系统中,需要把.h文件拷贝到/usr/include目录下,把库拷贝到/lib64目录下
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ sudo cp *.h /usr/include/
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls /usr/include/my_*
/usr/include/my_stdio.h  /usr/include/my_string.h
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ sudo cp libmystdio.a /lib64/
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls /lib64/libmystdio.a 
/lib64/libmystdio.a
  1. 除了将文件拷贝到系统当中,我们自己写的库为第三方库,gcc不能直接识别,所以我们在使用gcc编译使用这个库的代码的时候需要指明
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ cat main.c
#include <my_stdio.h>
#include <my_string.h>
#include <stdio.h>int main()
{const char *s = "aaa";printf("%s, %d\n", s, my_strlen(s));mFILE *fp = mfopen("./text.txt", "w");if(fp == NULL) return 1;mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfclose(fp);return 0;
}
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ gcc -o main main.c -lmystdio
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ ls
main  main.c  stdio
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ ./main
aaa, 3

在指明的时候,库的前缀lib和后缀.a都不需要,在前面加上-l(注意一下,不需要有空格,如果需要多个库,每个库都要按照这个格式写,包括-l

2.直接在当前目录下使用

gcc编译器默认只会在/usr/lib64目录下寻找库是否存在,不会再当前路径下查找,如果要使用当前路径下的库,则在指明库的前面加上-L.(L后面的.表示在当前目录),告诉编译器,编译的时候,查找库,除了系统路径,也要在我指明的路径下找,还要注意,在代码中头文件的包含不能再使用<>而是要用""

如果不修改会出现:

[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ gcc -o main main.c -L. -lmystdio
main.c:1:22: fatal error: my_stdio.h: No such file or directory#include <my_stdio.h>^
compilation terminated.

gcc使用:

[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ gcc -o main main.c -L. -lmystdio
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls
libmystdio.a  main  main.c  my_stdio.c  my_stdio.h  my_stdio.o  my_string.c  my_string.h  my_strin.o
3.使用指定路径下的库

将编写好的库进行打包后交给其他人使用:

[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ cat Makefile
libmystdio.a: my_stdio.o my_string.o@ar -rc $@ $^@echo "build $^ to $@ ... done"
%.o:%.c@gcc -c $<@echo "compling $< to $@ ... done".PHONY:clean
clean:@rm -f *.a *.o@echo "clean done".PHONY:output
output:@mkdir -p stdc/lib@mkdir -p stdc/include@cp -f *.h stdc/include @cp -f *.a stdc/lib@tar -czf stdc.tgz stdc @echo "output stdc ... done"
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ make
compling my_stdio.c to my_stdio.o ... done
compling my_string.c to my_string.o ... done
build my_stdio.o my_string.o to libmystdio.a ... done
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ make output
output stdc ... done
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ll
total 64
-rw-rw-r-- 1 user user 4374 Aug 30 17:28 libmystdio.a
-rw-rw-r-- 1 user user  368 Aug 30 17:29 Makefile
-rw-rw-r-- 1 user user 1496 Aug 30 16:14 my_stdio.c
-rw-rw-r-- 1 user user  447 Aug 21 11:06 my_stdio.h
-rw-rw-r-- 1 user user 2848 Aug 30 17:28 my_stdio.o
-rw-rw-r-- 1 user user  133 Aug 21 11:13 my_string.c
-rw-rw-r-- 1 user user   44 Aug 21 11:14 my_string.h
-rw-rw-r-- 1 user user 1272 Aug 30 17:28 my_string.o
drwxrwxr-x 4 user user 4096 Aug 30 17:29 stdc
-rw-rw-r-- 1 user user 1719 Aug 30 17:29 stdc.tgz

使用:

[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ tar xzf stdc.tgz 
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ ls
main.c  stdc  stdc.tgz  stdio
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ gcc -o main main.c -Istdc/include -Lstdc/lib -lmystdio
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ ls
main  main.c  stdc  stdc.tgz  stdio
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ ./main
aaa, 3
  • 使用-I+路径告知gcc额外要查找的头文件路径
  • 使用-L+路径告知gcc额外要查找的静态库路径

动态库

制作一个动态库

与制作静态库不同,制作动态库不使用ar命令,直接用gcc命令但是带有-shared用来制作动态库

[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ cat Makefile 
libmystdio.so: my_stdio.o my_string.ogcc -o $@ $^ -shared%.o:%.cgcc -fPIC -c $<.PHONY:clean
clean:rm -rf *.o *.so
  • 制作动态库gcc命令需要带有-share选项
  • 制作动态库的.o文件在生成时需要带有-fPIC选项,表示生成位置无关码
动态库的使用
1.将库安装到系统目录下
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ sudo cp -f libmystdio.so /lib64
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ sudo cp -f *.h /usr/include/
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ll /lib64/libmy*
-rwxr-xr-x 1 root root 8592 Aug 30 19:02 /lib64/libmystdio.so
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ll /usr/include/my*
-rw-r--r-- 1 root root 447 Aug 30 19:02 /usr/include/my_stdio.h
-rw-r--r-- 1 root root  44 Aug 30 19:02 /usr/include/my_string.h
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ gcc -o main main.c -lmystdio
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls
libmystdio.so  main  main.c  Makefile  my_stdio.c  my_stdio.h  my_stdio.o  my_string.c  my_string.h  my_string.o
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ./main
aaa, 3
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ldd mainlinux-vdso.so.1 =>  (0x00007ffe5376c000)libmystdio.so (0x00007fde939f9000)libc.so.6 => /lib64/libc.so.6 (0x00007fde9362c000)/lib64/ld-linux-x86-64.so.2 (0x00007fde93bfb000)
  • ldd命令用于查看可执行程序依赖哪些动态库

  • 如果动态库被删掉,可执行文件就无法执行了

2.直接在当前目录下使用

与静态库一致,这里就不重复了

3.使用指定路径下的库(此操作前需要先把系统中安装的动态库删除)
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ cat Makefile 
libmystdio.so: my_stdio.o my_string.ogcc -o $@ $^ -shared%.o:%.cgcc -fPIC -c $<.PHONY:clean
clean:@rm -rf *.so *.o stdc*@echo "clean done".PHONY:output
output:@mkdir -p stdc/lib@mkdir -p stdc/include@cp -f *.h stdc/include @cp -f *.so stdc/lib@tar -czf stdc.tgz stdc @echo "output stdc ... done"
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls
main  main.c  Makefile  my_stdio.c  my_stdio.h  my_string.c  my_string.h  text.txt
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ make
gcc -fPIC -c my_stdio.c
gcc -fPIC -c my_string.c
gcc -o libmystdio.so my_stdio.o my_string.o -shared
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ make output
output stdc ... done
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ ls
libmystdio.so  main  main.c  Makefile  my_stdio.c  my_stdio.h  my_stdio.o  my_string.c  my_string.h  my_string.o  stdc  stdc.tgz  text.txt
[user@iZ7xvdsb1wn2io90klvtwlZ stdio]$ cd ..
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ ls
main.c  stdio
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ cp stdio/stdc.tgz .
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ tar xzf stdc.tgz 
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ ls
main.c  stdc  stdc.tgz  stdio
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ gcc -o main main.c -Istdc/include -Lstdc/lib -lmystdio
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ ./main
./main: error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory
[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ ldd mainlinux-vdso.so.1 =>  (0x00007ffc10de7000)libmystdio.so => not foundlibc.so.6 => /lib64/libc.so.6 (0x00007f3d4c56b000)/lib64/ld-linux-x86-64.so.2 (0x00007f3d4c938000)

我们会发现按照静态库的使用方法能够正常编译得到可执行文件,但是可执行文件不能正确执行,通过ldd指令显示没有找到动态库。原因是虽然在gcc命令中已经指定了动态库的路径和名称,但也只是在编译过程中指定的,所以可执行文件编译没有出错,但是执行可执行文件是系统的操作,对于操作系统来说,它仍然不知道你的动态库的路径,操作系统寻找库文件依赖于一个环境变量:LD_LIBRARY_PATH

[user@iZ7xvdsb1wn2io90klvtwlZ lession21]$ env
XDG_SESSION_ID=9332
HOSTNAME=iZ7xvdsb1wn2io90klvtwlZ
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=218.77.77.245 59880 22
OLDPWD=/home/user/lession21/stdio
SSH_TTY=/dev/pts/1
USER=user
LD_LIBRARY_PATH=:/home/user/.VimForCpp/vim/bundle/YCM.so/el7.x86_64     #在这行
LS_COLORS=rs=#...................#
MAIL=/var/spool/mail/user
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/user/.local/bin:/home/user/bin
PWD=/home/user/lession21
LANG=en_US.UTF-8
HISTCONTROL=ignoredups
SHLVL=1
HOME=/home/user
LOGNAME=user
SSH_CONNECTION=218.77.77.245 59880 172.18.20.98 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
XDG_RUNTIME_DIR=/run/user/1001
_=/usr/bin/env

因此我们可以通过一下方法使可执行文件正常运行:

  1. 将动态库添加到LD_LIBRARY_PATH指定的路径——一般就是/lib64路径下(拷贝)
  2. /lib64路径下建立所需动态库的软链接
  3. 新增环境变量LD_LIBRARY_PATH的库文件路径,但要注意环境变量在每次服务器重启后会重新加载
  4. /etc/ld.so.conf.d路径下允许用户添加配置文件,在这个路径下添加一个任意名称的.conf文件,文件内容为动态库的路径,系统就会自动找到这个路径下的动态库,ldconfig更新
总结一下
  1. 此时就需要清楚动态库与静态库的区别了,通过静态库形成的可执行文件不需要再找静态库的位置,不多解释。

  2. 如果同时提供动态库和静态库,则优先使用动态库,如果需要强制使用静态库,则在gcc命令后面添加-static选项(这个之前讲过)

  3. 如果需要强制静态链接,必须提供对应的静态库;如果只提供静态库,但是链接方式是动态的,gcc、g++没得选,只能针对提供的.a局部性采用静态链接

64路径下建立所需动态库的软链接 3. 新增环境变量LD_LIBRARY_PATH的库文件路径,但要注意环境变量在每次服务器重启后会重新加载 4. 在/etc/ld.so.conf.d路径下允许用户添加配置文件,在这个路径下添加一个任意名称的.conf`文件,文件内容为动态库的路径,系统就会自动找到这个路径下的动态库,ldconfig更新

总结一下
  1. 此时就需要清楚动态库与静态库的区别了,通过静态库形成的可执行文件不需要再找静态库的位置,不多解释。

  2. 如果同时提供动态库和静态库,则优先使用动态库,如果需要强制使用静态库,则在gcc命令后面添加-static选项(这个之前讲过)

  3. 如果需要强制静态链接,必须提供对应的静态库;如果只提供静态库,但是链接方式是动态的,gcc、g++没得选,只能针对提供的.a局部性采用静态链接

http://www.xdnf.cn/news/19181.html

相关文章:

  • Spring Cloud Gateway 网关(五)
  • java字节码增强,安全问题?
  • MySQL-事务(上)
  • 【分享】如何显示Chatgpt聊天的时间
  • 用Git在 Ubuntu 22.04(Git 2.34.1)把 ROS 2 工作空间上传到全新的 GitHub 仓库 步骤
  • 系统质量属性
  • Git 安装与国内加速(配置 SSH Key + 镜像克隆)
  • 设置word引用zotero中的参考文献的格式为中文引用格式或中英文格式
  • 电子战:Maritime SIGINT Architecture Technical Standards Handbook
  • Linux之Shell编程(三)流程控制
  • 深度学习重塑医疗:四大创新应用开启健康新纪元
  • 深度学习系列 | Seq2Seq端到端翻译模型
  • Ansible Playbook 调试与预演指南:从语法检查到连通性排查
  • Qt QML注册全局对象并调用其函数和属性
  • 针对 “TCP 连接中断 / 终止阶段” 的攻击
  • PostgreSQL 灾备核心详解:基于日志文件传输的物理复制(流复制)
  • LINUX-网络编程-TCP-UDP
  • 【光照】[光照模型]发展里程碑时间线
  • 拆解《AUTOSAR Adaptive Platform Core》(Core.pdf)—— 汽车电子的 “基础技术说明书”
  • 无网络安装来自 GitHub 的 Python 包
  • More Effective C++ 条款18:分期摊还预期的计算成本(Amortize the Cost of Expected Computations)
  • 构建坚不可摧的数据堡垒:深入解析 Oracle 高可用与容灾技术体系
  • 开发中使用——鸿蒙CoreSpeechKit让文字发声
  • 基于SpringBoot的电脑商城系统【2026最新】
  • 【C++】第二十七节—C++11(下) | 可变参数模版+新的类功能+STL中一些变化+包装器
  • Gray Code (格雷码)
  • 【机器学习入门】4.1 聚类简介——从“物以类聚”看懂无监督分组的核心逻辑
  • 【蓝桥杯 2024 省 Python B】缴纳过路费
  • 网格纹理采样算法
  • SEO关键词布局总踩坑?用腾讯云AI工具从核心词到长尾词一键生成(附青少年英语培训实操案例)