如何逐个chown文件系统:perl中的inode对,或更好的解决方案

时间:2011-04-22 15:06:39

标签: linux perl unix posix

我有一个相对复杂的perl脚本,它遍历文件系统并存储更新的所有权列表,然后遍历该列表并应用更改。我这样做是为了更新已更改的UID。因为我有几种情况我交换用户a和用户b的UID,我不能只说“现在1的所有内容应该是2,而2的所有内容应该是1”,因为这个脚本也可以被中断,系统将处于“从备份恢复并重新开始”之外的完全破坏,几乎无法恢复的状态。这真是太糟糕了。

为了避免这个问题,我做了上面的两个方法,创建了一个像$ changes-> {path} - > \%c这样的结构,其中c有属性行newuid,olduid,newgid和olduid。然后我冻结了哈希,一旦它写入磁盘,我就读回哈希并开始进行更改。这样,如果我被打断了,我可以检查冻结的哈希是否存在,如果有,则再次开始应用更改。

缺点是有时候不断变化的用户拥有数百万个文件,通常路径非常长。这意味着我将很多非常长的字符串存储为哈希键,有时我会耗尽内存。所以,我想出了两个选择。与此问题相关的一个是将元素存储为设备:inode对。这样可以提高空间效率,并且可以唯一地识别文件系统元素。缺点是我还没有找到一种特别有效的方法来从inode获取设备相对路径,或者只是将我想要的stat()更改应用于inode。是的,我可以做另一个查找,并为每个文件查找我存储的设备和inode列表,看看是否需要更改。但是,如果有一个perl可访问的系统调用 - 可以在HP-UX,AIX和Linux上移植 - 我可以直接说“在这个设备上对这个inode进行了这些更改”,它会从一个明显更好的绩效观点。

我在几千个系统中运行它,其中一些系统具有PB级文件系统,拥有数万亿个文件。因此,虽然性能可能不会在我的家用PC上产生很大差异,但在这种情况下它实际上有些重要。 :)性能需求,顺便说一句,这就是为什么我真的不想做其他选项 - 这是通过将哈希绑定到基于磁盘的文件来绕过内存问题。这就是为什么我宁愿做更多的工作来避免不得不第二次遍历整个文件系统。

当然,也欢迎可以减少内存消耗的替代建议。 :)我的要求只是我需要记录旧的和新的UID / GID值,因此我可以支持更改/验证更改/更新从清理日期之前的备份恢复的文件。我已经考虑过使/ path / to / file看起来像$ {changes} - > {root} - > {path} - > {to} - > {file},但这需要更多的工作来遍历,我不知道它真的会节省我足够的内存空间来解决我的问题。折叠整个事物 - > {device} - > {inode}使它基本上只有两个整数而不是N个字符的大小,对于比2更长的任何路径, substatial 字符。 :)

2 个答案:

答案 0 :(得分:1)

简化的想法

当我提到流媒体时,我并不是指不受控制的。数据库日志(例如)也以流模式写入,以进行比较。

另请注意,您“甚至无法对单个子目录进行排序”的声明与使用Perl哈希来存储相同信息完全相矛盾(如果您不相信我不会责怪您没有CS背景。)

所以这里有一个非常简单的例子,说明你可以做什么。请注意,路上的每一步都是流式传输,可重复和记录。

# export SOME_FIND_OPTIONS=...?
find $SOME_FIND_OPTIONS -print0 | ./generate_script.pl > chownscript.sh

# and then
sh -e ./chownscript.sh

generate_script.pl的一个例子(显然,根据您的需要调整它):

#!/usr/bin/perl
use strict;
use warnings;

$/="\0";
while (<>)
{
    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat;

    # demo purpose, silly translation:
    my ($newuid, $newgid) = ($uid+1000, $gid+1000);

    print "./chmod.pl $uid:$gid $newuid:$newgid '$_'\n"
}

您可以拥有chmod.pl的系统相关实现(这有助于降低复杂性,因此:风险):

#!/usr/bin/perl
use strict;
use warnings;

my $oldown = shift;
my $newown = shift;
my $path   = shift;

($oldown and $newown and $path) or die "usage: $0 <uid:gid> <newuid:newgid> <path>";

my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat $path;

die "file not found: $path" unless $ino;
die "precondition failed" unless ($oldown eq "$uid:$gid");

($uid, $gid) = split /:/, $newown;

chown $uid, $gid, $path or die "unable to chown: $path"

这将允许您在中途停止活动时重新启动,它甚至允许您在必要时手动挑选异常。您可以保存脚本,这样您就可以负责。我已经做了一个合理的尝试,使脚本安全运行。但是,这显然只是一个起点。最重要的是,我不处理文件系统交叉,符号链接,套接字,设备节点,你可能需要注意它们。


原始回复如下:

是的,如果性能问题,请在C

中进行

不要对整个文件系统进行持久性日志记录(顺便说一下,为什么需要将它们保存在单个哈希中?流式输出是你的朋友)

相反,每个目录的日志已完成运行。您可以轻松地逐步打破映射:

 user A: 1  -> 99
 user B: 2  -> 1
 user A: 99 -> 2

Ownify - 我使用的(代码

只要您可以保留像99这样的临时uid / guids的范围,就不会有任何重启风险(不是任何更多而不是在实时文件系统上执行此转换)反正)。

你可以从这个漂亮的C代码开始(事实上,这个代码并不是非常高度优化):

