命令行重要快捷键
[tab]补全命令,补全目录,补全命令参数等等
[Ctrl+c]强行终止当前程序,不退出终端
[Ctrl+d] 键盘输入结束或退出终端
[Ctrl+s] 暂定当前程序,暂停后按下任意键恢复运行
[Ctrl+z] 将当前程序放到后台运行,恢复到前台为命令fg
[Ctrl+a] 将光标移至输入行头,相当于Home键
[Ctrl+e] 将光标移至输入行末,相当于End键
[Ctrl+k] 删除从光标所在位置到行末
[Alt+Backspace] 向前删除一个单词
[Shift+PgUp] 将终端显示向上滚动
[Shift+PgDn] 将终端显示向下滚动
历史输入命令 ↑可以恢复你输入过的指令
通配符
通配符是一种特殊语句,主要有星号(*)和问号(?),用来对对字符串进行模糊匹配(比如文件名,参数名)。当查找文件夹时,可以使用它来代替一个或多个真正字符;当不知道真正字符或者懒得输入完整名字时,常常使用通配符代替一个或多个真正的字符。
man -k
man有一个-k 选项用起来非常好,这个选项让你学习命令、编程时有了一个搜索引擎,可以举一反三。结合后面学习的grep 命令和管道,可以多关键字查找: man -k key1 | grep key2 | grep key3 | ...
Man中分了八个区段,要查看相应区段的内容,就在 man 后面加上相应区段的数字即可,如 man 3 ls 。想要获得更详细的帮助,你还可以使用info命令,不过通常使用man就足够了。如果你知道某个命令的作用,只是想快速查看一些它的某个具体参数的作用,那么你可以使用--help参数,大部分命令都会带有这个参数
Linux 用户管理
查看用户
输入的第一列表示打开当前伪终端的用户的用户名(要查看当前登录用户的用户名,去掉空格直接使用 whoami 即可) ,第二列的 pts/0 中 pts 表示伪终端,所谓伪是相对于 /dev/tty 设备而言的,还记得上一节讲终端时的那七个使用 [Ctrl]+[Alt]+[F1]~[F7] 进行切换的 /dev/tty 设备么,这是“真终端”,伪终端就是当你在图形用户界面使用 /dev/tty7 时每打开一个终端就会产生一个伪终端, pts/0 后面那个数字就表示打开的伪终端序号,你可以尝试再打开一个终端,然后在里面输入 who am i ,看第二列是不是就变成 pts/1 了,第三列则表示当前伪终端的启动时间。
who 命令其它常用参数
-a 打印能打印的全部
-d 打印死掉的进程
-m 同am i,mom likes
-q 打印当前登录用户数及用户名
-u 打印当前登录用户登录信息
-r 打印运行等级
ls 命令的一些其它常用的用法:
l ls -A 显示除了 '.'(当前目录),'..' 上一级目录之外的所有包含隐藏文件(Linux 下以 '.' 开头的文件为隐藏文件)
l ls –Al 同时使用 '-A' 和 '-l' 参数:
l ls -dl <目录名> 查看某一个目录的完整属性,而不是显示目录里面的文件属性
l ls -AsSh 显示所有文件大小,并以普通人类能看懂的方式呈现
文件搜索
which小而精
which本身是 Shell 内建的一个命令,我们通常使用which来确定是否安装了某个指定的软件,因为它只从PATH环境变量指定的路径中去搜索命令:
$ which man
find精而细
find应该是这几个命令中最强大的了,它不但可以通过文件类型、文件名进行查找而且可以根据文件的属性(如文件的时间戳,文件的权限等)进行搜索。
1) 在指定目录下搜索指定文件名的文件:
$ find /etc/ -name interfaces
注意 find 命令的路径是作为第一个参数的,基本命令格式为:
find [path] [option] [action]
2) 与时间相关的命令参数:
-atime 最后访问时间
-ctime 创建时间
-mtime 最后修改时间
以-mtime参数为例:
-mtime n: n 为数字,表示为在n天之前的”一天之内“修改过的文件
-mtime +n: 列出在n天之前(不包含n天本身)被修改过的文件
-mtime -n: 列出在n天之前(包含n天本身)被修改过的文件
grep 命令
在文本中或 stdin 中查找匹配字符串
grep命令的一般形式为:
grep [命令选项]... 用于匹配的表达式 [文件]...
查找宏:
grep -nr XXX /usr/include(XXX为所要找的宏)
-r 参数表示递归搜索子目录中的文件
-n表示打印匹配项行号
-I表示忽略二进制文件
sort 排序命令
将输入按照一定方式排序,然后再输出,它支持的排序有按字典排序,数字排序,按月份排序,随机排序,反转排序,指定特定字段进行排序等等。
默认为字典排序:
$ cat /etc/passswd | sort
反转排序:
$ cat /etc/passwd | sort –r
按特定字段排序:
$ cat /etc/passwd | sort -t':' -k 3
上面的-t参数用于指定字段的分隔符,这里是以":"作为分隔符;-k 字段号用于指定对哪一个字段进行排序。这里/etc/passwd文件的第三个字段为数字,默认情况下是一字典序排序的,如果要按照数字排序就要加上-n参数:
$ cat /etc/passwd | sort -t':' -k 3 -n
cheat 搜索工具 例如:
cheat ls
Vim的六种基本模式
1.普通模式
Vim的普通模式用的编辑器命令,比如移动光标,删除文本等等。这也是Vim启动后的默认模式,而不是大多数编辑器的插入模式。
普通模式命令往往需要一个操作符结尾。
普通模式进入插入模式的方法:a键(append/追加)键或者i(insert/插入)键。
2.插入模式
在这个模式中,大多数按键都会向文本缓冲中插入文本。大多数新用户希望文本编辑器编辑过程中一直保持这个模式。
插入模式中回到普通模式:ESC键。
3.可视模式
与普通模式类似,但移动命令会扩大高亮的文本区域。高亮区域可以是字符、行或者是一块文本。当执行一个非移动命令时,命令会被执行到这块高亮的区域上。
4.选择模式
这个模式中,可以用鼠标或者光标键高亮选择文本。
如果输入任何字符,Vim会用这个字符替换选择的高亮文本块,并且自动进入插入模式。
5.命令行模式
在命令行模式中可以输入会被解释成并执行的文本。例如执行命令(:键),搜索(/和?键)或者过滤命令(!键)。
在命令执行之后,Vim返回到命令行模式之前的模式,通常是普通模式。
常用模式的切换:
常用模式:普通模式、插入模式和命令行模式。
普通→插入:
i 或 a
插入→普通:
Esc 或 Ctrl + [
普通→命令行:
:
命令行→普通:
Esc 或 Ctrl + [
命令行模式下输入wq ,回车后保存并退出。
删除文本
1.普通模式下删除vim文本信息
进入普通模式,使用下列命令可以进行文本快速删除:
x 删除游标所在的字符
X 删除游标所在前一个字符
Delete 同x
dd 删除整行
dw 删除一个单词(不适用中文)
d$或D 删除至行尾
d^ 删除至行首
dG 删除到文档结尾处
d1G 删至文档首部
vim重复命令
1.重复执行上次命令
在普通模式下.(小数点)表示重复上一次的命令操作。
2.执行指定次数相同的命令
进入普通模式输入N,N表示重复后面的次数。
在普通模式下, dw或者daw(delete a word)删除一个单词,所以dnw(n替换为相应数字)表示删除n个单词
二、游标的快速跳转
普通模式下,下列命令可以让光标快速调转到指定位置,我们分别讨论快速实现行间跳转和行内跳转
1.行间跳转 •nG(n Shift+g) 游标移动到第 n 行
•gg 游标移动到到第一行
•G(Shift+g) 到最后一行
2.行内跳转
普通模式下使用下列命令在行内按照单词为单位进行跳转 w 到下一个单词的开头 e 到下一个单词的结尾 b 到前一个单词的开头 ge 到前一个单词的结尾 0或^ 到行头 $ 到行尾
※ ~可以将游标所在字母转换大小写
f<字母> 向后搜索<字母>并跳转到第一个匹配的位置(非常实用)
F<字母> 向前搜索<字母>并跳转到第一个匹配的位置
编译器GCC
常用选项
-c 只编译不链接,生成目标文件.o
-S 只编译不汇编,生成汇编代码
-E 只进行预编译,不做其他处理
-g 在可执行程序中包含标准调试信息
-o file 将file文件指定为输出文件
-v 打印出编译器内部编译各过程的命令行信息和编译器的版本
-I dir 在头文件的搜索路径列表中添加dir目录
编译过程
预处理:
gcc –E hello.c –o hello.i; gcc –E调用cpp 生成中间文件
编 译:
gcc –S hello.i –o hello.s; gcc –S调用ccl 翻译成汇编文件
汇 编:
gcc –c hello.s –o hello.o;gcc -c 调用as翻译成可重定位目标文件
链 接:
gcc hello.o –o hello ;gcc -o 调用ld**创建可执行目标文件
静态库
ar命令的格式:
ar [-]{dmpqrtx}[abcfilNoPsSuvV] [membername] [count] archive files...
{dmpqrtx}部分称为操作选项
[abcfilNoPsSuvV]部分称为任选项
{dmpqrtx}中的操作选项在命令中只能并且必须使用其中一个,它们的含义如下: d:从库中删除模块。按模块原来的文件名指定要删除的模块。如果使用了任选项v则列出被删除的每个模块。
m:该操作是在一个库中移动成员。当库中如果有若干模块有相同的符号定义(如函数定义),则成员的位置顺序很重要。如果没有指定任选项,任何指定的成员将移到库的最后。也可以使用'a','b',或'i'任选项移动到指定的位置。
p:显示库中指定的成员到标准输出。如果指定任选项v,则在输出成员的内容前,将显示成员的名字。如果没有指定成员的名字,所有库中的文件将显示出来。
q:快速追加。增加新模块到库的结尾处。并不检查是否需要替换。'a','b',或'i'任选项对此操作没有影响,模块总是追加的库的结尾处。如果使用了任选项v则列出每个模块。 这时,库的符号表没有更新,可以用'ar s'或ranlib来更新库的符号表索引。
r:在库中插入模块(替换)。当插入的模块名已经在库中存在,则替换同名的模块。如果若干模块中有一个模块在库中不存在,ar显示一个错误消息,并不替换其他同名模块。默认的情况下,新的成员增加在库的结尾处,可以使用其他任选项来改变增加的位置。
t:显示库的模块表清单。一般只显示模块名。
x:从库中提取一个成员。如果不指定要提取的模块,则提取库中所有的模块。
a:在库的一个已经存在的成员后面增加一个新的文件。如果使用任选项a,则应该为命令行中membername参数指定一个已经存在的成员名。
b:在库的一个已经存在的成员前面增加一个新的文件。如果使用任选项b,则应该为命令行中membername参数指定一个已经存在的成员名。
c:创建一个库。不管库是否存在,都将创建。
f:在库中截短指定的名字。缺省情况下,文件名的长度是不受限制的,可以使用此参数将文件名截短,以保证与其它系统的兼容。
i:在库的一个已经存在的成员前面增加一个新的文件。如果使用任选项i,则应该为命令行中membername参数指定一个已经存在的成员名(类似任选项b)。
N:与count参数一起使用,在库中有多个相同的文件名时指定提取或输出的个数。
o:当提取成员时,保留成员的原始数据。如果不指定该任选项,则提取出的模块的时间将标为提取出的时间。
P:进行文件名匹配时使用全路径名。ar在创建库时不能使用全路径名(这样的库文件不符合POSIX标准),但是有些工具可以。
s:写入一个目标文件索引到库中,或者更新一个存在的目标文件索引。甚至对于没有任何变化的库也作该动作。对一个库做ar s等同于对该库做ranlib。
S:不创建目标文件索引,这在创建较大的库时能加快时间。
u:一般说来,命令ar r...插入所有列出的文件到库中,如果你只想插入列出文件中那些比库中同名文件新的文件,就可以使用该任选项。该任选项只用于r操作选项。
v:该选项用来显示执行操作选项的附加信息。
V:显示ar的版本。
动态链接共享库
构造创建共享库:(同静态库的范例) gcc -shared -fPIC -o libvector.so addvec.c multvec.c
参数解析:
-fPIC 指示编译器生成与位置无关的代码
-shared 指示链接器创建一个共享的目标文件
-o 命名生成文件
动作是把.c文件编译成为.o文件,放入新建的共享库中,并且命名。
链接程序
gcc -o p2 main2.c ./libvector.so
这样会创建一个可执行目标文件p2,在运行时可以和动态库libverctor.so链接。
静态库和动态共享库的代码区别 #:
创建
静态库的创建需要调用归档工具ar:
gcc -c addvec.c multvec.c
ar rcs libvector.a addvec.o multvec.o
而动态库只需要gcc即可:
gcc -shared -fPIC -o libvector.so addvec.c multvec.c
链接
静态库的链接需要先编译生成.o文件,然后再和库链接,并且需要staric命令构建一个完全链接的可执行文件:
gcc -02 -c main2.c
gcc -static -o p2 main2.o ./libvector.a
动态库可以直接把.c文件和库文件链接
gcc -o p2 main2.c ./libvector.so
GDB
最基本的命令:
gdb programm(启动GDB)
l 查看所载入的文件
b 设断点
info b 查看断点情况
run 开始运行程序
bt 打印函数调用堆栈
p 查看变量值
c 从当前断点继续运行到下一个断点
n 单步运行(不进入)
s 单步运行(进入)
quit 退出GDB
整数 # (需要注意的)
截断数字
不用额外的位来扩展数值,而是减少表示一个数字的位数。而这么做可能会改变它的值,这也是溢出的一种形式。
将一个w位的数截断为k位数字时,就会丢弃高w-k位。
对于无符号数来说,就相当于 mod 2的k次幂
对于有符号数来说,先按照无符号数截断,然后再转化为有符号数
补码运算
1.加法
两个数的w位补码之和与无符号之和有完全相同的位级表示。
溢出
补码加法的溢出情况比无符号运算更为复杂,分为正溢出、正常、负溢出。正溢出就是超过正数的最大范围,负溢出就是超过负数的最大范围,具体的公式在书58页,正负溢出的范围和原因,直观一点的图在59页。
但是其实公式里给的本质仍然是模运算,模掉w位的补码最高有效位的权重2的w次幂。
2.非
(1)补码的非运算
对于范围在[-2^(w-1),2^(w-1))中的x,补码的非运算有如下两种情况:
x=-2^(w-1)时,为-2^(w-1)
x>-2^(w-1)时,为-x
(2)求位级补码非 •对每一位求补,再对结果+1
•设k为最右面的1的位置,将k左边的所有位取反。
3.乘法
c语言中的有符号乘法是通过将2w位的乘积截断为w位的方式实现的。也就是说,需要mod 2的w次幂。
所以:对于无符号和补码乘法来说,乘法运算的位级表示都是一样的。
三、乘以常数
在机器运算中,乘法总是很慢的,而加法和移位(左移)是相对较快的。所以在编译器中,会使用移位和加法运算组合的方式来代替乘以常数因子。这种方法对于无符号运算和补码运算都是适用的。
1.常数为2的k次幂的时候
直接左移k位即可。
2.常数不是2的整数次幂的时候
将常数C表示为2的几个整数次幂的和,结合移位运算和加法运算。
溢出?不影响结果。
四、除以2的幂
机器运算中,除法比乘法更慢。当被除数为2的整数次幂时,通过右移来解决。右移时需要区分无符号数和补码。
ISA
定义了处理器处理状态、指令的格式,以及每条指令对状态的影响。
在命令行使用-S 选项,就能得到C语言编译器产生的汇编代码
64位的机器产生32位的代码:gcc -m32 -S xxx.c
二进制文件查看内容 使用反汇编器
unix> objdump -d code.o
指针使用示例
参数xp是一个指向整数的指针,而y是一个整数。像x这样的局部变量,通常是保存在寄存器中。
语句 int x =*xp;(指针的间接引用,将xp的值存放到名为x的局部变量)
语句 *xp = y;(将参数y的值写到xp所指的位置,标明的是一个写操作)
CMP指令根据它们的两个操作数之差来设置条件码;
除了只设置条件码而不更新目标寄存器之外,CMP指令与SUB指令的行为是一样的。
为单个过程分配的那部分栈称为栈帧。最顶端的栈帧以两个指针界定,寄存器%ebp为帧指针,而寄存器%esp为栈指针。
当程序进行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。
假设过程p(调用者)调用过程q(被调用者),则q的参数放在p的栈帧中。当p调用q时,p中的返回地址被压入栈中,形成p的栈帧的末尾。返回地址就是程序从q返回时应该继续执行的地方。q的栈帧从保存的帧指针的值开始,后面保存其他寄存器的值。
根据惯例,寄存器%eax \%edx\%ecx被划分为调用者保存寄存器。%ebx\%esi\%edi被划分为被调用者保存寄存器。
Y86指令集体系结构
程序员可见的状态
-
程序寄存器:%eax,%ecx,%edx,%ebx,%esi,%edi,%esp,%ebp。他们都可以存储一个字。%esp被入栈、出栈、调用和返回指令作为栈指针。
-
PC(程序计数器):存放当前正在执行指令的地址。
-
程序存储器:就是一个很大的字节数组,保存着程序和数据。Y86用虚拟地址来引用存储器位置,硬件和操作系统软件联合起来将虚拟地址翻译成实际或物理地址。
-
条件码:ZF、OF、SF。都是一位条件码,用来保存最近的算术或逻辑指令所造成影响的有关信息。
-
Stat:程序状态的最后一个部分是状态码,它表明程序执行的总体状态。它会指示是正常运行还是出现了某种异常。
Y86指令
-
1.在movl指令中,指令名字的第一个字母就表明了源类型。指令第二个字母指明了目的类型。立即数(i)、寄存器(r)、存储器(m)。两个存储器传送指令中的存储器引用方式是简单地基址和偏移量形式。
-
2.addl,subl,andl,xorl是四个整数操作指令,Y86中只允许对寄存器数据进行操作。
-
3.jmp,jle,jl,je,jne,jge,jg是七个跳转指令,根据分支指令的类型和条件码的设置来选择分支。
-
4.cmovle,cmovl,cmove,cmovne,cmovge,cmovg是六个条件传送指令,这些指令的格式与寄存器-寄存器传送指令rrmovl一样,但只有当条件码满足所需要的约束时,才会更新目的寄存器的值。
-
5.call指令将返回地址入栈,然后跳到目的地址。
-
6.pushl和popl指令实现了入栈和出栈。
-
7.halt指令停止指令的执行(IA32中的指令hlt)。
指令编码
每条指令的第一个字节表明指令的功能 这个字节分为两个部分 高四位是代码部分 低四位是功能部分
有的指令只有一个字节长,因为可能附加有寄存器指示符字节,指定一个或两个寄存器。这些寄存器字段为rA,rB。有则有,无则无,只有一个的则将第二个设为0xF。 有的指令需要一个附加的4字节常数字作为立即数数据或地址指示符偏移量或分支指令和调用指令的目的地址。所有整数采用小端法编码。
Y86异常
Y86程序 •以“.”开头的词是编辑器命令
•命令 .pos0告诉编辑器应该从地址0处开始产生代码,这个地址是所有Y86程序的起点
第二节 逻辑设计和硬件控制语言HCL
逻辑门 •与(AND):Y=a&&b
•或(OR):Y=a||b
•非(NOT):Y=!a
组合电路和HCL布尔表达式。
两个限制: •两个或多个逻辑门的输出不能连在一起
•这个网必须是无环的
字级的组合电路和HCL整数表达式
在HCL中,多路复用函数是用情况表达式来描述的,格式如下: [ select1 : expr1 select2 : expr2 ... selectk : exprk ]
集合关系
判断集合关系: iexpr in {iexpr1,iexpr2,...,iexprk} 其中被测值iexpr和待匹配的值iexpr1-iexprk
寄存器和时钟
组合电路简单的响应输入信号,产生等于输入的某个函数的输出,并存在状态在这个状态上进行计时,如此产生了时序电路。 储存器设备: •时钟计时器
•随机访问储存器 时钟计时器:
随机访问储存器:
第三节 Y86的顺序实现
将处理组织成阶段
1.取指 2.译码 3.执行 4.访问 5.写回 6.更新PC
SEQ硬件结构
1.取指 2.译码 3.执行 4.访存 5.写回
SEQ的时序
四个硬件单元:
1.程序计数器 2.条件码寄存器 3.数据存储器 4.寄存器文件
由于Y86指令集得本质,要遵循以下原则组织计算: 处理器从来不需要为了完成一条指令的执行而去读由该指令更新了的状态
SEQ阶段的实现 1.取指阶段,包括指令储存器硬件单元
2.译码和回写阶段
3.执行阶段,包括算数/逻辑单元
4.访存阶段,读或写程序数据
5.更新PC阶段
6.SEQ小结
第四节 流水线的通用原理
流水线化的一个重要特性是增加了系统的吞吐量
计算流水线
吞吐量={1 instruction/(加载寄存器时间+组合逻辑时间)}*{1000 picosecond/1 nanosecond}
流水线操作的局限性
书上的例子是一个理想的流水线化的系统,若将这个系统分成三个互相独立的阶段,每个阶段所需的时间应是原来逻辑需要时间的三分之一,然而现实中不能实现。 原因:
1.不一致的划分 2.流水线过深,收益反而下降
存储器的层次结构
存储技术 •静态RAM
SRAM将每一位存储在双稳态的存储器单元中 •动态RAM
DRAM将每一位存储为对电容充电。 •传统的DRAM
d*w的DRAM存储dw位信息 • 增强的DRAM
• 非易失性存储器
• 访问主存
磁盘存储 •磁盘构造 •磁盘容量
公式:磁盘容量=(字节数/扇区) * (平均扇区数/磁道)* (磁道数/表面)*(表面数/盘片) * (盘片数/磁盘) •磁盘操作
磁盘以扇区大小的块来读写数据。对扇区的访问时间有三个主要部分:寻道时间、旋转时间、和传送时间。
旋转时间:Tmax rotation =(1/RPM)* (60 secs/1 min) Tavg 是 Tmax 的一半。
传送时间:Tavg transfer =(1/RPM)* (1/平均扇区数/磁道)* (60 secs/1 min)
访问时间= 平均寻道时间 + 平均旋转时间 + 平均传送时间
局部性
局部性分为时间局部性和空间局部性。 •对数据引用的局部性
顺序引用模式(步长为1);步长为K的引用模式;
随着步长的增加,空间局部性降低
双重嵌套循环按照优先顺序访问数组元素
程序局部性的简单原则: • 重复引用同一个变量的程序具有良好的时间局部性
• 对于具有步长为k的引用模式的程序,步长越小,空间局部性越好。具有步长为1的引用模式的程序有很多的空间局部性。在存储器中以大步长跳来跳去的程序空间局部性会很差
• 对于取指令来说,循环有好的时间和空间局部性。循环体越小,循环迭代次数越多,局部性越好。
存储器层次结构
中心思想:对于每个K,位于K层的更快更小的存储设备作为位于k+1层的更大更慢的存储设备的缓存。 • 缓存命中
• 缓存不命中
• 缓存不命中的种类:强制性不命中(冷不命中)、冲突不命中;
• 缓存管理
高速缓存存储器
通用的高速缓存存储器结构
M = 2 ^m S = 2^s B = 2^b C = S * E* B
t=m-(b+s)
高速缓存工作方式:
1、组选择 2、行匹配 3、字抽取
学习总结:
在这八周的学习中,我对计算机系统有所理解,也逐渐理解了计算机语言的使用,以及部分Linux系统的使用。比如gcc编译,gdb调试,静态库动态库的建立。
学习中,一开始觉得很难,毫无头绪地跟着做实验,量很大,一直对着电脑晕晕沉沉的,记不住知识点。常常是学习下一部分内容,就忘了上一部分内容,效果很差。后来学习内容逐渐转移到课本上,知识点感觉也掌握得比之前的牢固些。学习书本上的知识也不需要网络,学习上也有一定的便利。书本上的知识比用电子产品印象来得深刻些。 我同时也反省自己一开始实践太少,自己以后应该要勤于实践,循序渐进。一开始第一次实验中,自己的gcc\gdb\都没有实验成功。后来自己慢慢地继续学了之后,感觉还是很有成就感的。有些实验不是一开始就可以做出来,还要根据自己越学愈多,实验才能完成的更好。
在这里也想给娄老师提些小建议:实践很有必要,但是希望能够一点一点的来,这样会扎实些。比如一星期学个vim,一星期学个gcc调试,任务量减小点,让我们把该掌握的东西扎扎实实地学好。课上最好还可以讲些我们书本习题中存有疑虑的地方,毕竟这学期我们还是以《深入理解计算机系统》这本书为主展开的学习内容。这是我的个人粗鄙的想法,希望娄老师能酌情采纳。
资料参考: 《深入理解计算机系统》 、20135302闫佳歆博客