Linux的进程间通信-文件和文件锁

Linux的进程间通信-文件和文件锁

版权声明:

本文章内容在非商业使用前提下可无需授权任意转载、发布。

转载、发布请务必注明作者和其微博、微信公众号地址,以便读者询问问题和甄误反馈,共同进步。

微博ID:orroz

微信公众号:Linux系统技术

前言

使用文件进行进程间通信应该是最先学会的一种IPC方式。任何编程语言中,文件IO都是很重要的知识,所以使用文件进行进程间通信就成了很自然被学会的一种手段。考虑到系统对文件本身存在缓存机制,使用文件进行IPC的效率在某些多读少写的情况下并不低下。但是大家似乎经常忘记IPC的机制可以包括“文件”这一选项。

我们首先引入文件进行IPC,试图先使用文件进行通信引入一个竞争条件的概念,然后使用文件锁解决这个问题,从而先从文件的角度来管中窥豹的看一下后续相关IPC机制的总体要解决的问题。阅读本文可以帮你解决以下问题:

  1. 什么是竞争条件(racing)?。
  2. flock和lockf有什么区别?
  3. flockfile函数和flock与lockf有什么区别?
  4. 如何使用命令查看文件锁?

如果您觉得本文有用,请刷二维码随意捐助。

收钱

竞争条件(racing)

我们的第一个例子是多个进程写文件的例子,虽然还没做到通信,但是这比较方便的说明一个通信时经常出现的情况:竞争条件。假设我们要并发100个进程,这些进程约定好一个文件,这个文件初始值内容写0,每一个进程都要打开这个文件读出当前的数字,加一之后将结果写回去。在理想状态下,这个文件最后写的数字应该是100,因为有100个进程打开、读数、加1、写回,自然是有多少个进程最后文件中的数字结果就应该是多少。但是实际上并非如此,可以看一下这个例子:

[zorro@zorrozou-pc0 process]$ cat racing.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/file.h>
#include <wait.h>

#define COUNT 100
#define NUM 64
#define FILEPATH "/tmp/count"

int do_child(const char *path)
{
    /* 这个函数是每个子进程要做的事情
    每个子进程都会按照这个步骤进行操作:
    1. 打开FILEPATH路径的文件
    2. 读出文件中的当前数字
    3. 将字符串转成整数
    4. 整数自增加1
    5. 将证书转成字符串
    6. lseek调整文件当前的偏移量到文件头
    7. 将字符串写会文件
    当多个进程同时执行这个过程的时候,就会出现racing:竞争条件,
    多个进程可能同时从文件独到同一个数字,并且分别对同一个数字加1并写回,
    导致多次写回的结果并不是我们最终想要的累积结果。 */
    int fd;
    int ret, count;
    char buf[NUM];
    fd = open(path, O_RDWR);
    if (fd < 0) {
        perror("open()");
        exit(1);
    }
    /*  */
    ret = read(fd, buf, NUM);
    if (ret < 0) {
        perror("read()");
        exit(1);
    }
    buf[ret] = '\0';
    count = atoi(buf);
    ++count;
    sprintf(buf, "%d", count);
    lseek(fd, 0, SEEK_SET);
    ret = write(fd, buf, strlen(buf));
    /*  */
    close(fd);
    exit(0);
}

int main()
{
    pid_t pid;
    int count;

    for (count=0;count<COUNT;count++) {
        pid = fork();
        if (pid < 0) {
            perror("fork()");
            exit(1);
        }

        if (pid == 0) {
            do_child(FILEPATH);
        }
    }

    for (count=0;count<COUNT;count++) {
        wait(NULL);
    }
}

这个程序做后执行的效果如下:

[zorro@zorrozou-pc0 process]$ make racing
cc     racing.c   -o racing
[zorro@zorrozou-pc0 process]$ echo 0 > /tmp/count
[zorro@zorrozou-pc0 process]$ ./racing 
[zorro@zorrozou-pc0 process]$ cat /tmp/count 
71[zorro@zorrozou-pc0 process]$ 
[zorro@zorrozou-pc0 process]$ echo 0 > /tmp/count
[zorro@zorrozou-pc0 process]$ ./racing 
[zorro@zorrozou-pc0 process]$ cat /tmp/count 
61[zorro@zorrozou-pc0 process]$ 
[zorro@zorrozou-pc0 process]$ echo 0 > /tmp/count
[zorro@zorrozou-pc0 process]$ ./racing 
[zorro@zorrozou-pc0 process]$ cat /tmp/count 
64[zorro@zorrozou-pc0 process]$ 

我们执行了三次这个程序,每次结果都不太一样,第一次是71,第二次是61,第三次是64,全都没有得到预期结果,这就是竞争条件(racing)引入的问题。仔细分析这个进程我们可以发现这个竞争条件是如何发生的:

最开始文件内容是0,假设此时同时打开了3个进程,那么他们分别读文件的时候,这个过程是可能并发的,于是每个进程读到的数组可能都是0,因为他们都在别的进程没写入1之前就开始读了文件。于是三个进程都是给0加1,然后写了个1回到文件。其他进程以此类推,每次100个进程的执行顺序可能不一样,于是结果是每次得到的值都可能不太一样,但是一定都少于产生的实际进程个数。于是我们把这种多个执行过程(如进程或线程)中访问同一个共享资源,而这些共享资源又有无法被多个执行过程存取的的程序片段,叫做临界区代码。

那么该如何解决这个racing的问题呢?对于这个例子来说,可以用文件锁的方式解决这个问题。就是说,对临界区代码进行加锁,来解决竞争条件的问题。哪段是临界区代码?在这个例子中,两端/ /之间的部分就是临界区代码。一个正确的例子是:

...
    ret = flock(fd, LOCK_EX);
    if (ret == -1) {
        perror("flock()");
        exit(1);
    }

    ret = read(fd, buf, NUM);
    if (ret < 0) {
        perror("read()");
        exit(1);
    }
    buf[ret] = '\0';
    count = atoi(buf);
    ++count;
    sprintf(buf, "%d", count);
    lseek(fd, 0, SEEK_SET);
    ret = write(fd, buf, strlen(buf));
    ret = flock(fd, LOCK_UN);
    if (ret == -1) {
        perror("flock()");
        exit(1);
    }
...

我们将临界区部分代码前后都使用了flock的互斥锁,防止了临界区的racing。这个例子虽然并没有真正达到让多个进程通过文件进行通信,解决某种协同工作问题的目的,但是足以表现出进程间通信机制的一些问题了。当涉及到数据在多个进程间进行共享的时候,仅仅只实现数据通信或共享机制本身是不够的,还需要实现相关的同步或异步机制来控制多个进程,达到保护临界区或其他让进程可以处理同步或异步事件的能力。我们可以认为文件锁是可以实现这样一种多进程的协调同步能力的机制,而除了文件锁以外,还有其他机制可以达到相同或者不同的功能,我们会在下文中继续详细解释。

再次,我们并不对flock这个方法本身进行功能性讲解。这种功能性讲解大家可以很轻易的在网上或者通过别的书籍得到相关内容。本文更加偏重的是Linux环境提供了多少种文件锁以及他们的区别是什么?

flock和lockf

从底层的实现来说,Linux的文件锁主要有两种:flock和lockf。需要额外对lockf说明的是,它只是fcntl系统调用的一个封装。从使用角度讲,lockf或fcntl实现了更细粒度文件锁,即:记录锁。我们可以使用lockf或fcntl对文件的部分字节上锁,而flock只能对整个文件加锁。这两种文件锁是从历史上不同的标准中起源的,flock来自BSD而lockf来自POSIX,所以lockf或fcntl实现的锁在类型上又叫做POSIX锁。

除了这个区别外,fcntl系统调用还可以支持强制锁(Mandatory locking)。强制锁的概念是传统UNIX为了强制应用程序遵守锁规则而引入的一个概念,与之对应的概念就是建议锁(Advisory locking)。我们日常使用的基本都是建议锁,它并不强制生效。这里的不强制生效的意思是,如果某一个进程对一个文件持有一把锁之后,其他进程仍然可以直接对文件进行各种操作的,比如open、read、write。只有当多个进程在操作文件前都去检查和对相关锁进行锁操作的时候,文件锁的规则才会生效。这就是一般建议锁的行为。而强制性锁试图实现一套内核级的锁操作。当有进程对某个文件上锁之后,其他进程即使不在操作文件之前检查锁,也会在open、read或write等文件操作时发生错误。内核将对有锁的文件在任何情况下的锁规则都生效,这就是强制锁的行为。由此可以理解,如果内核想要支持强制锁,将需要在内核实现open、read、write等系统调用内部进行支持。

从应用的角度来说,Linux内核虽然号称具备了强制锁的能力,但其对强制性锁的实现是不可靠的,建议大家还是不要在Linux下使用强制锁。事实上,在我目前手头正在使用的Linux环境上,一个系统在mount -o mand分区的时候报错(archlinux kernel 4.5),而另一个系统虽然可以以强制锁方式mount上分区,但是功能实现却不完整,主要表现在只有在加锁后产生的子进程中open才会报错,如果直接write是没问题的,而且其他进程无论open还是read、write都没问题(Centos 7 kernel 3.10)。鉴于此,我们就不在此介绍如何在Linux环境中打开所谓的强制锁支持了。我们只需知道,在Linux环境下的应用程序,flock和lockf在是锁类型方面没有本质差别,他们都是建议锁,而非强制锁。

flock和lockf另外一个差别是它们实现锁的方式不同。这在应用的时候表现在flock的语义是针对文件的锁,而lockf是针对文件描述符(fd)的锁。我们用一个例子来观察这个区别:

