4 文件IO
操作
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
creat
int fd=creat(char *filename,mode_t mode);
mode指定文件的权限模式;
实际权限模式=指定权限模式mode & 新建文件掩码umask的反码;
int res=unlink(char* path);//删除path到文件的一个链接,将文件对于的i-node链接数-1
open
int fd=open(char* name,int how);
int fd=open(char* name,int how,mode_t mode);
how:
O_APPEND:写文件之前,自动将文件位置指针移动到文件末尾(且lseek的指针移动不影响写时的追加);
O_TRUNC:将fd指向的文件内容清空;
O_CREAT:打开的文件不存在时,自动创建该文件,需要使用mode参数指定文件权限;
O_EXCL|O_CREAT:文件已存在时返回错误;
O_NONBLOCK:对文件无阻塞读写,即读时不需要等待数据,没数据-1返回设置errno为EAGAIN,写时没位置也不等待;
int res=close(fd);
read
int read(int fd, const void *buf,size_t nbytes);
fd:要读取数据的文件描述符
buf:缓冲区,读取到的数据会被放在缓冲区中
nbytes:需要从文件中读取的字节数
返回值:返回所读取的字节数;读到EOF,读取字符小于length,返回0;出错返回-1;
int write(int fd, const void *buf,size_t nbytes);
fd:写入文件的文件描述符
buf:指向缓冲区,存放了需要写入文件的数据
nbytes:需要写入文件的字节数
返回值:成功时返回实际写入文件的字节数,出错返回-1
lseek
off_t oldpos = lseek(int fd,off_t dist,int base);
fd:文件描述符
dist:文件指针移动距离 可正可负
base:指针起点 可以为:
SEEK_SET从文件开始的地方
SEEK_END从文件结尾的地方
SEEK_CUR从指针当前位置
返回值:成功时oldpos为移动之前的位置;失败时返回-1 设置errno
vi编辑器会在每一行的结尾自动添加换行符
fcntl
int result = fcntl(int fd,int cmd);
int result = fcntl(int fd,int cmd,long arg,...);
fd:文件描述符
cmd:具体操作
arg:操作所需参数
FD:文件描述符系列
F_DUPFD 复制文件描述符
F_GETFD 获得文件描述符
F_SETFD 设置文件描述符
FL:文件描述符当前模式系列
F_GETFL 获取文件描述符当前模式
F_SETFL 设置文件描述符当前模式
OWN:异步I/O所有权
F_GETOWN 获得异步I/O所有权
F_SETOWN 设置异步I/O所有权
LK:记录锁系列
F_GETLK 获得记录锁
F_SETLK 设置记录锁
F_SETLKW 设置记录锁 同时也是上面的wait版本?
如果无法锁定那它就会一直等待直到可以锁定为止。
一旦进入等待状态,则这个函数只有在被可以进行锁定或者是接收到信号时才会返回。
返回值:成功时取决于cmd参数的值;失败返回-1 设置errno
int flag=fcntl(fd,F_GETFL,0); //0不知道什么玩意
flag |= F_NONBLOCK;//设置为阻塞
flag &= ~F_NONBLOCK;//设置为非阻塞
fcntl(fd,F_SETFL,0);
stat
获取文件和目录的信息,并保存在struct stat结构体
int stat(char *fname,struct stat *bufp);
返回值:成功返回0;失败返回-1;
int lstat(char* path,struct stat* buf);
//path为符号链接时,lstat得到符号链接文件本身的信息,stat得到符号链接指向的文件信息
struct stat {
mode_t st_mode; //文件对应的模式,文件,目录等
ino_t st_ino; //inode节点号
dev_t st_dev; //设备号码
dev_t st_rdev; //文件为字符或块设备时的设备号码
nlink_t st_nlink; //文件的连接数
uid_t st_uid; //文件所有者
gid_t st_gid; //文件所有者对应的组
off_t st_size; //普通文件,对应的文件字节数
time_t st_atime; //文件最后被访问的时间
time_t st_mtime; //文件内容最后被修改的时间
time_t st_ctime; //文件状态改变时间
blksize_t st_blksize; //文件内容对应最佳IO块大小
blkcnt_t st_blocks; //系统为文件分配的数据块数量
};
判断文件类型,用掩码与模式进行&操作,将结果与文件类型常量(如S_IFDIR目录文件、S_IFCHR字符设备文件、S_IFIFO命名管道文件)进行比较来判断;
link&symlink
int link(const char *src,const char *dest);
int symlink(const char *src,const char *dest);
//link硬链接 symlink软链接
chown&chmod&utime
int chown(const char *path,uid_t owner,gid_t group);
//将参数path指定文件的所有者变更为参数owner代表的用户,而将该文件的组变更为参数group组
owner文件新所有者标识符;
group文件新组标识符;
int chmod(const char *path,mode_t mode);
#include <utime.h>
int utime(const char *path,const struct utimbuf *times);
struct utimbuf{
time_t actime; //访问时间
time_t modtime; //文件内容修改时间
};
其他
文件锁
- 文件锁包括建议性锁和强制性锁
- flock()函数用于上建议性锁。
- fcntl()二者均可,还能对文件的某一记录上锁,也就是记录锁。
- 记录锁又分读取锁和写入锁。
- 读取锁又称共享锁,可以多个进程在文件同一部分建立读取锁,
- 写入锁是互斥锁,在任意时刻只能有一个进程在文件的某个部分建立写入锁。
- 已有读取锁,阻塞读或非阻塞读均可正常读取数据,阻塞写会阻塞,非阻塞写会返回EAGAIN错误
- 已有写入锁,阻塞读和阻塞写被阻塞,非阻塞读和非阻塞写返回EAGAIN错误;
struct flock
struct flock{
short l_type; //lock的类型
short l_whence; //偏移量的起始位置,SEEK_SET, SEEK_CUR, or SEEK_END
off_t l_start; //从l_whence参数指定位置开始的偏移量(字节为单位)
off_t l_len; //从指定位置开始连续被锁住的字节数,0代表对剩下所有的内容上锁
pid_t l_pid; //返回拥有锁的进程id
};
l_type:
当fcntl的命令参数为SETLK时,
F_RDLCK:请求读锁;
F_WRLCK:请求写锁;
F_UNLCK:解锁;
当fcntl的命令参数为GETLK时,
F_RDLCK:已存在冲突读锁;
F_WRLCK:已存在冲突写锁;
F_UNLCK:不存在冲突锁;
如果要加锁整个文件,将l_start=0,l_whence=SEEK_SET,l_len=0。
新锁替换老锁:如果一个进程对文件区间已经加锁,后来该进程又企图在同一文件区域再加一把锁,那么新锁将替换老锁。
inode
文件储存在硬盘上,磁盘最小储存单位是扇区(0.5KB大小)
操作系统读取硬盘时会一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。 块大小通常是4KB。
文件数据都储存在"块"中,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。 每一个文件都有对应的inode。块位图和inode位图
块位图:记录了本组数据块的使用情况,每一块对应一个bit,为1表示被占用,本身也会占据一个数据块。
inode位图:记录inode表中inode的使用情况,也占用一个数据块。
//inode内容
1.文件的字节数
2.文件拥有者id
3.文件所属组id
4.文件的三种权限
5.三种时间戳:
ctime指inode上一次变动时间
mtime指文件内容上一次变动时间
atime指文件上一次打开的时间
6.链接数,有多少个文件名指向这个inode
7.文件数据的位置
//creat一个文件:
1.内核从inode表中找到可以使用的inode来存此文件的属性,具体见上
2.内核从数据块中找到空闲数据块,将数据写入,并将数据块的地址写入此inode中
3.将inode位图和块位图更新,将使用块的对应位的值置为1
4.将文件名字及inode存储到文件所述的目录文件中
- 由于每个文件都必须有一个inode,因此有可能发生inode已经用光,但是硬盘还未存满的情况。这时,就无法在硬盘上创建新文件。
- 系统内部将“ 用户通过文件名,打开文件 ”这个过程分成三步:首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。
- inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区,存放inode所包含的信息
命令
stat 文件名 //查看inode
df -i //查看每个硬盘分区的inode总数和已用数量
ls -i 文件名 //获得文件名的inode号码(如果是文件夹,获得文件夹下文件的inode)
目录
#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>
int main(int ac,char **av){
DIR *dir;
struct dirent *content;
dir=opendir(av[1]);//打开目录
if(dir!=NULL){//有目录
while((content=readdir(dir))!=NULL){//还有目录项
printf("%s\n",content->d_name);
}
close(dir);
}
}
DIR *opendir(const char *dir_name);//打开目录,无此目录返回NULL
struct dirent *readdir(DIR *dir);//读取目录的信息并保存到结构体,读完目录项返回NULL
struct dirent{
char d_name[1]; //文件名称
int d_fileno; //文件inode号
};
int closedir(DIR *dir);
void seekdir(DIR *dir,off_t offset);//调整下一次调用readdir函数的位置
off_t telldir(DIR *dir);//成功时返回下次读取位置,失败-1
void rewinddir(DIR *dir);//重置目录流的位置到起点位置
int mkdir(char *pathname,mode_t mode);//路径和权限
int rmdir(char *pathname);//路径,删除目录时,要求目录必须为空!
int chdir(const char *path);//切换进程的工作目录
int rename(const char *from,const char *to);//重命名或移到新位置
char *getcwd(char *buf,size_t size);//获取当前工作目录的绝对路径到buf,size=buf的空间大小
链接
硬链接
- 命令**
ln file hardlink
**;不同文件名对应同一个inode,一个文件修改另一个文件也变化; - 创建硬链接inode的链接数++,
unlink
删除其中一个文件名 inode的链接数–,当这个值减到0,表明没有文件名指向这个inode,系统就会回收这个inode号码,以及其所对应block区域。 - 目录文件的"链接数":
创建目录时,默认会生成两个目录项:".“和”…"。
前者的inode号码就是当前目录的inode号码,等同于当前目录的"硬链接".
后者的inode号码就是当前目录的父目录的inode号码,等同于父目录的"硬链接"。
所以,任何一个目录的"硬链接"总数,总是等于2加上它的子目录总数(含隐藏目录),也就是说新建立的目录的硬链接会被默认为2。
如果再在里面创立一个子目录 原来的目录就变成3了,而不是2+里面的2。
如果再在子目录新建,只会改变子目录的硬连接数,不会动最开始的硬链接数,因为子目录的子目录已经不是子目录了。
软链接/符号链接
- 命令**
ln -s file softlink
**; - A是B的软链接,i-node互不相同。但是文件A的内容是B的路径即指向名字不是指向inode。读取文件A时,系统会导向文件B。所以此时打开A、B,读取的都是文件B。
此时删掉文件B,再打开A,会发生:“No such file or directory”。软链接创建的时候inode链接数不会发生变化。
Tips
- 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。
- 移动文件或重命名文件,只是改变文件名,不影响inode号码。(这就是为什么查询inode的时候不会显示文件名。)
- 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。软件更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode则被回收。
5 进程管理
- 程序:静态指令集合,不占用系统运行资源;
- 进程:随时可能发生变化的、动态的、使用系统运行资源的程序;
- 线程:
- 关系:每个运行的程序至少由一个进程组成,一个程序运行多次产生多个进程;
Linux有3种进程类型:交互进程、批处理进程、守护进程
父子进程
父子进程具有相同的代码段、数据段,独立改变不受影响;
僵尸进程指父进程还没有结束而子进程已经结束运行,父进程没有wait调用获取子进程的结束状态,子进程就会成为僵尸进程。 用ps
命令查看进程时,如果进程名称旁边出现defunct则表明是僵尸进程;
孤儿进程父进程比子进程先退出,子进程变成孤儿进程,被1号进程也叫init进程收养;
创建子进程时,子进程会继承父进程的信号处理方式。除非调用exec函数,会将父进程的处理方式还原为默认;
子进程退出时,会向父进程发送SIGCHLD信号,默认情况下父进程忽略,但是可以用wait
或者waitpid
获取子进程的退出状态;
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
WNOHANG参数调用waitpid,没有子进程结束也会立即返回,不会像wait那样永远等下去。
WUNTRACED参数不常用 貌似可以不掌握
如果我们不想使用它们,也可以设为0
WIFEXITED(status)非0表明进程正常结束。
WIFSIGNALED(status)非0表明进程异常终止。
WIFSTOPPED(status)非0表明进程处于暂停状态
WIFCONTINUED(status)非0表示暂停后已经继续运行。
//但事实上wait就是特殊的waitpid 或者说是经过包装的waitpid
pid_t wait(int * wait_stat) {
return waitpid(-1,wait_stat,0);
}
pid_t waitpid(pid_t pid, int* status, int options);
options:
WNOHANG表示如果没有任何已经结束的子进程则马上返回,不等待;
WUNTRACED表示如果子进程进入暂停执行清空则马上返回,但结束状态不理会;
0与wait()作用一样,阻塞父进程;
#include <sys/types.h>
#include <wait.h>
#include <signal.h>
#include <unistd.h>
int pid,status;
whiel((pid=waitpid(0,&status,WNOHANG)) > 0){
if(WIFEXITED(status))//正常退出,输出进程号和退出码
printf("Child %d exit %d normally\n", pid, WEXITSTATUS(status));
else if(WIFSIGNALED(status)) //因信号退出
printf("Child %d terminate by signal %d abnormally\n", pid, WTERMSIG(status));
else if(WIFSTOPPED(status)) //因信号赞同
printf("Child %d stopped by signal %d abnormally\n", pid, WSTOPSIG(status));
else printf("else");
}
exec
int execvp(const char *file,const char *argv[]);
//在指定路径中查找并执行一个文件
file 要执行的程序的名称或路径
argv 要传给程序的内容;存在字符串数组中
返回值:成功没有返回值;失败-1——没找到可执行文件时
execvp和execv,以p结尾表示可以只给出文件名,系统自动从环境变量$PATH种找;
l(list),v(vector)
执行过程:将指定程序复制到调用当前进程;将字符串数组打包变成传说中的argv数组传给程序;运行程序;
//一个wzb的例子
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
int main(){
char buf[10],s1[10]="zss1",s2[10]="zss2";
int pid,fd;
fd=open("ret.txt",O_RDWR);//这里书上有误,写的只写 和后面的描述对不上
read(fd,buf,5);
printf("%s\n",buf);
pid=fork();
if(pid==0){
write(fd,s1,strlen(s1));
}else{
write(fd,s2,strlen(s2));
}
return 0;
}
/* in ret.txt
aaaaa
bbbbbb
ccccccccc
*/
//after ./re1
/* in ret.txt
aaaaazss2zss1ccccccccc
*/
//这是因为读完5个a之后 父子进程都在回车的位置这个时候zss2和zss1代替了回车+6个b+回车因此剩下9个c会接着 这也就说明了 父子进程共享文件读写位置 当然可能出现一个写了一半 被另一个拦住先写完了也就是可能会有"zszss1s2"的东西出现
守护进程
进程组:一组进程集合,进程组PID是进程组组长的ID,一般而言终端是进程组组长;
会话期:始于用户登录,结束于用户退出,期间所有用户运行的进程组都属于此会话期;
创建守护进程
#include<unistd.h>
#include<sys/types.h>
int pid=fork();//创建子进程
setsid();//子进程脱离终端控制,创建新的会话并成为组长
chdir("/");//改变当前目录为根目录
umask(0);//修改文件权限掩码
close(fd);//关闭0、1、2等文件描述符
6 信号及信号处理
信号分类
-
不可靠信号:指多个信号产生时会无法及时处理,造成信号丢失 来源于早期UNIX系统简单原始的信号机制;信号值小于SIGRTMIN的均为不可靠信号
-
可靠信号:克服了信号可能丢失的问题(通过信号排队); 而SIGRTMIN到SIGRTMAX闭区间的信号都是可靠信号
-
非实时信号,不支持排队,不可靠信号。
-
实时信号,支持排队,可靠信号。
-
SIGKILL 、SIGSTOP不能被忽略
信号 说明 默认处理 SIGINT Ctrl+C 即^C 进程终止 SIGQUIT Ctrl+\即^\ 进程终止+core文件 SIGALRM alarm或setitimer定时器 进程终止 SIGCHLD 子进程退出时向父进程发送 父进程忽略此信号 SIGUSR1 用户自定义信号 进程终止
信号处理
#include<signal.h>
void handler(int signum, siginfo_t* info,void* myact){
//balabala
}
struct sigaction act;
act.sa_flags=SA_SIGINFO;
act.sa_sigation=handler;
signal
int signal(int signum, void(*action)(int));
返回值:-1失败;prevaction返回之前的处理函数指针;
signal(SIGINT,SIG_IGN);//忽略SIGINT信号,SIGKILL和SIGSTOP不能忽略
signal(SIGINT,SIG_DEF);//默认方式处理,缺省处理
若进程执行一个阻塞系统调用如read()
时,发生了一个信号;此时若用户输入的内容在回车之前遇到信号,则程序没有读取(hello ^C \n);若用户输入内容在回车后遇到信号,则可以读取用户输入(hello \n ^C读到hello,hel ^C lo \n 读到lo);
sigaction
int sigaction(int signum, const struct sigaction* act, struct sigaction* prev);
signum:处理的信号;act:进行的操作;prev:被替换的操作;返回值:成功0失败-1;
struct sigaction{
void* sa_handler(int signum);//相当于signal函数
void* sa_sigaction(int signum, siginfo_t*, void*);//处理函数
sigset_t sa_mask;//包含信号集合的结构体
int sa_flags;//行为标志
}
对sa_mask操作:
sigaddset(sigset_t* ,int);//加入
sigfillset(sigset_t *);//所有信号填充
sigdelset(sigset_t*,int);//删除
sigemptyset(sigset_t*);//清空所有
sigismember(sigset_t*,int);//判断信号是否在集合中
对sa_flags设置:
SA_RESETHAND:当调用信号处理函数时或信号处理函数结束后,将信号的处理设置为系统默认值;
SA_NODEFER:处理此信号时,如果出现别的信号,立刻进入其他信号的处理,不阻塞;
SA_RESTART:由此信号中断的系统调用会自动重 启;
SA_SIGINFO:sa_sigaction有效,会提供附加信息siginfo_t结构的指针;
返回值:成功0失败-1;
sigprocmask
int sigprocmask(int how,const sigset_t *newset,sigset_t *oldset);
SIG_BLOCK newset||oldset
SIG_UNBLOCK oldset-newset
SIG_SETMASK oldset=newset
olddset用于保存进程原有的信号量集,程序执行完之后一般要还原;
用法:
sigset_t newset,oldset;
//一些操作,向newset中添加信号
sigprocmack(SIG_BLOCK,&newset,&oldset);//阻塞newset的信号,原信号集保存在oldset中
//其他操作
sigprocmask(SIG_BLOCK,&oldset,NULL);//阻塞oldset中的信号
信号发送
kill
int kill(pid_t pid,int sig);
raise
int raise(int sig);
//向自身进程发送信号
//相当于kill(getpid(),signum);
sigqueue
发送信号,传递附加信息
int sigqueue(pid_t pid,int sig,const union sigval value);
value:
union sigval{
int sival_int;
void* sival_ptr;
};
此参数会被进程信号处理函数获得 sa_flags包含SA_SIGINFO选项会吧value传给信号处理函数
可重入函数
可重入函数:函数可被多个任务并发使用,不会造成数据错误,则具有可重入性;可重入函数不能使用静态变量、malloc/free函数和标准IO库、全局变量。
lab中的描述:
异步信号安全函数(async-signal-safe function)是可以在信号处理函数中安全调用的函数,即一个函数在返回前被信号中断,并在信号处理函数中再次被调用,均可以得到正确结果。通常情况下,不可重入函数(non-reentrant function)都不是异步信号安全函数,都不应该在信号处理函数中调用。
异步信号安全函数、可重入函数、线程安全函数是三个不同的概念,有细微差别,具体请查阅资料。
定时
sleep
#include<unistd.h>
unsigned int sleep(unsigned int seconds);
void usleep(unsigned long usec);
sleep=alarm+pause;
使用pause的原因是将进程挂起防止提前退出;
pause
将进程挂起,当进程收到信号后返回。
alarm
unsigned int alarm(unsigned int seconds);
seconds秒后发送SIGALARM的信号,如果没有SIGALARM处理函数,则进程终止;
如果seconds秒内,再次调用了alarm函数,会对之前的进行覆盖;
如果seconds=0,之前设置的定时器闹钟将被取消,并将剩下的时间返回。
//每2s处理一下
void sig_handler(int num){
printf("receive the signal %d.\n",num);
alarm(2);
}
int main(){
signal(SIGALRM,sig_handler);
alarm(2);
while(1){//做一个死循环,防止主线程提早退出,相等于线程中的join
//也就是一直让出CPU 防止归还给主线程之后exit(0)跑路
pause();
}
exit(0);
}
计时器分为真实计时器、虚拟计时器和实用计时器。
真实计时器是程序运行的实际时间。SIGALRM
虚拟计时器计算的是用户态=实际时间-系统调用切换-程序睡眠。这个东西也叫处于用户态消耗的时间。SIGVTALRM
实用计时器计算的是虚拟机时期的时间+处于内核态所消耗的时间之和。SIGPROF
setitimer
int getitimer(int which,struct itimerval* value);
which表示获取的是哪个计时器 可以选:
ITIMER_REAL(真实)
ITIMER_VITUAL(虚拟)
ITIMER_PROF(实用)
value用来保存初始间隔时间和重复间隔时间;
返回值:成功0失败-1;
int setitimer(int which, struct itimerval* value,struct itimerval* ovalue);
struct itimerval{
struct timeval it_value;///设置总时间
struct timeval it_interval;//设置间隔
}
struct timeval{
long tv_sec;//时间的s
long tv_usec;//时间的us
}
#include<sys/time.h>
struct timerval t;
t.it_alue.tv_sec=1;
t.it_value.tv_usec=0;//总时间1s,0us
t.it_interval.tv_sec=1;
t.it_interva.tv_usec=0;//间隔1s,0us
setitimer(ITIMER_REAL,&t,NULL)//设置计时器,SIGALRM 信号
7 进程间通信
进程通信的作用:数据传输、共享数据、通知事件、资源共享、进程控制;
Linux支持的进程通信方式:管道、信号、消息队列(克服信号的信息量少、管道只能传输无格式字节流以及缓冲区大小受限的缺点)、共享内存(最快)、信号量、套接字;
比较
本地套接字:最稳定
信号:开销最小
共享内存:速度最快,进程间耦合性强;
信号量:适用于进程之间控制信号的传递,不能传输数据;
消息队列:对进程的关系没有要求,能传递大量、不同类型数据;是一种特殊文件;
管道:使用简单,只能传递无格式字节流,缓冲区大小受限,单向;
-
无名管道:父子进程通信;
-
命名管道:不同进程传递数据;是一种特殊文件;
管道
特点:单工单向、数据以字节流形式传送;
无名管道
#include<unistd.h>
int pipe(int pipe[2]);
pipe[0]为读端文件描述符,pipe[1]写端;
使用举例:父子进程通信,父进程创建管道pipe(mypipe),创建子进程,父进程write(&mypipr[1],sendmsg,strlen(sendmsg)),子进程read(&mypipe[0],recmsg,strlen(sendmsg));
命名管道
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *pathname,mode_t mode);
//创建了一个管道文件 没有亲缘关系的进程可以打开此文件,向文件读写数据,达到通信的目的
//对管道文件读写,读进程在管道空时阻塞,写进程在管道满时阻塞
mkfifo("hello",0666);
管道文件的标识是p,占用inode不占用数据块
管道与重定向
int newfd=dup(oldfd);
复制文件描述符oldfd,此时oldfd和newfd都指向文件;
int newfd=dup2(oldfd,newfd);
相当于先关掉newfd,再将newfd赋值为oldfd
均返回newfd的值,错误返回-1;
oldfd非法,newfd不关闭,并返回错误;oldfd合法,但oldfd=newfd,只返回newfd,newfd不关闭;
//标准输入变为a.txt
int fd1=open("a.txt",O_RDWR);//fd1=3
int newfd=dup2(fd1,0);//关闭0,然后newfd赋值fd1,根据文件描述符最低可用原则,newfd=0
ps -aux|grep init
FILE* fd=popen("command",mode);
command:需要执行的命令,shell任意命令
mode为w或r,表示向进程从中写或读数据
readfp=popen("ps aux","r");
writefp=popen("grep init","w");
pclose(readfp);pclose(writefp);
信号量
//操作极其繁琐的。见课本158
semget
int semget(key_t key,int sems,int semflag);
key:多个进程通过信号量的键值key访问同一个信号量 semflag为IPC_PRIVATE时,表示创建当前进程的私有信号量
sems:表示创建的信号量数
semflag:前四个数八进制表示权限,后面会或
1.IPC_CREAT表示无论这个信号量存不存在都新建新的信号量;
2.IPC_EXCL表示信号量如果已经存在则返回错误;
返回值:成功返回创建的信号量标识符,施拜返回-1;
semget(ftok(".",'a'),1,0666|IPC_CREAT);
key_t ftok(const char* pathname, int proj_id);//获取指定文件的i节点号,在其之前加上子序列号作为键值返回
semctl
int semctl(int semid,int semnum,int cmd,union semun arg);
semid:semget的返回值,信号量标识符;
semnum:信号量编号,一般为0,表示取第一个,书上说使用信号量集才会真正有用;
cmd:
是对信号量的操作 对于单个信号量:
IPC_STAT:获取信号量的semid_ds结构 并直接转交给arg的semid_ds
IPC_SETVAL:将arg参数中的值设置为信号量的值
IPC_GETVAL:获得信号量的值
IPC_RMID:从系统中删除指定信号量
union semun{
int val;
struct semid_ids *buf;
unsigned short *array;
}
返回值:成功时IPC_GETVAL返回信号量当前值,其他cmd的值返回0;失败时返回-1
信号量集:当任务需要与多个事件同步时,根据多个逻辑信号量组合作用的结果来决定任务的运行方式,于是就需要把他们归到一起,就有了信号量集;
semop
int semop(int semid,struct sembuf *sops,size_t nsops);
semid:semget函数的信号量标识符
sops:指向信号量操作数
struct sembuf{
short sem_num;//信号量编号 使用单个信号量时 取值为0
short sem_op;//-1表示P操作 +1表示V操作
short sem_flag;//通常设置为SEM_UNDO,在进程未释放信号量直接退出时,由系统自动释放
};
nsops:表示操作个数
返回值:成功时返回信号量标识符;失败时返回-1
POSIX有名信号量
char SEM_NAME1[]="process1";
char SEM_NAME2[]="process2";
sem_t* sem1=sem_open(SEM_NAME1,O_CREAT,0777,1);//初始资源1个
sem_t* sem2=sem_open(SEM_NAME2,O_CREAT,0777,0);//初始0个
while(1){
sem_wait(sem1);
//操作
sem_post(sem2);//进程1,进程2跟他相反即可,实现交替
}
sem_close(sem1);//指针
sem_unlink(SEM_NAME1);//指针
sem_open
sem_t* sem_open(const char *name,int oflag,mode_t mode,int value);
name:路径名,有命信号量一般存在/dev/shm下;
oflag:O_CREAT(不存在创建)或O_EXCL(不存在返回错误);
mode:有命信号量的访问权限;
value:信号量初始值;
sem_close
关闭信号量
int sem_close(sem_t *sem);
sem_unlink
删除有命信号量
int sem_unlink(const char* name);
P操作V操作
共享内存
允许不相关的进程访问同一逻辑内存。指在两个运行的进程之间共享和传递数据的一种非常有效的方式。
逻辑内存的空间甚至可以超过物理内存的实际空间,从逻辑内存到物理内存会有一个地址转换的过程;共享内存共享的是逻辑内存,但逻辑内存相同映射到物理内存也会是相同的。共享内存直接操作的是逻辑地址,不能直接操作物理地址。共享内存的本质也就是因为从逻辑地址映射到的物理地址相同,避免了数据拷贝
fork产生自进程时,父进程创建的共享内存区段都会被自进程继承,即共享内存区段。
课本P163
第1句话,“共享内存允许不相关的进程访问同一个逻辑内存”;
第3句话,“不同进程之间共享的内存通常安排为同一段物理内存”;
第5句话,“引起数据从用户地址空间向内核地址空间的一次复制”;
第7句话,“共享内存会映射到进程的虚拟地址空间”;
疑问:逻辑内存、物理内存、用户地址空间、内核地址空间、虚拟地址空间相互的关系是什么?
听说虚拟地址空间远远大于物理内存,这是什么机制?应该看什么来理解这些呢
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VvPyRE3h-1626501134058)(img/8 多线程编程_img/image-20210615195354238.png)]
搭配信号量
int shmID=shmget(KEY,1024,IPC_CREAT|0666);
//键值KEY为其他任意值时可以非亲缘关系进程共享内存,为PRIVATE时为父子关系
int* addr=shmat(shmID,0,0);//将共享内存映射到程序地址空间,0代表系统自动分配地址,0代表可读可写,SHM_RDONLY只读
sem_t* sem=sem_open(NAME,O_CREAT,0666,1);//有名信号量
sem_init(sem,2,1);//非0代表进程间共享,1代表1个资源,但是这个键值不知道,进程间共享不方便???
sem_wait(sem);//P操作
*addr=m;//将m写入共享内存
m=*addr;//从共享内存中读出
sem_post(sem);//信号量V操作
shmdt((void*)addr);//取消映射
shmctl(shmID,IPC_RMID,NULL);//删除共享内存
sem_close(sem);//关闭信号量
sem_unlink(NAME);//删除有名信号量
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
shmget
获取共享内存区的ID
int shmget(key_t key, int size, int shmflg);
key共享内存的键值,多个进程通过它访问同一共享内存;当使用IPC_PRIVATE时,表示key为0,此key和IPC对象的变化没有对应关系,就不能通过key值得到IPC对象的编号,因而不能用于毫无关系的进程间通信,常用于父子进程。
size共享内存区大小;
shmflg四位八进制数,IPC_CREAT|0666;
返回值:成功时返回共享内存段标识符;失败时返回-1
shmat
将共享内存区映射到进程的地址空间;
char *shmat(int shmid,const void *shmaddr,int shmflg);
shmid:共享内存区标识符;
shmaddr:将共享内存映射到指定地址 (0则表示系统自动分配地址 并映射到调用进程的地址空间);
shmflg:SHM_RDONLY共享内存只读;0共享内存可读可写;
返回值:成功时返回被映射的段地址;失败时返回-1
shmdt
解除映射
int shmdt(const void *shmaddr);
shmaddr 被映射的共享内存段地址;
返回值:成功0,失败-1;
shmctl
int shmctl(int shmid, int cmd,struct shmid_ds *buf);
shmid:共享内存标识符;
cmd:对共享内存执行的命令;
IPC_STAT:得到共享内存的状态,把共享内存地shmid_ds结构复制到buf中
IPC_SET:把buf中uid,gid,mode复制到共享内存的shmid_ds结构中
IPC_RMID:删除该共享内存
buf:共享内存管理结构体;
mmap
使用
int fd=open("a.txt",O_CREAT|O_REWR|P_TRUNC,07777);
int* addr=mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd,0);
//对addr操作完事
munmap(addr,len);//取消映射
把进程映射到同一个普通文件来实现共享内存,用读写内存 的方式来操作普通文件
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset);
start: 是映射的起始地址,一般设为NULL,让系统自动分配;
length: 申请字节数,从映射文件的offset个字节开始算起;
prot: 指定共享内存的访问权限 PROT_READ|PROT_WRITE|PROT_EXEC(可执行), PROT_NONE(不可访问)
flags: MAP_SHARED或者MAP_PRIVATE二者必须选一个;
当fd设为-1时,flags设为MAP_ANON表示匿名映射,不用创建和打开文件了
fd:文件描述符 来源于open函数
返回值:成功时返回映射到进程空间的地址;失败时返回-1
munmap
解除映射关系
int munmap(void *addr,size_t len);
addr是调用mmap()返回的地址;
len是映射区的大小;
返回值:成功时返回0;失败时返回-1
msync
将共享内存区的内容复制到磁盘上的文件上
int msync(void *addr,size_t len,int flags);
addr是调用mmap()返回的地址;
len是映射区的大小;
flags:
MS_ASYNC(异步) 调用会立即返回 更新还是得更新
MS_SYNC(同步) 等更新完成之后返回
MS_INVALIDATE(通知其他进程 数据已改变) 同时其他映射会失效,需重新获取最新值
返回值:成功时返回0;失败时返回-1
消息队列
发送方:不用等待接收方接收消息,可以不断发。所以这里,发送方和接收方都是可以只负责自己的那部分,无需管另一方。新消息放在队末,接受的时候可以从中间挑
struct msgbuf{
long type;
char text[MAX_TEXT];
}msg1,msg2;//消息结构体
msg1.type=123,msg2.type=456;
int msgID=msgget(KEY,IPC_CREAT|0666);//KEY键值比如111
msgsnd(msgID,&msg1,MAX_TEXT,0);//0表示满时阻塞,IPC_NOWAIT表示满时返回
msgrcv(msgID,&msg2,MAX_TEXT,123,0);//123是消息类型,0表示消息队列为空时阻塞,IPC_NOWAIT表示空时errno=ENOMSG//接收时wzb用来BUFSIZ表示系统默认缓冲区大小
msgctl(msgID,IPC_RMID,NULL);//删除
#include<sys/msg.h>
msgget
创建/获取消息队列
int msgget(key_t key,int msgflg);
key:键值同前,IPC_PRIVATE同前,也可以用ftok()函数来获取唯一的键值
msgflg: IPC_CREAT和IPC_EXCL同前
返回值:成功时返回消息队列标识符;失败时返回-1
msgsnd
int msgsn(int msgid, struct msgbuf *msgp, int msgsz,int msgflg);
msgid:消息队列ID标识;
msgp:指向消息缓冲区的指针;
struct msgbuf{//消息结构体
long type;
char text[MAX_TEXT];
};
msgsz:消息文本的大小=sizeof(struct msgbuf)-sizeof(long)=MAX_TEXT,不包括type大小;
msgflg:0或IPC_NOWAIT,0阻塞;IPC_NOWAIT时,不写入直接返回;
返回值:成功时返回0;失败时返回-1
msgrcv
接收消息
int msgrcv(int msgid, struct msgbuf* msgp, int msgsz,long mytype, int msgflg);
msgid:消息队列ID标识;
msgp:存储接收到的消息的结构体指针
msgsz:不包括mtype的长度
mtype:消息类型 如果为0,则读驻留在队列汇总时间最长的消息
msgflg:与上类似,0会一直阻塞直到有消息可读,IPC_NOWAIT表示如果没有消息,则立刻返回-1,并将errno设置为ENOMSG
返回值:成功时返回0;失败时返回-1
msgctl
int magctl(int msgid, int cmd,struct msgqid_ds *buf);
msgid:消息队列ID标识;
cmd:
IPC_STAT 获取消息队列的详细信息,包括权限、各种时间、id等;
IPC_RMID 删除 见前;
IPC_SET 设置消息队列信息;
buf:消息队列状态得结构指针;
返回值:成功时返回0;失败时返回-1;
msgctl(msgid, IPC_SET, &buf);
//修改消息队列权限struct msgqid_ds buf;buf.xx=xx;
msgctl(msgID,IPC_RMID,NULL);//删除消息队列
8 多线程编程
线程同步和互斥小结
信号量:资源的同步和互斥访问
线程互斥:程序只需定义一个信号量;如A执行完a后,B执行b,保证操作原子性;
线程同步:程序定义多个信号量;如A执行a、B执行b、A执行a等轮流执行,2个信号量;
互斥量:共享资源的互斥访问;
条件变量:和互斥量结合,等待某种事情发生;
互斥锁的缺点是只有两种状态:锁定和非锁定。当两个线程操作同一临界区时,只通过互斥锁保护,若A线程已经加锁,B线程再加锁时候会被阻塞,直到A释放锁,B再获得锁运行。但是在线程B被CPU调度的时候必须不停的主动获得锁、检查、释放锁、再获得锁、再检查、再释放,一直到满足运行的条件的时候才可以(而此过程中其他线程一直在等待该线程的结束),而且互斥锁在加锁操作时涉及上下文的切换,所以这种方式是比较消耗系统的资源的。
线程函数
#include<pthread.h>
pthread_create
int pthread_create(pthread_t *tid,const pthread_attr_t *pth_attr,void *(*start_rtn)(void*),void *arg);
tid:创建的线程id指针
第二个参数(人话):表示创建线程的属性,只见过是NULL的
第三个参数(人话):线程要执行的代码 一个函数啦
arg:要传递给线程的参数
返回值:成功为0;失败返回失败原因代码 不要用errno,因为可能被破坏
void* fun(void *arg);
int main(){
pthread_t pt;
pthread_create(&pt,NULL,fun,(void *)NULL);
//线程pt属性NULL,要执行函数fun,参数是NULL
return 0;
}
pthread_exit
exit会导致此线程所在的进程全体gg 而pthread_exit只会让自己gg
void pthread_exit(void *ret_val);
ret_val 线程退出时返回的指针 需要将这个东西返回给pthread_join
void *tmp(void *arg){
//xxxx
pthread_exit(NULL);
}
int main(){
pthread_t pid1;
pthread_create(&pid1,NULL,*tmp,NULL);
pthread_join(pid1,NULL);//NULL搞到这了
return 0;
}
pthread_join
等待线程结束
int pthread_join(pthread_t thread,void **thread_return);
thread:等待线程ID
thread_return:为指向thread线程返回值的指针
返回值:成功0;失败返回错误编码
pthread_join妙用:让一个线程等另一个线程结束再操作
void *t1(void *arg){}
void *t2(void *arg){
pthread_join((pthread_t)arg,NULL);//等t1
}
int main(){
pthread_t pid1,pid2;
pthread_create(&pid1,NULL,t1,NULL);
pthread_create(&pid2,NULL,t2,(void *)pid1);//竟然在这把线程传进去了 牛!
pthread_join(pid2,NULL);//等t2
return 0;
}
pthread_cleanup_push
pthread_cleanup_pop
防止因线程终止未释放资源造成资源浪费,这两个函数一定一定要配套使用!执行顺序相当于栈
void pthread_cleanup_push(void (*rtn)(void *),void *arg);
rtn指线程要结束的时候需要执行的函数
arg指传递给该函数的参数
void pthread_cleanup_pop(int execute);//书上讲的不清楚
execute pop的数量写不对(多/少)是过不了编译的qwq
pthread_exit是不受execute参数的影响的,无论execute是多少都会执行pop函数
exit也不受execute参数影响 无论execute是多少都不会执行pop函数
只有正常退出会受影响 execute非0时会执行pop函数 为0时不执行
pthread_self
获取自身线程号
pthread_t pthread_self();
//输出的时候直接 %lu就可以了 强制转换都不用qwq
互斥量
让临界区代码只能同时有一个线程进入,防止多线程对共享资源的访问导致结果不一致。
使用
pthread_mutex mutex;//互斥量
void * fun(void * arg){
pthread_mutex_lock(&mutex);//加锁
//一段访问资源的代码
pthread_mutex_unlock(&mutex);//解锁
}
int main(){
pthread_mutex_init(&mutex,NULL);//互斥量初始化
pthread_t p1,p2;//两个线程
pthread_create(&p1,NULL,fun,NULL);//创建线程1
pthread_create(&p2,NULL,fun,NULL);//创建线程2
pthread_join(p1,NULL);//等待线程1结束
pthread_join(p2,NULL);//等待线程2结束
pthread_mutex_destory(mutex);//销毁互斥量
}
pthread_mutex_init
锁声明和初始化
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutexattr);
mutex:被初始化的互斥量
mutexattr:指定互斥量的属性,NULL为默认 一般为NULL,NULL相当于PTHREAD_MUTEX_TIMED_NP缺省属性
返回值:成功0;失败返回错误编码
pthread_mutex_init(&mutex,NULL);
pthread_mutex_lock
互斥量加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
加锁 直至unlock 如果遇到这段代码的时候,这个锁正在被别的线程使用,则当前线程需要等待别的线程释放,在此之前一直被阻塞。
返回值:成功0;失败返回错误编码
int pthread_mutex_trylock(pthread_mutex_t *mutex);
互斥量判断是否加锁,当锁被占用时返回EBUSY
pthread_mutex_unlock
解锁互斥量
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功0;失败返回错误编码
pthread_mutex_destroy
注销互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值:成功0;失败返回错误编码
信号量
线程A执行操作a,线程B执行操作b;
线程互斥:程序只需定义一个信号量;如A执行完a后,B执行b,保证操作原子性;
线程同步:程序定义多个信号量;如A执行a、B执行b、A执行a等轮流执行,2个信号量;
#include<semaphore.h>
//---------------互斥-------------------
static sem_t sem;//信号量
void *fun1(void* arg){//线程A的操作a
sem_wait(&sem);
//操作
sem_post(&sem);
}
//线程B的操作b
int main(){
pthread_t pA,pB;
sem_init(&sem,0,1);//0表示线程共享,1表示信号量值初始化为1
//pA、pB创建、结束
sem_destroy(&sem);
}
//-------------同步---------------------
static sem_t sem1,sem2;//信号量
void *fun1(void* arg){//线程A的操作a
sem_wait(&sem1);//申请资源1
//操作a
sem_post(&sem2);//释放资源2
}
void *fun2(void* arg){//线程B的操作b
sem_wait(&sem2);//申请2
sem_post(&sem1);//释放1
}
void main(){
sem_init(&sem1,0,1);//资源1初始1个
sem_init(&sem2,0,0);//资源2初始0个
//code....
}
sem_init
初始化一个sem
信号量
int sem_init(sem_t *sem,int pshared,unsigned int value);
sem是要初始化的信号量
pshared ≠0: 进程共享,信号量必须存在于共享内存中;
0: 线程共享,如全局变量,或者堆上动态分配的变量;
value 信号量的初始值
返回值:成功返回0;失败返回-1 并返回错误代码errno
sem_wait P操作
int sem_wait(sem_t *sem);//sem为要操作的信号量
返回值:成功返回0;失败返回-1 并返回错误代码errno
信号量的value为0时,阻塞,不为0时,value--;
sem_post V操作
int sem_post(sem_t *sem);
返回值:成功返回0;失败返回-1 并返回错误代码errno
无阻塞时,value++;
sem_destroy
int sem_destroy(sem_t *sem);
sem:要销毁的信号量
返回值:成功返回0;失败返回-1 并返回错误代码errno
-
sem_getValue
获取信号量当前值 -
sem_trywait
信号量值<0,sem_wait阻塞进程,value–;sem_trywait立即返回,value–;
条件变量
使用
pthread_mutex_t mutex;
pthread_cond_t cond;
void *t1(void *arg){
for(aaa){
pthread_mutex_lock(&mutex);//互斥量上锁
if(balabala) pthread_cond_signal(&cond);//如果满足balabala,就发信号唤醒
else xxx;
pthread_mutex_unlock(&mutex);//互斥量解锁
sleep(1);
}
}
void *t2(void *arg){
for(bbb){
pthread_mutex_lock(&mutex);//互斥量上锁
if(!labalaba) pthread_cond_wait(&cond,&mutex);
//wait,解锁互斥量,cond阻塞t2,等待条件满足
//t1互斥量上锁,一直在循环,直到满足balabala,然后发信号唤醒t2;
//t2被唤醒时,t1处还互斥量还未解锁,t2仍在阻塞,待互斥锁释放后,t2中wait执行第三步,判断资源可用性并重新上锁
else yyy;
pthread_mutex_unlock(&mutex);//互斥量解锁
sleep(1);
}
}
int main(){
pthread_t t_a,t_b;
pthread_mutex_init(&mutex,NULL);//互斥锁初始化
pthread_cond_init(&cond,NULL);//条件变量初始化
pthread_create(&t_a,NULL,t1,NULL);//创建线程
pthread_create(&t_b,NULL,t2,NULL);//创建线程
pthread_join(t_a,NULL);//等待线程结束
pthread_join(t_b,NULL);//等待线程结束
pthread_mutex_destroy(&mutex);//销毁互斥量
pthread_cond_destroy(&cond);//销毁条件变量
return 0;
}
pthread_cond_init
静态初始化:pthread_cond_t my_condition = PTHREAD_COND_INITALIZER;
动态初始化,不能多个线程同时初始化一个条件变量,当需要重新初始化或者释放一个条件变量时,需保证此条件变量未被使用过;
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
cond:条件变量
attr:根据这个初始化 一般设为NULL 则属性取默认值
返回值:成功0;失败-1 设置errno
pthread_cond_wait
条件变量等待系统调用;系统调用执行顺序:
互斥量mutex解锁,当前线程阻塞在cond条件变量上;
等待其他进程的pthread_cond_signal
或pthread_cond_broad_cast
信号,判断资源可用性,对mutex上锁;
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
cond:条件变量
mutex:互斥量
返回值:成功0;失败-1 设置errno
pthread_cond_signal
如果没有人在wait此函数无作用,反之,会唤起1个线程,顺序取决于调度策略。
int pthread_cond_signal(pthread_cond_t *cond);
返回值:成功0;失败-1 设置errno
pthread_cond_destroy
释放条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
返回值:成功0;失败-1 设置errno
杂项
Linux
groupadd myproject # 创建组
useradd A -G myproject # 创建用户A并加入组
GDB调试:使用 info locals 查看所有局部变量值
在⽣成可执⾏⽂件时,动态库不会被链接进去。所以⽣成的可执⾏⽂件⼤⼩⽐静态库的要⼩。但是此时其运⾏需要依赖动态库。
执行命令将当前目录添加到库搜索路径中export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:;
POSIX 表示可移植操作系统接口,定义了操作系统应该为应用程序提供的接口标准
英文 | 含义 |
---|---|
sem、semaphore | 信号量 |
mutex | 互斥量、互斥锁 |
unzip -n ./sp-labs/lab01/lab01.zip # 解压
find . -name "tmp*" -exec mv {} ../Download/tmp \; # 移动
find pathname -name "name" -exec command {} \;
find命令对匹配的文件执行command的shell命令
head -n 10 a1009.cpp # 查看前十行
tail -n 10 a1009.cpp # 后十行
tar cvf tmp.tar.gz * # 打包
du -sh # 列出当前目录下的文件大小
find -type d -empty # 用命令找出空目录
- vi 编辑器有哪几种模式?简述这几种模式间如何互相切换?
模式:
- 命令模式
- 底行模式 :set nu 设置行号 :set nonu 取消行号
- 插入模式
如何切换:用 vi 打开文件后默认进入命令模式。在命令模式输入操作符后进入插入模式,输入esc
退出插入模式,返回命令模式;在命令模式下输入:
或/
进入底行模式,输入esc
退出底行模式,返回命令模式。
nG 光标移动到文档第 n 行首字符
$ 光标移动到当前行尾字符
yy 复制当前行
ynG 从第 n 行开始复制直到当前行(包括)
p 将复制的内容粘贴到当前字符的下一个位置
x 删除当前字符
dd 删除当前行