Linux系统编程 复习笔记

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

  1. 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。
  2. 移动文件或重命名文件,只是改变文件名,不影响inode号码。(这就是为什么查询inode的时候不会显示文件名。)
  3. 打开一个文件以后,系统就以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表示如果子进程进入暂停执行清空则马上返回,但结束状态不理会;
0wait()作用一样,阻塞父进程;

#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不能被忽略

    信号说明默认处理
    SIGINTCtrl+C 即^C进程终止
    SIGQUITCtrl+\即^\进程终止+core文件
    SIGALRMalarm或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_signalpthread_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 编辑器有哪几种模式?简述这几种模式间如何互相切换?

模式:

  1. 命令模式
  2. 底行模式 :set nu 设置行号 :set nonu 取消行号
  3. 插入模式

如何切换:用 vi 打开文件后默认进入命令模式。在命令模式输入操作符后进入插入模式,输入esc退出插入模式,返回命令模式;在命令模式下输入:/进入底行模式,输入esc退出底行模式,返回命令模式。

nG 光标移动到文档第 n 行首字符

$ 光标移动到当前行尾字符

yy 复制当前行

ynG 从第 n 行开始复制直到当前行(包括)

p 将复制的内容粘贴到当前字符的下一个位置

x 删除当前字符

dd 删除当前行

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值