[zorro@zorrozou-pc0 locktest]$ cat flock.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <wait.h>

#define PATH "/tmp/lock"

int main()
{
    int fd;
    pid_t pid;

    fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
    if (fd < 0) {
        perror("open()");
        exit(1);
    }

    if (flock(fd, LOCK_EX) < 0) {
        perror("flock()");
        exit(1);
    }
    printf("%d: locked!\n", getpid());

    pid = fork();
    if (pid < 0) {
        perror("fork()");
        exit(1);
    }

    if (pid == 0) {
/*
        fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
        if (fd < 0) {
                perror("open()");
                exit(1);
        }
*/
        if (flock(fd, LOCK_EX) < 0) {
            perror("flock()");
            exit(1);
        }
        printf("%d: locked!\n", getpid());
        exit(0);
    }
    wait(NULL);
    unlink(PATH);
    exit(0);
}

上面代码是一个flock的例子,其作用也很简单:

  1. 打开/tmp/lock文件。
  2. 使用flock对其加互斥锁。
  3. 打印“PID:locked!”表示加锁成功。
  4. 打开一个子进程,在子进程中使用flock对同一个文件加互斥锁。
  5. 子进程打印“PID:locked!”表示加锁成功。如果没加锁成功子进程会推出,不显示相关内容。
  6. 父进程回收子进程并推出。

这个程序直接编译执行的结果是:

[zorro@zorrozou-pc0 locktest]$ ./flock 
23279: locked!
23280: locked!

父子进程都加锁成功了。这个结果似乎并不符合我们对文件加锁的本意。按照我们对互斥锁的理解,子进程对父进程已经加锁过的文件应该加锁失败才对。我们可以稍微修改一下上面程序让它达到预期效果,将子进程代码段中的注释取消掉重新编译即可:

...
/*
        fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
        if (fd < 0) {
                perror("open()");
                exit(1);
        }
*/
...

将这段代码上下的/ /删除重新编译。之后执行的效果如下:

[zorro@zorrozou-pc0 locktest]$ make flock
cc     flock.c   -o flock
[zorro@zorrozou-pc0 locktest]$ ./flock 
23437: locked!

此时子进程flock的时候会阻塞,让进程的执行一直停在这。这才是我们使用文件锁之后预期该有的效果。而相同的程序使用lockf却不会这样。这个原因在于flock和lockf的语义是不同的。使用lockf或fcntl的锁,在实现上关联到文件结构体,这样的实现导致锁不会在fork之后被子进程继承。而flock在实现上关联到的是文件描述符,这就意味着如果我们在进程中复制了一个文件描述符,那么使用flock对这个描述符加的锁也会在新复制出的描述符中继续引用。在进程fork的时候,新产生的子进程的描述符也是从父进程继承(复制)来的。在子进程刚开始执行的时候,父子进程的描述符关系实际上跟在一个进程中使用dup复制文件描述符的状态一样(参见《UNIX环境高级编程》8.3节的文件共享部分)。这就可能造成上述例子的情况,通过fork产生的多个进程,因为子进程的文件描述符是复制的父进程的文件描述符,所以导致父子进程同时持有对同一个文件的互斥锁,导致第一个例子中的子进程仍然可以加锁成功。这个文件共享的现象在子进程使用open重新打开文件之后就不再存在了,所以重新对同一文件open之后,子进程再使用flock进行加锁的时候会阻塞。另外要注意:除非文件描述符被标记了close-on-exec标记,flock锁和lockf锁都可以穿越exec,在当前进程变成另一个执行镜像之后仍然保留。

上面的例子中只演示了fork所产生的文件共享对flock互斥锁的影响,同样原因也会导致dup或dup2所产生的文件描述符对flock在一个进程内产生相同的影响。dup造成的锁问题一般只有在多线程情况下才会产生影响,所以应该避免在多线程场景下使用flock对文件加锁,而lockf/fcntl则没有这个问题。


如果您觉得本文有用,请刷二维码随意捐助。

收钱


为了对比flock的行为,我们在此列出使用lockf的相同例子,来演示一下它们的不同:

[zorro@zorrozou-pc0 locktest]$ cat lockf.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <wait.h>

#define PATH "/tmp/lock"

int main()
{
    int fd;
    pid_t pid;

    fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
    if (fd < 0) {
        perror("open()");
        exit(1);
    }

    if (lockf(fd, F_LOCK, 0) < 0) {
        perror("lockf()");
        exit(1);
    }
    printf("%d: locked!\n", getpid());

    pid = fork();
    if (pid < 0) {
        perror("fork()");
        exit(1);
    }

    if (pid == 0) {
/*
        fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
        if (fd < 0) {
            perror("open()");
            exit(1);
        }
*/
        if (lockf(fd, F_LOCK, 0) < 0) {
            perror("lockf()");
            exit(1);
        }
        printf("%d: locked!\n", getpid());
        exit(0);
    }
    wait(NULL);
    unlink(PATH);   
    exit(0);
}

编译执行的结果是:

[zorro@zorrozou-pc0 locktest]$ ./lockf 
27262: locked!

在子进程不用open重新打开文件的情况下,进程执行仍然被阻塞在子进程lockf加锁的操作上。关于fcntl对文件实现记录锁的详细内容,大家可以参考《UNIX环境高级编程》中关于记录锁的14.3章节。

标准IO库文件锁

C语言的标准IO库中还提供了一套文件锁,它们的原型如下:

#include <stdio.h>

void flockfile(FILE *filehandle);
int ftrylockfile(FILE *filehandle);
void funlockfile(FILE *filehandle);

从实现角度来说,stdio库中实现的文件锁与flock或lockf有本质区别。作为一种标准库,其实现的锁必然要考虑跨平台的特性,所以其结构都是在用户态的FILE结构体中实现的,而非内核中的数据结构来实现。这直接导致的结果就是,标准IO的锁在多进程环境中使用是有问题的。进程在fork的时候会复制一整套父进程的地址空间,这将导致子进程中的FILE结构与父进程完全一致。就是说,父进程如果加锁了,子进程也将持有这把锁,父进程没加锁,子进程由于地址空间跟父进程是独立的,所以也无法通过FILE结构体检查别的进程的用户态空间是否家了标准IO库提供的文件锁。这种限制导致这套文件锁只能处理一个进程中的多个线程之间共享的FILE 的进行文件操作。就是说,多个线程必须同时操作一个用fopen打开的FILE 变量,如果内部自己使用fopen重新打开文件,那么返回的FILE *地址不同,也起不到线程的互斥作用。

我们分别将两种使用线程的状态的例子分别列出来,第一种是线程之间共享同一个FILE *的情况,这种情况互斥是没问题的:

[zorro@zorro-pc locktest]$ cat racing_pthread_sharefp.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/file.h>
#include <wait.h>
#include <pthread.h>

#define COUNT 100
#define NUM 64
#define FILEPATH "/tmp/count"
static FILE *filep;

void *do_child(void *p)
{
    int fd;
    int ret, count;
    char buf[NUM];

    flockfile(filep);

    if (fseek(filep, 0L, SEEK_SET) == -1) {
        perror("fseek()");
    }
    ret = fread(buf, NUM, 1, filep);

    count = atoi(buf);
    ++count;
    sprintf(buf, "%d", count);
    if (fseek(filep, 0L, SEEK_SET) == -1) {
        perror("fseek()");
    }
    ret = fwrite(buf, strlen(buf), 1, filep);

    funlockfile(filep);

    return NULL;
}

int main()
{
    pthread_t tid[COUNT];
    int count;

    filep = fopen(FILEPATH, "r+");
    if (filep == NULL) {
        perror("fopen()");
        exit(1);
    }

    for (count=0;count<COUNT;count++) {
        if (pthread_create(tid+count, NULL, do_child, NULL) != 0) {
            perror("pthread_create()");
            exit(1);
        }
    }

    for (count=0;count<COUNT;count++) {
        if (pthread_join(tid[count], NULL) != 0) {
            perror("pthread_join()");
            exit(1);
        }
    }

    fclose(filep);

    exit(0);
}

另一种情况是每个线程都fopen重新打开一个描述符,此时线程是不能互斥的:

[zorro@zorro-pc locktest]$ cat racing_pthread_threadfp.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/file.h>
#include <wait.h>
#include <pthread.h>

#define COUNT 100
#define NUM 64
#define FILEPATH "/tmp/count"

void *do_child(void *p)
{
    int fd;
    int ret, count;
    char buf[NUM];
    FILE *filep;

    filep = fopen(FILEPATH, "r+");
    if (filep == NULL) {
        perror("fopen()");
        exit(1);
    }

    flockfile(filep);

    if (fseek(filep, 0L, SEEK_SET) == -1) {
        perror("fseek()");
    }
    ret = fread(buf, NUM, 1, filep);

    count = atoi(buf);
    ++count;
    sprintf(buf, "%d", count);
    if (fseek(filep, 0L, SEEK_SET) == -1) {
        perror("fseek()");
    }
    ret = fwrite(buf, strlen(buf), 1, filep);

    funlockfile(filep);

    fclose(filep);
    return NULL;
}

int main()
{
    pthread_t tid[COUNT];
    int count;


    for (count=0;count<COUNT;count++) {
        if (pthread_create(tid+count, NULL, do_child, NULL) != 0) {
            perror("pthread_create()");
            exit(1);
        }
    }

    for (count=0;count<COUNT;count++) {
        if (pthread_join(tid[count], NULL) != 0) {
            perror("pthread_join()");
            exit(1);
        }
    }


    exit(0);
}

