添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Perl中提供了下面一大堆的内置函数用来获取用户、组、网络相关的信息。这些perl函数在C中也都有对应的函数。

# 获取和设置用户和组
endgrent - be done using group file
endpwent - be done using passwd file
getgrent - get next group record
getgrgid - get group record given group user ID
getgrnam - get group record given group name
getlogin - return who logged in at this tty
getpwent - get next passwd record
getpwnam - get passwd record given user login name
getpwuid - get passwd record given user ID
setgrent - prepare group file for use
setpwent - prepare passwd file for use
# 获取和设置网络信息
endhostent - be done using hosts file
endnetent - be done using networks file
endprotoent - be done using protocols file
endservent - be done using services file
gethostbyaddr - get host record given its address
gethostbyname - get host record given name
gethostent - get next hosts record
getnetbyaddr - get network record given its address
getnetbyname - get networks record given name
getnetent - get next networks record
getprotobyname - get protocol record given name
getprotobynumber - get protocol record numeric protocol
getprotoent - get next protocols record
getservbyname - get services record given its name
getservbyport - get services record given numeric port
getservent - get next services record
sethostent - prepare hosts file for use
setnetent - prepare networks file for use
setprotoent - prepare protocols file for use
setservent - prepare services file for use

从动作上分为3类:

  • getXXX:获取信息操作
  • setXXX:设置操作
  • endXXX:关闭操作
  • getXXXXent表示从对应文件中读取每一行并迭代,这个过程中间会隐式打开对应文件(所以隐式地维护了一个文件句柄)并使用指针指向每次读取完的位置,可以使用setXXXent来重置指针使其指向文件开头(也就是说从头开始迭代),使用endXXXent来关闭隐式打开的文件句柄(有些函数会通过网络解析得到结果,如DNS解析,endXXXent也会关闭这个网络连接从而终止解析过程)。

    这里共有18个函数。包括以下几个文件(以Linux操作系统为例):

  • /etc/group:{end | set | get}grent
  • /etc/passwd、/etc/shadow:{end | set | get}pwent
  • /etc/hosts:{end | set | get}hostent
  • /etc/networks:{end | set | get}netent
  • /etc/services:{end | set | get}servent
  • /etc/protocols:{end | set | get}protoent
  • 通过getXXXent迭代这些文件里的每一行,都得到一个XXXent结构体对象。例如/etc/group中的每一行对应一个grent对象,这一行中的各个字段是一个grent结构体的各个字段。

    这里6种结构体对象包含的字段如下:

    # 0        1          2           3         4
    ( $name,   $passwd,   $gid,       $members  ) = getgr*
    ( $name,   $aliases,  $addrtype,  $net      ) = getnet*
    ( $name,   $aliases,  $port,      $proto    ) = getserv*
    ( $name,   $aliases,  $proto                ) = getproto*
    ( $name,   $aliases,  $addrtype,  $length,  @addrs ) = gethost*
    ( $name,   $passwd,   $uid,       $gid,     $quota,
    $comment,  $gcos,     $dir,       $shell,   $expire ) = getpw*
    # 5        6          7           8         9
    

    这种结构体对象并非只能从这些文件中获取,自己也可以构建或通过其它函数返回,相关内容在后面介绍中会提到。

    除了上面的分类,用户、组还能继续分为:

  • getgrgid:根据给定的gid,搜索/etc/group中对应的组,并返回该组信息
  • getgrnam:根据给定的组name,搜索/etc/group中对应的组,并返回该组信息
  • getpwnam:根据给定的uid,搜索/etc/passwd、/etc/shadow中对应的用户,并返回该用户信息
  • getpwuid:根据给定的用户name,搜索/etc/passwd、/etc/shadow中对应的用户,并返回该用户信息
  • getlogin:返回当前登录的用户名,返回的依据是从/run/utmp或/etc/utmp搜索
  • 获取网络信息的操作还有:

  • gethostbyaddr:通过ipv4地址获取host信息(这里的host和dns解析有关,即对应于/etc/hosts)
  • gethostbyname:通过主机名获取host信息(这里的host和dns解析有关,即对应于/etc/hosts)
  • getnetbyaddr:获取网段信息(对应于/etc/networks文件)
  • getnetbyname:获取网段信息(对应于/etc/networks文件)
  • getservbyname:获取服务信息(对应于/etc/services文件)
  • getservbyport:获取服务信息(对应于/etc/services文件)
  • getprotobyname:获取协议信息(对应于/etc/protocols文件)
  • getprotobynumber:获取协议信息(对应于/etc/protocols文件)
  • 这些函数在标量上下文中返回单个name或id类数据,在列表上下文中返回各自的ent结构体对象。

    虽然看着一大堆,分类后其实很容易记忆。但是,这些函数都不是很方便,因为有不少函数里的参数或返回值是需要或经过了二进制打包的。比如gethostbyaddr函数的参数是一个ip地址,但不能直接传递"192.168.100.12"这样的地址,而是先使用pack()或使用Socket模块提供的inet_aton()将其转换成二进制格式,再传递给gethostbyaddr

    正是因为这些函数使用起来并不方便,所以每种类型(passwd、group、host、network、service、protocol)都有对应的面向对象的模块,使用这些模块中的方法,可以免去转换的过程。它们对应的模块为:

  • User::pwent
  • User::grent
  • Net::hostent
  • Net::netent
  • Net::servent
  • Net::protoent
  • 由于很多函数在用法上是非常类似的,所以在后文介绍重复内容时仅将简单说明重复函数的用法。

    获取用户和组信息

    相关的函数和模块有:

  • {get | set | end}pwent
  • {get | set | end}grent
  • getgrgid
  • getgrname
  • getpwuid
  • getpwnam
  • getlogin
  • User::pwent
  • User::grent
  • group信息

    首先解释getgrent以及grent结构对象,因为grent结构将group相关的东西全都串起来了。在后文也都按照这种方式介绍每一种分类。

    getgrent函数会遍历/etc/group文件,并从中读取每一行,每次遍历到一行都放进一个称为grent的结构对象中。前文列出过各种结构对象包含的字段,其中grent结构包括如下字段:

    # 0       1         2     3  
    ( $name,  $passwd,  $gid, $members ) = getgr*
    

    例如,/etc/group中的前三行为:

    $ head -n 3 /etc/group
    root:x:0:test1,test2,test3
    daemon:x:1:
    bin:x:2:
    

    使用getgrent()取前3行:

    #!/usr/bin/perl
    use strict;
    use warnings;
    use 5.010;
    my $i;
    while(my ($name, $passwd, $gid, $members) = getgrent) {
            $i++;
            last if $i == 4;
            say "group name: $name";
            say "group passwd: $passwd";
            say "group id: $gid";
            say "group member: $members";
            say "-" x 10;
    

    执行结果:

    group name: root
    group passwd: x
    group id: 0
    group member: test1 test2 test3
    ----------
    group name: daemon
    group passwd: x
    group id: 1
    group member:
    ----------
    group name: bin
    group passwd: x
    group id: 2
    group member:
    ----------
    

    前面说过,getXXXent()会隐式地打开对应的文件并维护一个对应的文件句柄,同时会使用指针标记好已读取到哪一行。可以使用setgrent()重置指针使其回到文件头部(也就是从头开始迭代),使用endgrent()关闭隐式维护的文件句柄,当然,再次调用getXXXent()会再次打开文件句柄。

    例如,读取两个组后,绕回到开头再读两个组,在这过程中打开的文件并没有关闭。然后使用endgrent关闭已打开的文件句柄。最后再次读取两个组,这会重新打开文件,直到程序退出后文件被关闭。

    #!/usr/bin/perl
    use strict;
    use warnings;
    use 5.010;
    say join ', ', getgrent;
    say join ', ', getgrent;
    setgrent;
    say join ', ', getgrent;
    say join ', ', getgrent;
    say '-' x 30;
    system 'lsof -n | grep "group"';
    endgrent;
    say '-' x 30;
    system 'lsof -n | grep "group"';
    say '-' x 30;
    say join ', ', getgrent;
    say join ', ', getgrent;
    

    执行结果:

    root, x, 0, test1 test2 test3
    bin, x, 1, 
    root, x, 0, test1 test2 test3
    bin, x, 1, 
    ------------------------------
    systemd   1      root    6r   DIR  0,21  0     1148 /sys/fs/cgroup/systemd
    perl      70014  root    3r   REG  8,2   721   35750228 /etc/group
    ------------------------------
    systemd   1      root    6r   DIR  0,21  0     1148 /sys/fs/cgroup/systemd
    ------------------------------
    root, x, 0, test1 test2 test3
    bin, x, 1,
    

    使用getgrnam()、getgrgid()也可以获取相关组信息。在标量上下文下,前者返回gid,后者返回group_name。在列表上下文,两者都返回grent结构:

    #!/usr/bin/perl
    use strict;
    use warnings;
    use 5.010;
    my $gr_gid = getgrnam 'root';
    say "gr_gid: $gr_gid";
    say '=' x 10, ' scalar context ', '=' x 10;
    my $gr_name = getgrgid 0;
    say "gr_name: $gr_name";
    say '=' x 10, ' list context ', '=' x 10;
    my ($name, $passwd, $gid, $members) = getgrnam 'root';
    say "name: $name";
    say "passwd: $passwd";
    say "gid: $gid";
    say "members: $members";
    

    执行结果:

    gr_gid: 0
    ========== scalar context ==========
    gr_name: root
    ========== list context ==========
    name: root
    passwd: x
    gid: 0
    members: test1 test2 test3
    

    user信息

    获取用户信息和组信息的方式是一样的,相关函数为getpwentsetpwentendpwentgetpwnamgetpwuid,唯一不一样的是pwent结构对象的字段和grent结构对象的字段不一样。

    # 0        1          2           3         4
    $name,     $passwd,   $uid,       $gid,     $quota,
    $comment,  $gcos,     $dir,       $shell,   $expire
    # 5        6          7           8         9
    

    需要注意的是,不同操作系统中支持的字段不一样。

    #!/usr/bin/perl
    use strict;
    use warnings;
    use 5.010;
    my $user_uid = getpwnam 'root';
    say "user_uid: $user_uid";
    say '=' x 10, ' scalar context ', '=' x 10;
    my $user_name = getpwuid 0;
    say "user_name: $user_name";
    say '=' x 10, ' list context ', '=' x 10;
    say join ', ', getpwnam 'root';
    
    user_uid: 0
    ========== scalar context ==========
    user_name: root
    ========== list context ==========
    root, 这里是root的密码, 0, 0, , , root, /root, /bin/bash
    

    当前登录用户

    getlogin()函数可以获取当前用户名。

    #!/usr/bin/perl
    use strict;
    use warnings;
    use 5.010;
    say getlogin;
    

    root用户执行结果:

    $ perl login.pl
    

    User::grent和User::pwent模块

    这两个模块提供了面向对象方式的操作。导入这两个模块,里面的函数默认会覆盖同名内置函数get{pw | gr}{ent | nam | gid},即Core模块中的这些函数,并且模块中的这些函数会返回对应的对象。

    # grent OO
    my $gr = getgrnam 'root' or die "no root group";
    # pwent OO
    my $pw = getpwuid 0 or die "no uid=0 user";
    

    通过这些对象,可以直接获取到给定字段的值,且在导入了:FIELDS标签后,可以直接使用变量来访问对应字段。例如:

    # grent OO
    方法             等价变量
    ------------------------
    $gr->name       $gr_name
    $gr->gid        $gr_gid
    $gr->passwd     $gr_passwd
    $gr->members    $gr_members
    # pwent OO
    方法             等价变量
    ------------------------
    $pw->name        $pw_name
    $pw->passwd      $pw_passwd
    $pw->uid         $pw_uid
    $pw->gid         $pw_gid
    $pw->quota       $pw_quota
    $pw->comment     $pw_comment
    $pw->gecos       $pw_gecos
    $pw->dir         $pw_dir
    $pw->shell       $pw_shell
    

    同时,还各自提供了getpw()和getgr()方法,这两个方法是getpwuid()、getpwnam()、getgrgid()、getgrnam()的多态方法,它根据参数类型判断调用哪个函数。例如给getpw()传递数值时,表示调用getpwuid(),传递字符串时表示调用getpwnam()。

    #!/usr/bin/perl
    use strict;
    use warnings;
    use 5.010;
    use User::pwent qw(:FIELDS);
    use User::grent qw(:FIELDS);
    my $gr = getgrnam 'root' or die 'no root group';
    my $pw = getpwuid 0 or die 'no uid=0 user';
    # grent OO
    say '=' x 10 , ' grent OO ', '=' x 10;
    say $gr->name;
    say $gr_name;
    say $gr->gid;
    say $gr_gid;
    say '=' x 10 , ' pwent OO ', '=' x 10;
    # pwent OO
    say $pw->name;
    say $pw->shell;
    say $pw_dir;
    

    执行结果:

    ========== grent OO ==========
    ========== pwent OO ==========
    /bin/bash
    /root
    

    地址解析:hosts信息

    经过前面grent和pwent的解释之后,再理解hosts、net、serv、proto就简单多了。

    hosts相关函数和模块:

  • gethostent
  • sethostent
  • endhostent
  • gethostbyaddr
  • gethostbyname
  • Net::hostent
  • gethostentgethostbyaddrgethostbyname用来给给定的主机名或地址进行DNS解析。它会读取/etc/hosts文件,也会从网络上做DNS解析。

    必要的基础知识

    在开始解释这些函数之前,先了解些必要基础知识。

    在/etc/hosts文件中包含了本地的DNS解析记录,例如:

    # IP_address canonical_hostname [aliases...]
    127.0.0.1   localhost localhost.localdomain
    ::1         localhost localhost.localdomain
    192.168.100.12 www.longshuai.com www1.longshuai.com
    

    其中第一列是IP地址,第二列是该IP地址对应的规范主机名(canonical_hostname),从第三列开始全都是主机别名(aliases),即DNS里的CNAME记录。

    如果将上面的www.longshuai.com换成DNS资源记录,区域数据文件中对应的配置如下:

    www.longshuai.com.    IN  A   192.168.100.12
    www1.longshuai.com.   IN  CNAME  www.longshuai.com.
    

    也就是说,当查询www.longshuai.com时,会得到其对应的IP地址192.168.100.12,当查询别名www1.longshuai.com时,先解析回规范主机名www.longshuai.com,再得到其对应的A记录,即192.168.100.12

    例如,使用host命令(或nslookup、dig命令)解析一下www.baidu.com相关的记录:

    $ host www.baidu.com
    www.baidu.com is an alias for www.a.shifen.com.
    www.a.shifen.com has address 183.232.231.172
    www.a.shifen.com has address 183.232.231.174
    

    结果说明了,www.baidu.com只是www.a.shifen.com的一个别名,这个主机名有两个A记录,IP地址分别是183.232.231.172183.232.231.174

    hostent结构

    和前面介绍的grent、pwent一样,hostent也是一个结构体对象,该结构体对象包含如下字段:

    # 0       1          2           3         4
    ( $name,  $aliases,  $addrtype,  $length,  @addrs )
    
  • $aliases中可能会包含多个别名,它是一个数组,但必须使用$aliases而非@aliases,否则后面几个返回值都被收集到@aliases中
  • $addrtype是地址类型,现在支持AF_INET和AF_INET6两种类型
  • $length是长度,ipv4地址长度都是4字节,所以值为4,ipv6是16字节
  • @addrs是以二进制方式打包的地址列表,无法直接输出,需要将每个地址元素都解包。解包的方式有两种:
  • 使用unpack():for (@addrs) { my ($a, $b, $c, $d) = unpack 'C4', $_ }
  • 使用Socket模块的inet_ntoa():for (@addrs){ my $addr = inet_ntoa($_) }
  • 看下面的例子就知道了。

    例如,下面使用gethostent()获取/etc/hosts中的每一个结构对象:

    #!/usr/bin/perl
    use strict;
    use warnings;
    use 5.010;
    while(my ($name, $aliases, $addrtype, $length, @addrs) = gethostent){
        say "name: $name";
        say "aliases: $aliases";
        say "addrtype: $addrtype";
        say "length: $length";
        say "addrs: @addrs";
        say '-' x  10;
    
    name: localhost
    aliases: localhost.localdomain localhost4 localhost4.localdomain4
    addrtype: 2
    length: 4
    addrs: 
    ----------
    name: localhost
    aliases: localhost.localdomain localhost6 localhost6.localdomain6
    addrtype: 2
    length: 4
    addrs: 
    ----------
    name: www.longshuai.com
    aliases: www1.longshuai.com
    addrtype: 2
    length: 4
    addrs: 
            ----------
    

    可见,第四个字段是无法直接输出的,需要将其解包。可以使用unpack()或Socket模块中的inet_ntoa():

    # 使用inet_ntoa()将二进制打包的地址转换成点分十进制的IP
    use Socket qw(/inet/);
    for (@addrs){
        say "addrs: ", inet_ntoa $_;
    # 使用unpack()解包
    for (@addrs){
        say "addrs: ", join '.', unpack 'C4', $_;
    

    再看看gethostbyname(),只要给一个主机名,就可以解析出别名以及相关A记录。在列表上下文它返回hostent结构对象,在标量上下文,它返回打包好的IP地址。

    #!/usr/bin/perl
    use strict;
    use warnings;
    use Socket qw(/inet/);
    use 5.010;
    # scalar context
    my $addr = gethostbyname $ARGV[0];
    say "addr: ", inet_ntoa $addr;
    # list context
    my ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname $ARGV[0];
    say "name: $name";
    say "aliases: $aliases";
    say "addrtype: $addrtype";
    say "length: $length";
    for (@addrs){
        say "addrs: ", inet_ntoa $_;
    
    $ perl host.pl www.baidu.com
    addr: 183.232.231.174
    name: www.baidu.com
    aliases: 
    addrtype: 2
    length: 4
    addrs: 183.232.231.174
    addrs: 183.232.231.172
    $ perl host.pl www.perl.org
    addr: 151.101.42.217
    name: dualstack.osff.map.fastly.net
    aliases: www.perl.org cdn-fastly.perl.org
    addrtype: 2
    length: 4
    addrs: 151.101.42.217
    

    如果使用gethostbyaddr(),那么其IP地址参数需要使用inet_aton()或unpack()进行打包转换。但注意,一般gethostbyaddr()只用于解析/etc/hosts文件中有的地址。

    #!/usr/bin/perl
    use strict;
    use warnings;
    use Socket qw(/inet/ AF_INET);
    use 5.010;
    # scalar context
    my $host = gethostbyaddr(inet_aton('192.168.100.21'), AF_INET);
    say "host: ", $host;
    # list context
    my ($name, $aliases, $addrtype, $length, @addrs) = gethostbyaddr(inet_aton('192.168.100.21'), AF_INET);
    say "name: $name";
    say "aliases: $aliases";
    say "addrtype: $addrtype";
    say "length: $length";
    for (@addrs){
        say "addrs: ", inet_ntoa $_;
    

    一个简单的DNS A记录解析程序

    在使用gethostbyname和gethostbyaddr的时候,解析之后一定要判断返回结果是否存在,即$name是否为undef。如下是一个简单的DNS A记录解析系统:

    #!/usr/bin/perl
    use strict;
    use warnings;
    use Socket qw(/inet/);
    use 5.010;
    die "Give me a hostname" unless @ARGV;
    while(my $lookup = shift @ARGV){
        my ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname $lookup;
        # must check $name defined or not
        if($name){
            foreach(@addrs){
                $_ = inet_ntoa($_);
            if ($name eq$lookup){
                print "$lookup IP address: @addrs\n";
            } else {
                print "$lookup (real name $name) IP address: @addrs\n";
        } else {
            print "host $lookup not found\n";
    
    $ perl host.pl www.sina.com
    www.sina.com (real name wwwus.sina.com) IP address: 66.102.251.33
    $ perl host.pl www.cnblogs.com www.baidu.com
    www.cnblogs.com IP address: 42.121.252.58
    www.baidu.com IP address: 183.232.231.174 183.232.231.172
    

    Net::hostent

    User::pwentUser::grent模块一样,Net::hostent提供了与内置函数gethostent、gethostbyname、gethostbyaddr同名的面向对象的方法,使得不需要中间繁琐的打包、解包过程。默认导入该模块时会覆盖这几个内置函数,如果想要引用这几个函数,可以加上CORE::前缀,如CORE::gethostbyname

    my $host = gethostbyname 'www.baidu.com';
    my $host = gethostbyaddr '192.168.100.21';
    

    同样的,这个模块提供了hostent各字段对应的方法和变量:

    $host->name       ==>   $h_name
    $host->addr       ==>   $h_addr
    $host->aliases    ==>   @h_aliases
    $host->addrtype   ==>   $h_addrtype
    $host->length     ==>   $h_length
    $host->addr_list  ==>   @h_addr_list
    

    此外,还提供了gethost()方法来替代gethostbyname()和gethostbyaddr(),它根据传递的参数类型来决定调用这两个方法中的哪一个。

    网段解析:networks信息

    相关函数和模块:

  • getnetent
  • setnetent
  • endnetent
  • getnetbyaddr
  • getnetbyname
  • Net::netent
  • 这个用的不多,因为它解析/etc/networks文件,而这个文件是定义网段(注意是网段而不是具体的IP地址)和网段字符串名一一映射关系的。而且,它指定指定A、B、C类网段(注意是网段而不是具体的IP地址),所以这个文件中网段总是以.0结尾的。

    例如,基本上所有操作系统上都有这么两行:

    default 0.0.0.0
    loopback 127.0.0.0
    

    这表示default代表的是0.0.0.0这个默认网段,loopback表示的是127.0.0.0网段。当有需要解析/etc/networks文件的程序(如route命令解析该文件,ip命令不解析该文件)需要用到网段时,就可以指定或显示该网段对应的名称,当然在查询的时候可以使用-n(一般是这个选项),来禁用名称到ID的解析映射。

    跳过,基本用不上。

    服务和协议:services和protocols

    /etc/services中记录了服务和端口/协议相关的信息。例如:

    # service-name port/protocol [aliases ...]  [# comment]
    tcpmux          1/tcp                       # TCP port service multiplexer
    tcpmux          1/udp                       # TCP port service multiplexer
    rje             5/tcp                       # Remote Job Entry
    rje             5/udp                       # Remote Job Entry
    echo            7/tcp
    echo            7/udp
    discard         9/tcp        sink null
    discard         9/udp        sink null
    $ grep ' 22/' /etc/services
    ssh             22/tcp                      # The Secure Shell (SSH) Protocol
    ssh             22/udp                      # The Secure Shell (SSH) Protocol
    ssh             22/sctp                     # SSH
    $ grep ' 3306/' /etc/services
    mysql           3306/tcp                    # MySQL
    mysql           3306/udp                    # MySQL
    

    /etc/protocols文件中保存了tcp/ip协议栈各种协议的信息,包括协议名、代号、别名等等。例如ip协议的代号是0,别名是IP,tcp协议的代号是6,别名是TCP。这个文件是不能改的,一改就导致发送的TCP/IP相关的数据包出错。

    这两个服务基本上不用管,除非要TCP/IP编程。

    作者:骏马金龙
    Linux运维交流群:921383787