// vim: se ts=4 sw=4 et ar aw 
//
// make: g++ -D_FILE_OFFSET_BITS=64 ownify.cpp -o ownify 
//
// Ownify: ownify -h
//

#include <stdio.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

/* old habits die hard. can't stick to pure C ... */
#include <string>
#include <iostream>

#define do_stat(a,b)        lstat(a,b)
#define do_chown(a,b,c)     lchown(a,b,c)

//////////////////////////////////////////////////////////
// logic declarations
//
void ownify(struct stat& file)
{
//  if (S_ISLNK(file.st_mode))
//      return;
    switch (file.st_uid)
    {
#if defined(PASS1)
        case 1:  file.st_uid = 99; break;
        case 99: fputs(err, "Unexpected existing owned file!"); exit(255);
#elif defined(PASS2)
        case 2:  file.st_uid = 1;  break;
#elif defined(PASS3)
        case 99: file.st_uid = 1;  break;
#endif
    }
    switch (file.st_gid) // optionally map groups as well
    {
#if defined(PASS1)
#elif defined(PASS2)
#elif defined(PASS3)
#endif
    }
}

/////////////////////////////////////////////////////////
// driver
//
static unsigned int changed = 0, skipped = 0, failed  = 0;
static bool dryrun = false;

void process(const char* const fname)
{
    struct stat s;
    if (0==do_stat(fname, &s))
    {
        struct stat n = s;
        ownify(n);

        if ((n.st_uid!=s.st_uid) || (n.st_gid!=s.st_gid))
        {
            if (dryrun || 0==do_chown(fname, n.st_uid, n.st_gid))
                printf("%u\tchanging owner %i:%i '%s'\t(was %i:%i)\n", 
                        ++changed,
                        n.st_uid, n.st_gid, 
                        fname, 
                        s.st_uid, s.st_gid);
            else 
            {
                failed++;
                int e = errno;
                fprintf(stderr, "'%s': cannot change owner %i:%i (%s)\n", 
                        fname, 
                        n.st_uid, n.st_gid, 
                        strerror(e));
            }
        }
        else
            skipped++;
    } else
    {
        int e = errno;
        fprintf(stderr, "'%s': cannot stat (%s)\n", fname, strerror(e));
        failed++;
    }
}

int main(int argc, char* argv[])
{
    switch(argc)
    {   
        case 0: //huh?
        case 1: break;
        case 2:
            dryrun = 0==strcmp(argv[1],"-n") || 
                0==strcmp(argv[1],"--dry-run");
            if (dryrun)
                break;
        default:
            std::cerr << "Illegal arguments" << std::endl;
            std::cout << 
                argv[0] << " (Ownify): efficient bulk adjust of owner user:group for many files\n\n"
                           "Goal: be flexible and a tiny bit fast\n\n" 
                           "Synopsis:\n"
                           "    find / -print0 | ./ownify -n 2>&1 | tee ownify.log\n\n"
                           "Input:\n"
                           "    reads a null-delimited stream of filespecifications from the\n"
                           "    standard input; links are _not_ dereferenced.\n\n"
                           "Options:\n"
                           "    -n/--dry-run    - test run (no changes)\n\n"
                           "Exit code:\n"
                           "    number of failed items" << std::endl;
            return 255;
    }

    std::string fname("/dev/null");
    while (std::getline(std::cin, fname, '\0'))
        process(fname.c_str());

    fprintf(stderr, "%s: completed with %u skipped, %u changed and %u failed%s\n", 
            argv[0], skipped, changed, failed, dryrun?" (DRYRUN)":"");

    return failed;
}

请注意,这有很多安全措施

  • 在第一次通过时检查偏执(检查没有保留uid的fiels)
  • 根据链接
  • 更改do_statdo_chown行为的能力
  • --dry-run 选项(观察将要完成的内容 -n

该程序很乐意告诉您如何使用ownify -h

./ownify (Ownify): efficient bulk adjust of owner user:group for many files

Goal: be flexible and a tiny bit fast

Synopsis:
    find / -print0 | ./ownify -n 2>&1 | tee ownify.log

Input:
    reads a null-delimited stream of file specifications from the
    standard input; 

Options:
    -n/--dry-run    - test run (no changes)

Exit code:
    number of failed items

答案 1 :(得分:0)

想到一些可能的解决方案:

1)不要在文件中存储哈希,只是以任何可以合理解析的格式排序的列表。通过按文件名对列表进行排序,您应该再次运行find,而不是实际执行此操作:

# UID, GID, MODE, Filename
0,0,600,/a/b/c/d/e
1,1,777,/a/b/c/f/g
...

由于列表按文件名排序,因此每个目录的内容应该在文件中聚集在一起。您不必使用Perl对文件进行排序,sort在大多数情况下都能很好地完成。

然后,您可以逐行读取文件 - 或者使用不会破坏文件名的任何分隔符 - 只需执行任何更改。假设你可以一次告诉每个文件需要哪些更改,听起来好像你实际上需要哈希的随机访问功能,所以这应该这样做。

所以这个过程分三步进行:

  • 创建更改文件
  • 对更改文件进行排序
  • 根据更改文件执行更改

2)如果您无法一次确定每个文件需要哪些更改,则每个文件可以有多行,每行详细说明一部分更改。在您确定第一步所需的更改时,将生成每一行。然后,您可以在排序后合并它们。

3)如果您 需要随机访问功能,请考虑使用适当的嵌入式数据库,例如BerkeleyDB或SQLite。大多数嵌入式数据库都有Perl模块。但这并不会那么快。