以上程序大家可以自行编译执行看看效果。

文件锁相关命令

系统为我们提供了flock命令,可以方便我们在命令行和shell脚本中使用文件锁。需要注意的是,flock命令是使用flock系统调用实现的,所以在使用这个命令的时候请注意进程关系对文件锁的影响。flock命令的使用方法和在脚本编程中的使用可以参见我的另一篇文章《shell编程之常用技巧》中的bash并发编程和flock这部分内容,在此不在赘述。

我们还可以使用lslocks命令来查看当前系统中的文件锁使用情况。一个常见的现实如下:

[root@zorrozou-pc0 ~]# lslocks 
COMMAND           PID   TYPE  SIZE MODE  M      START        END PATH
firefox         16280  POSIX    0B WRITE 0          0          0 /home/zorro/.mozilla/firefox/bk2bfsto.default/.parentlock
dmeventd          344  POSIX    4B WRITE 0          0          0 /run/dmeventd.pid
gnome-shell       472  FLOCK    0B WRITE 0          0          0 /run/user/120/wayland-0.lock
flock           27452  FLOCK    0B WRITE 0          0          0 /tmp/lock
lvmetad           248  POSIX    4B WRITE 0          0          0 /run/lvmetad.pid

这其中,TYPE主要表示锁类型,就是上文我们描述的flock和lockf。lockf和fcntl实现的锁事POSIX类型。M表示是否事强制锁,0表示不是。如果是记录锁的话,START和END表示锁住文件的记录位置,0表示目前锁住的是整个文件。MODE主要用来表示锁的权限,实际上这也说明了锁的共享属性。在系统底层,互斥锁表示为WRITE,而共享锁表示为READ,如果这段出现*则表示有其他进程正在等待这个锁。其余参数可以参考man lslocks。

最后

本文通过文件盒文件锁的例子,引出了竞争条件这样在进程间通信中需要解决的问题。并深入探讨了系统编程中常用的文件锁的实现和应用特点。希望大家对进程间通信和文件锁的使用有更深入的理解。

如果您觉得本文有用,请刷二维码随意捐助。

收钱


大家好,我是Zorro!

如果你喜欢本文,欢迎在微博上搜索“orroz”关注我,地址是:http://weibo.com/orroz

大家也可以在微信上搜索:Linux系统技术 关注我的公众号。

我的所有文章都会沉淀在我的个人博客上,地址是:http://liwei.life。

欢迎使用以上各种方式一起探讨学习,共同进步。

公众号二维码:

Zorro] icon


 

Linux的进程间通信 - 管道

Linux的进程间通信 - 管道

版权声明:

本文章内容在非商业使用前提下可无需授权任意转载、发布。

转载、发布请务必注明作者和其微博、微信公众号地址,以便读者询问问题和甄误反馈,共同进步。

微博ID:orroz

微信公众号:Linux系统技术

前言

管道是UNIX环境中历史最悠久的进程间通信方式。本文主要说明在Linux环境上如何使用管道。阅读本文可以帮你解决以下问题:

  1. 什么是管道和为什么要有管道?
  2. 管道怎么分类?
  3. 管道的实现是什么样的?
  4. 管道有多大?
  5. 管道的大小是不是可以调整?如何调整?

如果觉得本文还不错,请扫码任意打赏捐助穷佐罗。

收钱

什么是管道?

管道,英文为pipe。这是一个我们在学习Linux命令行的时候就会引入的一个很重要的概念。它的发明人是道格拉斯.麦克罗伊,这位也是UNIX上早期shell的发明人。他在发明了shell之后,发现系统操作执行命令的时候,经常有需求要将一个程序的输出交给另一个程序进行处理,这种操作可以使用输入输出重定向加文件搞定,比如:

[zorro@zorro-pc pipe]$ ls  -l /etc/ > etc.txt
[zorro@zorro-pc pipe]$ wc -l etc.txt 
183 etc.txt

但是这样未免显得太麻烦了。所以,管道的概念应运而生。目前在任何一个shell中,都可以使用“|”连接两个命令,shell会将前后两个进程的输入输出用一个管道相连,以便达到进程间通信的目的:

[zorro@zorro-pc pipe]$ ls -l /etc/ | wc -l
183

对比以上两种方法,我们也可以理解为,管道本质上就是一个文件,前面的进程以写方式打开文件,后面的进程以读方式打开。这样前面写完后面读,于是就实现了通信。实际上管道的设计也是遵循UNIX的“一切皆文件”设计原则的,它本质上就是一个文件。Linux系统直接把管道实现成了一种文件系统,借助VFS给应用程序提供操作接口。

虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux上的管道就是一个操作方式为文件的内存缓冲区。

管道的分类和使用

Linux上的管道分两种类型:

  1. 匿名管道
  2. 命名管道

这两种管道也叫做有名或无名管道。匿名管道最常见的形态就是我们在shell操作中最常用的”|”。它的特点是只能在父子进程中使用,父进程在产生子进程前必须打开一个管道文件,然后fork产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。此时除了父子进程外,没人知道这个管道文件的描述符,所以通过这个管道中的信息无法传递给其他进程。这保证了传输数据的安全性,当然也降低了管道了通用性,于是系统还提供了命名管道。

我们可以使用mkfifo或mknod命令来创建一个命名管道,这跟创建一个文件没有什么区别:

[zorro@zorro-pc pipe]$ mkfifo pipe
[zorro@zorro-pc pipe]$ ls -l pipe 
prw-r--r-- 1 zorro zorro 0 Jul 14 10:44 pipe

可以看到创建出来的文件类型比较特殊,是p类型。表示这是一个管道文件。有了这个管道文件,系统中就有了对一个管道的全局名称,于是任何两个不相关的进程都可以通过这个管道文件进行通信了。比如我们现在让一个进程写这个管道文件:

[zorro@zorro-pc pipe]$ echo xxxxxxxxxxxxxx > pipe 

此时这个写操作会阻塞,因为管道另一端没有人读。这是内核对管道文件定义的默认行为。此时如果有进程读这个管道,那么这个写操作的阻塞才会解除:

[zorro@zorro-pc pipe]$ cat pipe 
xxxxxxxxxxxxxx

大家可以观察到,当我们cat完这个文件之后,另一端的echo命令也返回了。这就是命名管道。

Linux系统无论对于命名管道和匿名管道,底层都用的是同一种文件系统的操作行为,这种文件系统叫pipefs。大家可以在/etc/proc/filesystems文件中找到你的系统是不是支持这种文件系统:

[zorro@zorro-pc pipe]$ cat /proc/filesystems |grep pipefs
nodev   pipefs

观察完了如何在命令行中使用管道之后,我们再来看看如何在系统编程中使用管道。

PIPE

我们可以把匿名管道和命名管道分别叫做PIPE和FIFO。这主要因为在系统编程中,创建匿名管道的系统调用是pipe(),而创建命名管道的函数是mkfifo()。使用mknod()系统调用并指定文件类型为为S_IFIFO也可以创建一个FIFO。

使用pipe()系统调用可以创建一个匿名管道,这个系统调用的原型为:

#include <unistd.h>

int pipe(int pipefd[2]);

这个方法将会创建出两个文件描述符,可以使用pipefd这个数组来引用这两个描述符进行文件操作。pipefd[0]是读方式打开,作为管道的读描述符。pipefd[1]是写方式打开,作为管道的写描述符。从管道写端写入的数据会被内核缓存直到有人从另一端读取为止。我们来看一下如何在一个进程中使用管道,虽然这个例子并没有什么意义:

[zorro@zorro-pc pipe]$ cat pipe.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define STRING "hello world!"

int main()
{
    int pipefd[2];
    char buf[BUFSIZ];

    if (pipe(pipefd) == -1) {
        perror("pipe()");
        exit(1);
    }

    if (write(pipefd[1], STRING, strlen(STRING)) < 0) {
        perror("write()");
        exit(1);
    }

    if (read(pipefd[0], buf, BUFSIZ) < 0) {
        perror("write()");
        exit(1);
    }

    printf("%s\n", buf);

    exit(0);
}

这个程序创建了一个管道,并且对管道写了一个字符串之后从管道读取,并打印在标准输出上。用一个图来说明这个程序的状态就是这样的:

pipe1

一个进程自己给自己发送消息这当然不叫进程间通信,所以实际情况中我们不会在单个进程中使用管道。进程在pipe创建完管道之后,往往都要fork产生子进程,成为如下图表示的样子:

pipe2

如图中描述,fork产生的子进程会继承父进程对应的文件描述符。利用这个特性,父进程先pipe创建管道之后,子进程也会得到同一个管道的读写文件描述符。从而实现了父子两个进程使用一个管道可以完成半双工通信。此时,父进程可以通过fd[1]给子进程发消息,子进程通过fd[0]读。子进程也可以通过fd[1]给父进程发消息,父进程用fd[0]读。程序实例如下:

[zorro@zorro-pc pipe]$ cat pipe_parent_child.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

#define STRING "hello world!"

int main()
{
    int pipefd[2];
    pid_t pid;
    char buf[BUFSIZ];

    if (pipe(pipefd) == -1) {
        perror("pipe()");
        exit(1);
    }

    pid = fork();
    if (pid == -1) {
        perror("fork()");
        exit(1);
    }

    if (pid == 0) {
        /* this is child. */
        printf("Child pid is: %d\n", getpid());
        if (read(pipefd[0], buf, BUFSIZ) < 0) {
            perror("write()");
            exit(1);
        }

        printf("%s\n", buf);

        bzero(buf, BUFSIZ);
        snprintf(buf, BUFSIZ, "Message from child: My pid is: %d", getpid());
        if (write(pipefd[1], buf, strlen(buf)) < 0) {
            perror("write()");
            exit(1);
        }

    } else {
        /* this is parent */
        printf("Parent pid is: %d\n", getpid());

        snprintf(buf, BUFSIZ, "Message from parent: My pid is: %d", getpid());
        if (write(pipefd[1], buf, strlen(buf)) < 0) {
            perror("write()");
            exit(1);
        }

        sleep(1);

        bzero(buf, BUFSIZ);
        if (read(pipefd[0], buf, BUFSIZ) < 0) {
            perror("write()");
            exit(1);
        }

        printf("%s\n", buf);

        wait(NULL);
    }


    exit(0);
}

父进程先给子进程发一个消息,子进程接收到之后打印消息,之后再给父进程发消息,父进程再打印从子进程接收到的消息。程序执行效果:

[zorro@zorro-pc pipe]$ ./pipe_parent_child 
Parent pid is: 8309
Child pid is: 8310
Message from parent: My pid is: 8309
Message from child: My pid is: 8310

从这个程序中我们可以看到,管道实际上可以实现一个半双工通信的机制。使用同一个管道的父子进程可以分时给对方发送消息。我们也可以看到对管道读写的一些特点,即:

在管道中没有数据的情况下,对管道的读操作会阻塞,直到管道内有数据为止。当一次写的数据量不超过管道容量的时候,对管道的写操作一般不会阻塞,直接将要写的数据写入管道缓冲区即可。


如果觉得本文还不错,请扫码任意打赏捐助穷佐罗。

收钱


当然写操作也不会再所有情况下都不阻塞。这里我们要先来了解一下管道的内核实现。上文说过,管道实际上就是内核控制的一个内存缓冲区,既然是缓冲区,就有容量上限。我们把管道一次最多可以缓存的数据量大小叫做PIPESIZE。内核在处理管道数据的时候,底层也要调用类似read和write这样的方法进行数据拷贝,这种内核操作每次可以操作的数据量也是有限的,一般的操作长度为一个page,即默认为4k字节。我们把每次可以操作的数据量长度叫做PIPEBUF。POSIX标准中,对PIPEBUF有长度限制,要求其最小长度不得低于512字节。PIPEBUF的作用是,内核在处理管道的时候,如果每次读写操作的数据长度不大于PIPEBUF时,保证其操作是原子的。而PIPESIZE的影响是,大于其长度的写操作会被阻塞,直到当前管道中的数据被读取为止。

在Linux 2.6.11之前,PIPESIZE和PIPEBUF实际上是一样的。在这之后,Linux重新实现了一个管道缓存,并将它与写操作的PIPEBUF实现成了不同的概念,形成了一个默认长度为65536字节的PIPESIZE,而PIPEBUF只影响相关读写操作的原子性。从Linux 2.6.35之后,在fcntl系统调用方法中实现了F_GETPIPE_SZ和F_SETPIPE_SZ操作,来分别查看当前管道容量和设置管道容量。管道容量容量上限可以在/proc/sys/fs/pipe-max-size进行设置。

#define BUFSIZE 65536

......

ret = fcntl(pipefd[1], F_GETPIPE_SZ);
if (ret < 0) {
    perror("fcntl()");
    exit(1);
}

printf("PIPESIZE: %d\n", ret);

ret = fcntl(pipefd[1], F_SETPIPE_SZ, BUFSIZE);
if (ret < 0) {
    perror("fcntl()");
    exit(1);
}

......

PIPEBUF和PIPESIZE对管道操作的影响会因为管道描述符是否被设置为非阻塞方式而有行为变化,n为要写入的数据量时具体为:

O_NONBLOCK关闭,n <= PIPE_BUF:

n个字节的写入操作是原子操作,write系统调用可能会因为管道容量(PIPESIZE)没有足够的空间存放n字节长度而阻塞。

O_NONBLOCK打开,n <= PIPE_BUF:

如果有足够的空间存放n字节长度,write调用会立即返回成功,并且对数据进行写操作。空间不够则立即报错返回,并且errno被设置为EAGAIN。

O_NONBLOCK关闭,n > PIPE_BUF:

对n字节的写入操作不保证是原子的,就是说这次写入操作的数据可能会跟其他进程写这个管道的数据进行交叉。当管道容量长度低于要写的数据长度的时候write操作会被阻塞。

O_NONBLOCK打开,n > PIPE_BUF:

如果管道空间已满。write调用报错返回并且errno被设置为EAGAIN。如果没满,则可能会写入从1到n个字节长度,这取决于当前管道的剩余空间长度,并且这些数据可能跟别的进程的数据有交叉。

以上是在使用半双工管道的时候要注意的事情,因为在这种情况下,管道的两端都可能有多个进程进行读写处理。如果再加上线程,则事情可能变得更复杂。实际上,我们在使用管道的时候,并不推荐这样来用。管道推荐的使用方法是其单工模式:即只有两个进程通信,一个进程只写管道,另一个进程只读管道。实现为:

[zorro@zorro-pc pipe]$ cat pipe_parent_child2.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

#define STRING "hello world!"

int main()
{
    int pipefd[2];
    pid_t pid;
    char buf[BUFSIZ];

    if (pipe(pipefd) == -1) {
        perror("pipe()");
        exit(1);
    }

    pid = fork();
    if (pid == -1) {
        perror("fork()");
        exit(1);
    }

    if (pid == 0) {
        /* this is child. */
        close(pipefd[1]);

        printf("Child pid is: %d\n", getpid());
        if (read(pipefd[0], buf, BUFSIZ) < 0) {
            perror("write()");
            exit(1);
        }

        printf("%s\n", buf);

    } else {
        /* this is parent */
        close(pipefd[0]);

        printf("Parent pid is: %d\n", getpid());

        snprintf(buf, BUFSIZ, "Message from parent: My pid is: %d", getpid());
        if (write(pipefd[1], buf, strlen(buf)) < 0) {
            perror("write()");
            exit(1);
        }

        wait(NULL);
    }


    exit(0);
}

这个程序实际上比上一个要简单,父进程关闭管道的读端,只写管道。子进程关闭管道的写端,只读管道。整个管道的打开效果最后成为下图所示:

pipe3

此时两个进程就只用管道实现了一个单工通信,并且这种状态下不用考虑多个进程同时对管道写产生的数据交叉的问题,这是最经典的管道打开方式,也是我们推荐的管道使用方式。另外,作为一个程序员,即使我们了解了Linux管道的实现,我们的代码也不能依赖其特性,所以处理管道时该越界判断还是要判断,该错误检查还是要检查,这样代码才能更健壮。

FIFO

命名管道在底层的实现跟匿名管道完全一致,区别只是命名管道会有一个全局可见的文件名以供别人open打开使用。再程序中创建一个命名管道文件的方法有两种,一种是使用mkfifo函数。另一种是使用mknod系统调用,例子如下:

[zorro@zorro-pc pipe]$ cat mymkfifo.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{

    if (argc != 2) {
        fprintf(stderr, "Argument error!\n");
        exit(1);
    }

/*
    if (mkfifo(argv[1], 0600) < 0) {
        perror("mkfifo()");
        exit(1);
    }
*/
    if (mknod(argv[1], 0600|S_IFIFO, 0) < 0) {
        perror("mknod()");
        exit(1);
    }

    exit(0);
}

我们使用第一个参数作为创建的文件路径。创建完之后,其他进程就可以使用open()、read()、write()标准文件操作等方法进行使用了。其余所有的操作跟匿名管道使用类似。需要注意的是,无论命名还是匿名管道,它的文件描述都没有偏移量的概念,所以不能用lseek进行偏移量调整。

关于管道的其它议题,比如popen、pclose的使用等话题,《UNIX环境高级编程》中的相关章节已经讲的很清楚了。如果想学习补充这些知识,请参见此书。

最后

希望这些内容对大家进一步深入了解管道有帮助。如果有相关问题,可以在我的微博、微信或者博客上联系我。

如果觉得本文还不错,请扫码任意打赏捐助穷佐罗。

收钱


大家好,我是Zorro!

如果你喜欢本文,欢迎在微博上搜索“orroz”关注我,地址是:http://weibo.com/orroz

大家也可以在微信上搜索:Linux系统技术 关注我的公众号。

我的所有文章都会沉淀在我的个人博客上,地址是:http://liwei.life。

欢迎使用以上各种方式一起探讨学习,共同进步。

公众号二维码:

Zorro] icon


 

find命令详解

find命令详解

版权声明:

本文章内容在非商业使用前提下可无需授权任意转载、发布。

转载、发布请务必注明作者和其微博、微信公众号地址,以便读者询问问题和甄误反馈,共同进步。

微博ID:orroz

微信公众号:Linux系统技术

前言

find命令是我们日常工作中比较常用的Linux命令。全面的掌握这个命令可以使很多操作达到事半功倍的效果。如果对find命令有以下这些疑惑,本文都能帮你解决:

  1. find命令的格式是什么?
  2. 参数中出现+或-号是什么意思?比如find / -mtime +7与find / -mtime -7什么区别?
  3. find /etc/ -name “passwd” -exec echo {} \;和find /etc/ -name “passwd” -exec echo {} +有啥区别?
  4. -exec参数为什么要以“\;”结尾,而不是只写“;”?

捐助穷佐罗,让他过上温饱生活。

收钱

命令基础

find命令大家都比较熟悉,反倒想讲的有特色比较困难。那干脆我们怎么平淡怎么来好了。我们一般用的find命令格式很简单,一般分成三个部分:

find /etc -name "passwd"

格式如上,第一段find命令。第二段,要搜索的路径。这一段目录可以写多个,如:

find /etc /var /usr -name "passwd"

第三段,表达式。我们例子中用的是-name “passwd”这个表达式,指定条件为找到文件名是passwd的文件。对于find命令,最需要学习的是表达式这一段。表达式决定了我们要找的文件是什么属性的文件,还可以指定一些“动作”,比如将匹配某种条件的文件删除。所以,find命令的核心就是表达式(EXPRESSION)的指定方法。

find命令中的表达式有四种类型,分别是:

  1. Tests:就是我们最常用的指定查找文件的条件。
  2. Actions:对找到的文件可以做的操作。
  3. Global options:全局属性用来限制一些查找的条件,比如常见的目录层次深度的限制。
  4. Positional options:位置属性用来指定一些查找的位置条件。

这其中最重要的就是Tests和Actions,他们是find命令的核心。另外还有可以将多个表达式连接起来的操作符,他们可以表达多个表达式之间的逻辑关系和运算优先顺序,叫做Operators。

下面我们就来分类看一下这些个分类的功能。

TESTS

find命令是通过文件属性查找文件的。所以,find表达式的tests都是文件的属性条件,比如文件的各种时间,文件权限等。很多参数中会出现指定一个数字n,一般会出现三种写法:

+n:表示大于n。

-n:表示小于n。

n:表示等于n。

根据时间查找

比较常用数字方式来指定的参数是针对时间的查找,比如-mtime n:查找文件修改时间,单位是天,就是n*24小时。举个例子说:

[root@zorrozou-pc0 zorro]# find / -mtime 7 -ls

我们为了方便看到结果,在这个命令中使用了-ls参数,具体细节后面会详细解释。再此我们只需要知道这个参数可以将符合条件的文件的相关属性显示出来即可。那么我们就可以通过这个命令看到查找到的文件的修改时间了。

[root@zorrozou-pc0 zorro]# find / -mtime 7 -ls|head
   524295      4 drwxr-xr-x  12  root     root         4096 6月  8 13:43 /root/.config
   524423      4 drwxr-xr-x   2  root     root         4096 6月  8 13:43 /root/.config/yelp
   524299      4 drwxr-xr-x   2  root     root         4096 6月  8 13:23 /root/.config/dconf
   524427      4 -rw-r--r--   1  root     root         3040 6月  8 13:23 /root/.config/dconf/user
...

我们会发现,时间都集中在6月8号,而今天是:

[root@zorrozou-pc0 zorro]# date
2016年 06月 15日 星期三 14:30:09 CST

实际上,当我们在mtime后面指定的是7的时候,实际上是找到了距离现在7个24小时之前修改过的文件。如果我们在考究一下细节的话,可以使用这个命令再将找到的文件用时间排下顺序:

[root@zorrozou-pc0 zorro]# find / -mtime 7 -exec ls -tld {} \+

此命令用到了exec参数,后面会详细说明。我们会发现,找到的文件实际上是集中在6月7日的14:30到6月8日的14:30这个范围内的。就是说,实际上,指定7天的意思是说,找到文件修改时间范围属于距离当前时间7个24小时到8个24小时之间的文件,这是不加任何+-符号的7的含义。如果是-mtime -7呢?

[root@zorrozou-pc0 zorro]# find / -mtime -7 -exec ls -tld {} \+

你会发现找到的文件是从现在开始到7个24小时范围内的文件。但是不包括7个24小时到8个24小时的时间范围。那么-mtime +7也应该好理解了。这就是find指定时间的含义。类似的参数还有:

-ctime:以天为单位通过change time查找文件。

-atime:以天为单位通过access time查找文件。

-mmin:以分钟为单位通过modify time查找文件。

-amin:以分钟为单位通过access time查找文件。

-cmin:以分钟单位通过change time查找文件。

这些参数都是指定一个时间数字n,数字的意义跟mtime完全一样,只是时间的单位和查找的时间不一样。

除了指定时间以外,find还可以通过对比某一个文件的相关时间找到符合条件的文件,比如-anewer file。

[root@zorrozou-pc0 zorro]# find /etc -anewer /etc/passwd

这样可以在/etc/目录下找到文件的access time比/etc/passwd的access time更新的所有文件。类似的参数还有:

-cnewer:比较文件的change time。

-newer:比较文件的modify time。

-newer还有一种特殊用法,可以用来做各种时间之间的比较。比如,我想找到文件修改时间比/etc/passwd文件的change time更新的文件:

[root@zorrozou-pc0 zorro]# find /etc/ -newermc /etc/passwd

这个用法的原型是:find /etc/ -newerXY file。其中Y表示的是跟后面file的什么时间比较,而X表示使用查找文件什么时间进行比较。-newermc就是拿文件的modify time时间跟file的change time进行比较。X和Y可以使用的字母为:

a:文件access time。
c:文件change time。
m:文件modify time。

在某些支持记录文件的创建时间的文件系统上,可以使用B来表示文件创建时间。ext系列文件系统并不支持记录这个时间。

根据用户查找

-uid n:文件的所属用户uid为n。

-user name:文件的所属用户为name。

-gid n:文件的所属组gid为n。

-group name:所属组为name的文件。

-nogroup:没有所属组的文件。

-nouser:没有所属用户的文件。

根据权限查找

-executable:文件可执行。

-readable:文件可读。

-writable:文件可写。

-perm mode:查找权限为mode的文件,mode的写法可以是数字,也可以是ugo=rwx的方式如:

[root@zorrozou-pc0 zorro]# find /etc/ -perm 644 -ls

这个写法跟:

[root@zorrozou-pc0 zorro]# find /etc/ -perm u=rw,g=r,o=r -ls

是等效的。

另外要注意,mode指定的是完全符合这个权限的文件,如:

[root@zorrozou-pc0 zorro]# find /etc/ -perm u=rw,g=r -ls
   263562      4 -rw-r-----   1  root     brlapi         33 11月 13  2015 /etc/brlapi.key

没描述的权限就相当于指定了没有这个权限。

mode还可以使用/或-作为前缀进行描述。如果指定了-mode,就表示没指定的权限是忽略的,就是说,权限中只要包涵相关权限即可。如:

[root@zorrozou-pc0 zorro]# find /etc/ -perm 600 -ls

这是找到所有只有rw——-权限的文件,而-600就表示只要是包括了rw的其他位任意的文件。mode加/前缀表示的是,指定的权限只要某一位复合条件就可以,其他位跟-一样忽略,就是说-perm /600还可以找到r——–或者-w——-这样权限的文件。老版本的/前缀是用+表示的,新版本的find意境不支持mode前加+前缀了。

根据路径查找

-name pattern:文件名为pattern指定字符串的文件。注意如果pattern中包括*等特殊符号的时候,需要加””。

-iname:name的忽略大小写版本。

-lname pattern:查找符号连接文件名为pattern的文件。

-ilname:lname的忽略大小写版本。

-path pattern:根据完整路径查找文件名为pattern的文件,如:

[root@zorrozou-pc0 zorro]# find /etc -path "/e*d"| head
/etc/machine-id
/etc/profile.d
/etc/vnc/xstartup.old
/etc/vnc/config.d
/etc/vnc/updateid
/etc/.updated

-ipath:path的忽略大小写版本。

-regex pattern:用正则表达式匹配文件名。

-iregex:regex的忽略大小写版本。

其他状态查找

-empty:文件为空而且是一个普通文件或者目录。

-size n[cwbkMG]:指定文件长度查找文件。单位选择位:

c:字节单位。

b:块为单位,块大小为512字节,这个是默认单位。

w:以words为单位,words表示两个字节。

k:以1024字节为单位。

M:以1048576字节为单位。

G:以1073741824字节温单位。

n的数字指定也可以使用+-号作为前缀。意义跟时间类似,表示找到小于(-)指定长度的文件或者大于(+)指定长度的文件。

-inum:根据文件的inode编号查找。

-links n:根据文件连接数查找。

-samefile name:找到跟name指定的文件完全一样的文件,就是说两个文件是硬连接关系。

-type c:以文件类型查找文件:

c可以选择的类型为:

b:块设备

c:字符设备

d:目录

p:命名管道

f:普通文件

l:符号连接

s:socket

ACTIONS

表达式中的actions类型参数主要是用来对找到的文件进行操作的参数。在上面的例子中,我们已经看到可以使用-ls参数对找到的文件进行长格式显示,这就是一个actions类型的参数。类似的参数还有。

-fls file:跟-ls功能一样,区别是将信息写入file指定的文件,而不是显示在屏幕上。

-print:将找到的文件显示在屏幕上,实际上默认find命令就会将文件打印出来显示。

-print0:-print参数会将每个文件用换行分割,而这个参数适用null分割。有时候在脚本编程时可能会用上。

-fprint file:-print参数的写入文件版本。将内容写到文件中,而不是显示在屏幕上。

-fprint0 file:-print0的写入文件版本。

-delete:可以将找到的文件直接删除。

-printf:格式化输出方式打印。如:

[root@zorrozou-pc0 zorro]# find /etc/ -name "pass*" -printf "%p "
/etc/default/passwd /etc/pam.d/passwd /etc/passwd- /etc/passwd 

显示文件名,并以空格分隔。%p代表文件名。其他信息可以参见man find。

-prune:如果复合条件的是一个目录,则不进入目录进行查找。例子:

[root@zorrozou-pc0 zorro]# mkdir /etc/passs
[root@zorrozou-pc0 zorro]# touch /etc/passs/passwd
[root@zorrozou-pc0 zorro]# find /etc/ -name "pass*" -prune
/etc/passs
/etc/default/passwd
/etc/pam.d/passwd
/etc/passwd-
/etc/passwd
[root@zorrozou-pc0 zorro]# find /etc/ -name "pass*"
/etc/passs
/etc/passs/passwd
/etc/default/passwd
/etc/pam.d/passwd
/etc/passwd-
/etc/passwd

我们先创建了一个/etc/passs的目录,然后在这个目录下创建了一个叫passwd的文件。之后先用带-prune的find看到,能显示出passs目录,但是目录中的passwd文件并没有显示,说明这个参数让find命令没有进入这个目录查找。而后一个不带-prune参数的find显示出了passs目录下的passwd。

-quit:找到符合条件的文件后立即退出。


捐助穷佐罗,让他过上温饱生活。

收钱


find中执行命令

-exec

find命令的exec是一个非常好用的参数,当然其可能造成的破坏也可能非常大。在学习它之前,我先要提醒大家,使用之前千万要确定自己在做什么。

这个参数的常见格式是:

-exec command ;

注意后面的分号。它是用来给find做标记用的。find在解析命令的时候,要区分给定的参数是要传给自己的还是要传给command命令的。所以find以分号作为要执行命令所有参数的结束标记。命令返回值为0则返回true。在exec参数指定的执行命令中,可以使用{}符号表示当前find找到的文件名。比如:

[root@zorrozou-pc0 find]# find /etc/ -name "passwd" -exec echo {} \;
/etc/default/passwd
/etc/pam.d/passwd
/etc/passwd

上面的命令表示,找到/etc/目录下文件名为passwd的文件,并echo其文件名。注意再使用分号的时候前面要加转移字符\,因为分号也是bash的特殊字符,所以bash会先解释它。前面加上\就可以让bash直接将其船体给find命令,这个分号由find解释,而不是bash。其实这个exec用的比较废话,毕竟find本身就会找到相关条件的文件并显示其文件名。但是试想如果我们将echo换成rm或者cp,是不是就有意义的多?比如:

[root@zorrozou-pc0 find]# find /etc/ -name "passwd" -exec rm {} \;

请不要执行这个命令!!

或者:

[root@zorrozou-pc0 find]# find /etc/ -name "passwd" -exec cp {} {}.bak \;

这个命令可以将符合条件的文件都加个.bak后缀备份一份。于是我们可以执行删除了:

[root@zorrozou-pc0 find]# find /etc/ -name "passwd.bak" 
/etc/default/passwd.bak
/etc/pam.d/passwd.bak
/etc/passwd.bak
[root@zorrozou-pc0 find]# find /etc/ -name "passwd.bak" -exec rm {} \;
[root@zorrozou-pc0 find]# find /etc/ -name "passwd.bak" 

当然,删除前还是要确认清楚你要删的文件一定是对的。

-execdir

execdir和exec有一些差别,主要是在执行指定的命令时,那个相关命令是在那个工作目录下执行的差别。exec是在find所指定的起始目录,而execdir是文件所在目录。对比一下就明白了:

[root@zorrozou-pc0 find]# find /etc/ -name "passwd" -exec echo {} \;
/etc/default/passwd
/etc/pam.d/passwd
/etc/passwd
[root@zorrozou-pc0 find]# find /etc/ -name "passwd" -execdir echo {} \;
./passwd
./passwd
./passwd

一个命令打印出来的路径都是/etc/开头,另一个显示的都是当前目录下的某某文件。

execdir的方式要比exec安全一些,因为这种执行方式避免了在解析文件名时所产生的竞争条件。

出了上述两种比较典型的执行命令的方法以外,find还对这两个参数提供了另一种形式的命令执行格式:

-exec command {} +

-execdir command {} +

我们还是先用例子来看一下这个格式和以分号结束的方式的差别:

[root@zorrozou-pc0 find]# find /etc/ -name "passwd" -exec echo {} \;
/etc/default/passwd
/etc/pam.d/passwd
/etc/passwd
[root@zorrozou-pc0 find]# find /etc/ -name "passwd" -exec echo {} \+
/etc/default/passwd /etc/pam.d/passwd /etc/passwd

光这样看可能还不是很明显,我们可以这样在描述一遍他们的执行过程:

echo /etc/default/passwd
echo /etc/pam.d/passwd
echo /etc/passwd

echo /etc/default/passwd /etc/pam.d/passwd /etc/passwd

其实就是说,对于command {} ;格式来说,每找到一个文件就执行一遍相关命令,而command {} +格式的意思是说,先执行find,找到所有符合条件的文件之后,将每个文件作为命令的一个参数传给命令执行,exec指定的命令实际上只被执行了一次。这样用的限制也是不言而喻的:{}只能出现一次。

[root@zorrozou-pc0 find]# find /etc -mtime -7 -type f -exec cp -t /tmp/back/ {} \+

上面这个命令将符合条件的文件全部cp到了/tmp/back目录中,当然如果文件有重名的情况下,会被覆盖掉。从这个命令中我们学习一下{} +格式的使用注意事项,它不能写成:

find /etc -mtime -7 -type f -exec cp {} /tmp/back/ \+

所以只能使用-t参数改变cp命令的参数顺序来指定相关的动作。

无论如何,直接使用exec和execdir是很危险的,因为他们会直接对找到的文件调用相关命令,并且没有任何确认。所以我们不得不在进行相关操作前再三确认,以防止误操作。当然,find命令也给了更安全的exec参数,它们就是:

-ok

-okdir

它们的作用跟exec和execdir一样,区别只是在做任何操作之前,会让用户确认是不是ok?如:

[root@zorrozou-pc0 find]# find /etc -mtime -7 -type f -ok cp -t /tmp/back/ {} \;
< cp ... /etc/bluetooth/main.conf > ? 

于是,每一次cp你都要确认是不是要这么做。只要你输入的是y或者以y开头的任何字符串,都是确认。其他的字符串是否认。另外,这两个参数不支持{} +的格式。

OPERATORS

find的操作符(OPERATORS)实际上是用来连接多个表达式和确定其逻辑关系用的。如:

[root@zorrozou-pc0 zorro]# find /etc -name "pass*" -type f
/etc/passs/passwd
/etc/default/passwd
/etc/pam.d/passwd
/etc/passwd-
/etc/passwd

这个find命令中使用了两个表达式,他们之间没有任何分隔,这是实际上表达的含义是,找到两个条件都符合的文件。实际上就是表达式的逻辑与关系,这跟-a参数连接或者-and参数一样:

[root@zorrozou-pc0 zorro]# find /etc -name "pass*" -a -type f
/etc/passs/passwd
/etc/default/passwd
/etc/pam.d/passwd
/etc/passwd-
/etc/passwd
[root@zorrozou-pc0 zorro]# find /etc -name "pass*" -and -type f
/etc/passs/passwd
/etc/default/passwd
/etc/pam.d/passwd
/etc/passwd-
/etc/passwd

除了逻辑与关系以外,还有逻辑或关系:

[root@zorrozou-pc0 zorro]# find /etc -name "pass*" -o -type f

[root@zorrozou-pc0 zorro]# find /etc -name "pass*" -or -type f

表示两个条件只要符合其中一个都可以。

在条件表达式前面加!表示对表达式取非。同样的也可以用-not参数。另外如果表达式很多,可以使用( expr )确定优先级,如:

[root@zorrozou-pc0 zorro]# find / \( -name "passwd" -a -type f \) -o \( -name "shadow" -a -type f \)

这里表示的是:-name “passwd” -a -type f和-name “shadow” -a -type f是或关系。

最后

find中还可能常用的其他参数比如:

-depth:制定了这个参数后,遇到目录先进入目录操作目录中的文件,最后再操作目录本身。

-maxdepth:目录最大深度限制。

-mindepth:目录最小深度限制。

还有一些其他相关参数大家可以在man find中自行补充,就不在这更多废话了。希望本篇可以对大家深入的掌握find命令有所帮助。

如果有相关问题,可以在我的微博、微信或者博客上联系我。

捐助穷佐罗,让他过上温饱生活。

收钱


大家好,我是Zorro!

如果你喜欢本文,欢迎在微博上搜索“orroz”关注我,地址是:http://weibo.com/orroz

大家也可以在微信上搜索:Linux系统技术 关注我的公众号。

我的所有文章都会沉淀在我的个人博客上,地址是:http://liwei.life。

欢迎使用以上各种方式一起探讨学习,共同进步。

公众号二维码:

Zorro] icon


 

sed命令进阶

sed命令进阶

版权声明:

本文章内容在非商业使用前提下可无需授权任意转载、发布。

转载、发布请务必注明作者和其微博、微信公众号地址,以便读者询问问题和甄误反馈,共同进步。

微博ID:**orroz**

微信公众号:**Linux系统技术**

前言

本文主要介绍sed的高级用法,在阅读本文之前希望读者已经掌握sed的基本使用和正则表达式的相关知识。本文主要可以让读者学会如何使用sed处理段落内容

希望本文能对你帮助。

捐助穷佐罗,让他过上温饱生活。

收钱

问题举例

日常工作中我们都经常会使用sed命令对文件进行处理。最普遍的是以行为单位处理,比如做替换操作,如:

[root@TENCENT64 ~]# head -3 /etc/passwd 
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
[root@TENCENT64 ~]# head -3 /etc/passwd |sed 's/bin/xxx/g'
root:x:0:0:root:/root:/xxx/bash
xxx:x:1:1:xxx:/xxx:/sxxx/nologin
daemon:x:2:2:daemon:/sxxx:/sxxx/nologin

于是每一行中只要有”bin”这个关键字的就都被替换成了”xxx”。以”行”为单位的操作,了解sed基本知识之后应该都能处理。我们下面通过一个对段落的操作,来进一步学习一下sed命令的高级用法。

工作场景下有时候遇到的文本内容并不仅仅是以行为单位的输出。举个例子来说,比如ifconfig命令的输出:

[root@TENCENT64 ~]# ifconfig
eth1 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
inet addr:10.0.0.1 Bcast:10.213.123.255 Mask:255.255.252.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:4699092093 errors:0 dropped:0 overruns:0 frame:0
TX packets:4429422167 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0 
RX bytes:621383727756 (578.7 GiB) TX bytes:987104190070 (919.3 GiB)

eth2 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:421719237 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0 
RX bytes:17982386416 (16.7 GiB) TX bytes:0 (0.0 b)

eth3 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:401384950 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0 
RX bytes:16934321897 (15.7 GiB) TX bytes:0 (0.0 b)

eth4 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:139540553 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0 
RX bytes:5929444543 (5.5 GiB) TX bytes:0 (0.0 b)

eth5 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:1810276711441 errors:0 dropped:0 overruns:22712568 frame:0
TX packets:1951148522633 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 
RX bytes:743435149333809 (676.1 TiB) TX bytes:707431806048867 (643.4 TiB)
Memory:a9a40000-a9a60000

eth6 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:210438688172 errors:0 dropped:9029561 overruns:0 carrier:0
collisions:0 txqueuelen:0 
RX bytes:0 (0.0 b) TX bytes:219869363023063 (199.9 TiB)

eth7 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:628704252152 errors:0 dropped:20495142 overruns:0 carrier:0
collisions:0 txqueuelen:0 
RX bytes:0 (0.0 b) TX bytes:247252814884293 (224.8 TiB)

eth8 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:7987321426 errors:0 dropped:904533 overruns:0 carrier:0
collisions:0 txqueuelen:0 
RX bytes:0 (0.0 b) TX bytes:1467641590110 (1.3 TiB)

eth9 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:1088671818709 errors:0 dropped:13914796 overruns:0 carrier:0
collisions:0 txqueuelen:0 
RX bytes:0 (0.0 b) TX bytes:236639340770145 (215.2 TiB)

lo Link encap:Local Loopback 
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:16436 Metric:1
RX packets:3094508 errors:0 dropped:0 overruns:0 frame:0
TX packets:3094508 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0 
RX bytes:1579253954 (1.4 GiB) TX bytes:1579253954 (1.4 GiB)

br0 Link encap:Ethernet HWaddr FE:BD:B8:D5:79:46 
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1
RX packets:118073976200 errors:0 dropped:0 overruns:0 frame:0
TX packets:129141892891 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 
RX bytes:13271406153198 (12.0 TiB) TX bytes:21428348510630 (19.4 TiB)

br1 Link encap:Ethernet HWaddr FE:BD:B8:D5:87:36 
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1
RX packets:210447731529 errors:0 dropped:0 overruns:0 frame:0
TX packets:145867293712 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 
RX bytes:216934635012821 (197.3 TiB) TX bytes:112307933521307 (102.1 TiB)

br2 Link encap:Ethernet HWaddr FE:BD:B8:D5:A1:1C 
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1
RX packets:227580515069 errors:0 dropped:0 overruns:0 frame:0
TX packets:224128670696 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 
RX bytes:146402818737176 (133.1 TiB) TX bytes:121031384149060 (110.0 TiB)

br3 Link encap:Ethernet HWaddr FE:BD:B8:D5:A1:F4 
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1
RX packets:210447731529 errors:0 dropped:0 overruns:0 frame:0
TX packets:145867293713 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 
RX bytes:216934635012821 (197.3 TiB) TX bytes:112307933522807 (102.1 TiB)

br4 Link encap:Ethernet HWaddr FE:BD:B8:D5:A4:9A 
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1
RX packets:210447731531 errors:0 dropped:0 overruns:0 frame:0
TX packets:145867293714 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 
RX bytes:216934635013645 (197.3 TiB) TX bytes:112307933522867 (102.1 TiB)

br5 Link encap:Ethernet HWaddr FE:BD:B8:D5:AA:21 
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1
RX packets:7988225959 errors:0 dropped:0 overruns:0 frame:0
TX packets:9460529821 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 
RX bytes:1355936046078 (1.2 TiB) TX bytes:1354671618850 (1.2 TiB)

这是一个有很多网卡的服务器。观察以上输出我们会发现,每个网卡的信息都有多行内容,组成一段。网卡名称和MAC地址在一行,网卡名称IP地址不在一行。类似的还有网卡的收发包数量的信息,以及收发包的字节数。那么对于类似这样的文本内容,当我们想要使用sed将输出处理成:网卡名对应IP地址或者网卡名对应收包字节数,将不在同一行的信息放到同一行再输出该怎么处理呢?类似这样:

网卡名:收包字节数
eth1:621383727756
eth2:17982386416
...

这样的需求对于一般的sed命令来说显然做不到了。我们需要引入更高级的sed处理功能来处理类似问题。大家可以先将上述代码保存到一个文本文件里以备后续实验,我们先给出答案:

网卡名对应RX字节数:

[root@zorrozou-pc0 zorro]# sed -n '/^[^ ]/{s/^\([^ ]*\) .*/\1/g;h;: top;n;/^$/b;s/^.*RX bytes:\([0-9]\{1,\}\).*/\1/g;T top;H;x;s/\n/:/g;p}' ifconfig.out 
eth1:621383727756
eth2:17982386416
eth3:16934321897
eth4:5929444543
eth5:743435149333809
eth6:0
eth7:0
eth8:0
eth9:0
lo:1579253954
br0:13271406153198
br1:216934635012821
br2:146402818737176
br3:216934635012821
br4:216934635013645
br5:1355936046078

网卡名对应ip地址:

[root@zorrozou-pc0 zorro]# sed -n '/^[^ ]/{s/^\([^ ]*\) .*/\1/g;h;: top;n;/^$/b;s/^.*inet addr:\([0-9]\{1,\}\.[0-9]\{1,\}\.[0-9]\{1,\}\.[0-9]\{1,\}\).*/\1/g;T top;H;x;s/\n/:/g;p}' ifconfig.out 
eth1:10.0.0.1
lo:127.0.0.1

我们还会发现显示IP的sed命令很智能的过滤掉了没有IP地址的网卡,只把有IP的网卡显示出来。相信一部分人看到这个命令已经晕了。先不要着急,讲解这个命令的含义之前,我们需要先来了解一下sed的工作原理。

模式空间和保存空间

默认情况下,sed是将输入内容一行一行进行处理的。如果我们拿一个简单的sed命令举例,如:sed ‘1,$p’ /etc/passwd。处理过程中,sed会一行一行的读入/etc/passwd文件,查看每行是否匹配定址条件(1,$),如果匹配条件,就讲行内容放入模式空间,并打印(p命令)。由于文本流本身的输出,而模式空间内容又被打印一遍,所以这个命令最后会将每一行都显示2遍。

[zorro@zorrozou-pc0 ~]$ sed '1,$p' /etc/passwd
root:x:0:0:root:/root:/bin/bash
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/usr/bin/nologin
bin:x:1:1:bin:/bin:/usr/bin/nologin
daemon:x:2:2:daemon:/:/usr/bin/nologin
daemon:x:2:2:daemon:/:/usr/bin/nologin

默认情况下,sed程序在所有的脚本指令执行完毕后,将自动打印模式空间中的内容,这会导致p命令输出相关行两遍,-n选项可以屏蔽自动打印。

模式空间(pattern space)

模式空间的英文是pattern space。在sed的处理过程中使用这个缓存空间缓存了要处理的内容。由上面的例子可以看到,模式空间是sed处理最核心的缓存空间,所有要处理的行内容都会复制进这个空间再进行修改,或根据需要显示。默认情况下sed也并不会修改原文件本身内容,只修改模式空间内容。

保存空间(hlod space)

除了模式空间以外,sed还实现了另一个内容缓存空间,名字叫保存空间。大家可以想象这个空间跟模式空间一样,也就是一段内存空间,一般情况下不用,除非我们使用相关指令才会对它进行使用。

示例命令分析

了解了以上两个空间以后,我们来看看例子中的命令到底进行了什么处理,例子原来是这样的:

sed -n '/^[^ ]/{s/^\([^ ]*\) .*/\1/g;h;: top;n;/^$/b;s/^.*RX bytes:\([0-9]\{1,\}\).*/\1/g;T top;H;x;s/\n/:/g;p}' ifconfig.out

为了方便分析,我们将这个sed命令写成一个sed脚本,用更清晰的语法结构再来看一下。脚本跟远命令有少许差别,最后的p打印变成了写文件:

[zorro@zorrozou-pc0 ~]$ cat -n ifconfig.sed 
1 #!/usr/bin/sed -f
2 
3 /^[^ ]/{
4 s/^\([^ ]*\) .*/\1/g;
5 h;
6 : top;
7 n;
8 /^$/b;
9 s/^.*RX bytes:\([0-9]\{1,\}\).*/\1/g;
10 T top;
11 H;
12 x;
13 s/\n/:/g;
14 # p;
15 w result
16 }

这个sed脚本执行结果是这样的:

[zorro@zorrozou-pc0 ~]$ ./ifconfig.sed ifconfig.out 
eth1
inet addr:10.0.0.1 Bcast:10.213.123.255 Mask:255.255.252.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:4699092093 errors:0 dropped:0 overruns:0 frame:0
TX packets:4429422167 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0 
eth1:621383727756

eth2
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
......

最后所有有效输出会写入result文件:

[zorro@zorrozou-pc0 ~]$ cat result 
eth1:621383727756
eth2:17982386416
eth3:16934321897
eth4:5929444543
eth5:743435149333809
eth6:0
eth7:0
eth8:0
eth9:0
lo:1579253954
br0:13271406153198
br1:216934635012821
br2:146402818737176
br3:216934635012821
br4:216934635013645
br5:1355936046078

在分析脚本代码之前,我们先要理清楚根据输出内容要做的处理思路。观察整个ifconfig命令输出的内容,我们会发现,每个网卡输出的信息都是由多行组成的,并且每段之间由空行分隔。每个网卡信息段的输出第一行都是网卡名,并且网卡名称没有任何前缀字符:

sed

 

从输出内容分析,我们应该做的事情是,找到以非空格开头的行,取出网卡名记录下来,并且再从下面的非空行找RX bytes:这个关键字,取出它后面的数字(接收字节数)。如果碰见空行,则表示这段分析完成,开始下一段分析。开始分析我们的程序,我们先从头来,如何取出网卡名所在的行?这个很简单,只要行开始不是以空格开头,就是网卡名所在行,所以:

[zorro@zorrozou-pc0 ~]$ sed -n '/^[^ ]/p' ifconfig.out 
eth1 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
eth2 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
eth3 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
eth4 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
eth5 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 
......

非空格开头的正则表达式,锚定了段操作的开头。当然,我们找到段操作开头之后,后面绝不仅仅是一个简单的p打印,我们需要更复杂的组合操作。此时{}语法就起到作用了。sed的{}可以将多个命令组合进行操作,每个命令使用分号”;”分隔。如:

[zorro@zorrozou-pc0 ~]$ sed -n '/^[^ ]/{s/^\([^ ]*\) .*/\1/g;p}' ifconfig.out 
eth1
eth2
eth3
eth4
eth5

这个命令的意思就是,取出网卡名所在的行,现在模式空间中进行替换操作,只保留网卡名(s/^([^ ]) ./\1/g),然后打印模式空间内容p。这里涉及到一个sed替换命令的正则表达式保存功能,如果不清楚的请自行补充相关知识。

学会了组合命令的{}方法之后,我们就该考虑找到了网卡名所在行之后该做什么了。第一个该做的事情就是,保存网卡名,并且只保留网卡名,别的信息不要。刚才已经通过替换命令实现了。然后下一步应该读取下一行进入模式空间,然后看看下一行中有没有RX bytes:,如果有,就取后面的数组进行保存。如果没有就再看下一行,如此循环,直到看到空行为止。这个过程中,我们需要对找到的有用信息进行保存,如:网卡名,接收数据包。那么保存到哪里呢?如果保存到模式空间,那么下一行读入的时候保存的信息就没了。所以此时我们就需要保存空间来帮我们保存内容了。后面的命令整个组成了一个逻辑,所以我们一起拿出来看:

[zorro@zorrozou-pc0 ~]$ cat -n ifconfig.sed 
1 #!/usr/bin/sed -f
2 
3 /^[^ ]/{
4 s/^\([^ ]*\) .*/\1/g;
5 h;
6 : top;
7 n;
8 /^$/b;
9 s/^.*RX bytes:\([0-9]\{1,\}\).*/\1/g;
10 T top;
11 H;
12 x;
13 s/\n/:/g;
14 # p;
15 w result
16 }

第3行和第3行不用解释了,找到网卡名的行并且在模式空间里只保留网卡名称。之后是第5行的h命令。h命令的意思是,将现在模式空间中的内容,放到保存空间。于是,网卡名就放进了保存空间。

然后是第6行,: top在这里只起到一个标记的作用。我们使用:标记了一个位置名叫top。我们先暂时记住它。

第7行n命令。这个命令就是读取下一行到模式空间。就是说,网卡行已经处理完,并且保存好了,可以n读入下一行了。n之后,下一行的内容就进入模式空间,然后继续从n下面的命令开始处理,就是第8行。

第8行使用的是b命令,用来跳出本次sed处理。这里的含义是检查下一行内容如果是空行/^$/,就跳出本段的sed处理,说明这段网卡信息分析完毕,可以进行下一段分析了。如果不是空行,这行就不起作用,于是继续处理第9行。b命令除了可以跳出本次处理以外,还可以指定跳转的位置,比如上文中使用:标记的top。

第9行使用s替换命令取出行中RX bytes:字符之后的所有数字。这里本身并没有分支判断,分支判断出现在下一行。

第10行T命令是一个逻辑判断指令。它的含义是,如果前面出现的s替换指令没有找到符合条件的行,则跳转到T后面所指定的标记位置。在这里的标记位置为top,是我们在上面使用:标记出来的位置。就是说,如果上面的s替换找不到RX bytes:关键字的行,那么就执行过程会跳回top位置继续执行,就是接着再看下一行进行检查。

在这里,我们实际上是用了冒号标签和T指令构成了一个循环。当条件是s替换找不到指定行的时候,就继续执行本循环。直到找到为止。找到之后就不会再跳回top位置了,而是继续执行第11行。

第11行的指令是H,意思是将当前模式空间中的内容追加进保存空间。当前模式空间已经由s替换指令处理成只保留了接受的字节数。之前的保存空间中已经存了网卡名,追加进字节数之后,保存空间里的内容将由两行构成,第一行是网卡名,第二行是接收字节数。注意h和H指令的区别,h是将当前模式空间中的内容保存进保存空间,这样做会使保存空间中的原有内容丢失。而H是将模式空间内容追加进保存空间,保存空间中的原来内容还在。

此时保存空间中已经存有我们所有想要的内容了,网卡名和接收字节数。它们是放在两行存的,这仍然不是我们想要的结果,我们希望能够在一行中显示,并用冒号分隔。所以下面要做的是替换,将换行替换成冒号。但是我们不能直接操作保存空间中的内容,所以需要将保存空间的内容拿回模式空间才能操作。

第12行是用了x指令,意思是将模式空间内容和保存空间内容互换。互换之后,网卡和接收字节数就回到模式空间了。然后我们可以使用s指令对模式空间内容做替换。

第13行s/\n/:/g,将换行替换成冒号。之后模式空间中的内容就是我们真正想要的“网卡名:接收字节数”了。于是就可以p打印或者使用w指令,保存模式空间内容到某个文件中。

所有指令的退出条件有两个:

  1. 第8行,遇到空格之后本段解析结束。
  2. 所有指令执行完,打印或保存了相关信息之后执行完。

执行完退出后,sed会接着找下一个网卡信息开始的段,继续下一个网卡的解析操作。这就是这个复杂sed命令的整体处理过程。

明白了整个操作过程之后,我们对sed的模式空间,保存空间和语法相关指令就应该建立了相关概念了。接下来我们还有必要学习一下sed还支持哪些指令,以便以后处理复杂的文本时可以使用。

常用高级操作命令

d:删除模式空间内容并开始下一个输入处理。

D:删除模式空间中的第一行内容,并开始下一个输入的处理。如果模式空间中只有一行内容,那它的作用跟d指令一样。

h:将模式空间拷贝到保存空间。

H:将模式空间追加到保存空间。

g:将保存空间复制到模式空间。

G:将保存空间追加到模式空间。

n:读入下一行到模式空间。

N:追加下一行到模式空间。

p:打印模式空间中的内容。

s///:替换模式空间中的内容。

x:将模式空间内容和保存空间内容交换。
lable:标记一个标签,注意lable的字符个数限制为7个字符以内。

t:测试这个指令之前的替换s///命令,如果替换命令能匹配到可替换的内容,则跳转到t指令后面标记的lable上。

T:测试这个指令之前的替换s///命令,如果替换命令不能匹配到可替换的内容,则跳转到T指令后面标记的lable上。

[zorro@zorrozou-pc0 ~]$ ip ad sh|sed -n '/^[0-9]/{:top;n;s/inet6/inet/;T top;p}'
inet ::1/128 scope host 
inet fe80::95a9:3e28:5102:1a84/64 scope link 
[zorro@zorrozou-pc0 ~]$ ip ad sh|sed -n '/^[0-9]/{:top;n;s/inet6/&/;T top;p}'
inet6 ::1/128 scope host 
inet6 fe80::95a9:3e28:5102:1a84/64 scope link 

定义一个标签top,如果s///的替换功能不能执行,则表示这行中没有inet6这个单词,于是就会到top读取下一行(n命令功能)。如果测试s///成功执行,则p打印相关行。这样就拿出来了有ipv6地址的行。

最后

关于sed的其它指令,基本都不是难点,这里不再解释。大家可以通过man sed查看帮助。

本文试图通过分析一个sed处理段落内容的案例,介绍了sed的模式空间、保存空间以及跳转、标签(label)这些概念和高级指令。希望大家能理解其使用方法和编程思路,以便在工作中能使用sed更加方便灵活的处理文本信息。工欲善其事,必先利其器。

谢谢大家!

如果本文对你有用,请任意打赏,穷佐罗再次拜谢了。

收钱


大家好,我是Zorro!

如果你喜欢本文,欢迎在微博上搜索“orroz”关注我,地址是:http://weibo.com/orroz

大家也可以在微信上搜索:Linux系统技术 关注我的公众号。

我的所有文章都会沉淀在我的个人博客上,地址是:http://liwei.life。

欢迎使用以上各种方式一起探讨学习,共同进步。

公众号二维码:

Zorro] icon