这里主要介绍/proc伪文件系统及uname()函数来获取系统或进程的一些信息。


/proc文件系统介绍

在早期的UNIX发行版中,并不能很容易的分析内核的一些属性,并且很难回答以下问题:

  1. 系统有多少进程正在运行,并且谁拥有这些进程?

  2. 一个进程都打开了哪些文件?

  3. 哪些文件目前被锁住了,并且哪些进程拥有这些文件锁?

  4. 系统有哪些套接字正在使用?


一些早期的UNIX发行版解决该问题是允许有权限的程序进入内核的内存空间的数据结构中。这样的解决办法有诸多不便。然而,最蛋疼的是它需要程序员了解的内核中的数据结构,并且这些数据结构在不同的内核版本中会略有不同,需要程序员根据实际所依赖的内核进行代码的重写。


为了提供便捷的访问内核信息,很多现代的UNIX发行版提供了/proc虚拟文件系统。该文件系统驻留在/proc目录下,该目录下有很多文件以暴露内核信息,方便进程来从中读取信息,并在某种情形下还可以使用系统的I/O调用来修改这些信息。说/proc文件系统是虚的,主要是因为它下面的文件及子目录并不占用硬盘空间。相反是内核在进程访问这些信息的时候创建的。


获取某个进程的一些信息

对于每一个系统中进程来说,内核提供了一个在/proc中相应的目录,该目录一般是进程的PID。在/proc/PID目录中有一些文件及子目录等,这些文件及目录都包含了该进程的相关信息。在Gun/Linux系统中,我们最熟悉的1号进程就是init进程,与其相关的进程信息在/proc/1目录下。


在/proc/1目录下,有一个名为status的文件,status文件包含了关于该进程的一些详尽信息,接下来我们可以看看里面是什么东东,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[root@lavenliu ~]# cat /proc/1/status 
Name:   init
State:  S (sleeping)
Tgid:   1
Pid:    1
PPid:   0
TracerPid:  0
Uid:    0   0   0   0
Gid:    0   0   0   0
Utrace: 0
FDSize: 64
Groups:
VmPeak:    19296 kB
VmSize:    19232 kB
VmLck:         0 kB
VmHWM:      1588 kB
VmRSS:      1588 kB
VmData:      200 kB
VmStk:        88 kB
VmExe:       140 kB
VmLib:      2348 kB
VmPTE:        56 kB
VmSwap:        0 kB
Threads:    1
SigQ:   0/3878
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000001000
SigCgt: 00000001a0016623
CapInh: 0000000000000000
CapPrm: ffffffffffffffff
CapEff: fffffffffffffeff
CapBnd: ffffffffffffffff
Cpus_allowed:   3
Cpus_allowed_list:  0-1
Mems_allowed:   00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list:  0
voluntary_ctxt_switches:    577
nonvoluntary_ctxt_switches: 69


下面列出/proc/PID目录下的一些其他文件,如下:


文件 说明
cmdline 以'\0'结尾的命令行参数
cwd 指向当前工作目录的软连接
environ 环境变量列表,形如:NAME=value形式
exe 执行进程可执行文件的软连接
fd
该目录包含进程已打开的文件描述符,软链接至打开的文件
maps 内存映射
mem 进程的虚拟内存
mounts 进程使用的挂载点
root 软链接至根目录
status 进程的一些信息(如:进程ID、内存使用情况、信号等)
task 该目录下是进程的线程目录,每个线程一个子目录


/proc/PID/fd目录介绍

/proc/PID/fd目录中包含了该PID进程打开的每一个文件描述符的软链接。每一个软链接的名字都与该进程打开的文件描述符是一致的。举例来说,/proc/1990/1这个软链接指向该1990进程的标准输出。


任何一个进程都很方便地访问它自己的/proc/PID目录通过使用/proc/self软链接。


线程:/proc/PID/task目录

Linux内核2.4版本增加了线程组的概念以符合POSIX标准的线程模型。由于线程组里的线程的某些线程属性不同,所以Linux2.4内核版本在/proc/PID目录中增加了task子目录。对于进程的每个线程都对应一个目录,每个目录都是以线程ID命名的,形如/proc/PID/task/TID形式,TID为进程的一个线程。比如我们看一下PID为1287的进程的线程,它有如下的线程在运行,这些目录名称就是1287进程的线程ID(可以使用gettid()来获取线程的TID)

1
2
3
ls /proc/1287/task
1287  1297  1343  1528  1530  1680  1695  1776  1778  1863
1296  1342  1527  1529  1679  1694  1775  1777  1862

在每一个/proc/PID/task/TID子目录下也是一系列的文件和目录,与/proc/PID目录下的文件看起来很像。由于线程间共享了很多属性,这些目录下的文件所包含的信息是一样的。既然这些线程间都共享大量的相同属性,究竟是什么然它们得以区别于其他线程呢?在/proc/PID/task/TID/status文件中包含的信息可以区分于该线程组中的其他线程,这些信息有State,Pid,SigPnd,SigBlk,CapInh,CapPrm,CapEff和CapBnd等信息来区分于其他线程。如,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
[root@mydevops task]# cat 1287/status 
Name:   salt-master
State:  S (sleeping)
Tgid:   1287
Pid:    1287
PPid:   1282
TracerPid:  0
Uid:    0   0   0   0
Gid:    0   0   0   0
Utrace: 0
FDSize: 128
Groups:
VmPeak:  1140892 kB
VmSize:  1075356 kB
VmLck:         0 kB
VmHWM:     26800 kB
VmRSS:     26800 kB
VmData:   797808 kB
VmStk:       260 kB
VmExe:         4 kB
VmLib:     11504 kB
VmPTE:       520 kB
VmSwap:        0 kB
Threads:    19
SigQ:   0/3878
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000001001000
SigCgt: 0000000180000a02
CapInh: 0000000000000000
CapPrm: ffffffffffffffff
CapEff: ffffffffffffffff
CapBnd: ffffffffffffffff
Cpus_allowed:   3
Cpus_allowed_list:  0-1
Mems_allowed:   00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list:  0
voluntary_ctxt_switches:    437
nonvoluntary_ctxt_switches: 256
[root@mydevops task]# cat 1297/status 
Name:   salt-master
State:  S (sleeping)
Tgid:   1287
Pid:    1297
PPid:   1282
TracerPid:  0
Uid:    0   0   0   0
Gid:    0   0   0   0
Utrace: 0
FDSize: 128
Groups:
VmPeak:  1140892 kB
VmSize:  1075356 kB
VmLck:         0 kB
VmHWM:     26800 kB
VmRSS:     26800 kB
VmData:   797808 kB
VmStk:       260 kB
VmExe:         4 kB
VmLib:     11504 kB
VmPTE:       520 kB
VmSwap:        0 kB
Threads:    19
SigQ:   0/3878
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: fffffffe7ffbfeff
SigIgn: 0000000001001000
SigCgt: 0000000180000a02
CapInh: 0000000000000000
CapPrm: ffffffffffffffff
CapEff: ffffffffffffffff
CapBnd: ffffffffffffffff
Cpus_allowed:   3
Cpus_allowed_list:  0-1
Mems_allowed:   00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list:  0
voluntary_ctxt_switches:    49
nonvoluntary_ctxt_switches: 7


/proc目录下的系统信息

在/proc目录下有一些文件及目录用来提供操作系统级别的信息。这些文件及目录如下表所示:

目录 提供的信息
/proc 一些系统信息
/proc/net 网络及套接字的状态信息
/proc/sys/fs 与文件系统相关的设置
/proc/sys/kernel 与内核相关的一些设置
/proc/sys/net 网络与套接字相关的设置
/proc/sys/vm 内存管理相关的设置
/proc/sysvipc SystemV IPC相关的信息


访问/proc下面的文件

对于/proc目录下面的很多文件,我们可以通过shell脚本来轻松地访问(也可以使用Python与Perl等脚本语言来访问之),如

1
2
3
4
5
6
[root@mydevops ~]# cat /proc/sys/kernel/pid_max 
32768
[root@mydevops ~]# echo 100000 > /proc/sys/kernel/pid_max 
[root@mydevops ~]# cat /proc/sys/kernel/pid_max 
100000
[root@mydevops ~]#

/proc下的文件同样可以被程序通过使用一般的I/O系统调用来访问。程序使用I/O系统调用的方式访问时有如下的限制:

  1. /proc下的一些文件是只读的;那就是说它们的存在只是显示内核的信息,并不能用来修改内核的信息。这些限制同样适用于/proc/PID目录下的文件。

  2. /proc下的一些文件对文件的所有者是可以只读的(或者是一个已授权的进程)。例如,/proc/PID目录下的所有文件通常是拥有该进程的用户所拥有,并且这些文件(如/proc/PID/environ)通常是授权只读权限给文件的所有者。

  3. 除了/proc/PID子目录下的文件,/proc下的大部分文件的拥有者是root用户,通常这些文件有的是只可以通过root用户来修改的。


访问/proc/PID目录下的文件

/proc/PID目录下的文件是经常变动的。当一个进程启动时,相应的在/proc目录下会创建以该进程ID的目录;又当该进程终止时,该/proc/PID目录就会消失。


接下来,我们写一个简单的程序,来更新/proc/sys/kernel/pid_max文件,该程序需要从命令行中读取一个参数,并更新原有的数值;如果不提供参数,则获取原先的数值。程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# cat mypid_max.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
 
#define MAX_LINE 100
 
int main(int argc, char *argv[])
{
    int     fd;
    char    line[MAX_LINE];
    ssize_t n;
 
    fd = open("/proc/sys/kernel/pid_max", (argc > 1) ? O_RDWR : O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(1);
    }
     
    n = read(fd, line, MAX_LINE);
    if (n == -1) {
        perror("read");
        exit(1);
    }
 
    if (argc > 1) {
        printf("Old value: ");
    }
    printf("%.*s", (int) n, line);
 
    if (argc > 1) {
        if (lseek(fd, 0, SEEK_SET) == -1) {
            perror("lseek");
            exit(1);
        }
 
        if (write(fd, argv[1], strlen(argv[1])) != strlen(argv[1])) {
            perror("write() failed");
            exit(1);
        }
 
        system("echo /proc/sys/kernel/pid_max now contains "
               "`cat /proc/sys/kernel/pid_max`");
    }
 
    exit(EXIT_SUCCESS);
}


编译并运行:

1
2
3
4
5
6
7
8
gcc -o mypid_max mypid_max.c
[root@mydevops sysinfo]# ./mypid_max 
100000
[root@mydevops sysinfo]# ./mypid_max 1024
Old value: 100000
/proc/sys/kernel/pid_max now contains 1024
[root@mydevops sysinfo]# ./mypid_max 
1024


系统鉴别:uname()

uname()系统调用通常会返回一些系统相关的信息。使用的结构体为utsbuf。函数原型为:

1
2
3
#include <sys/utsname.h>
int uname(struct utsname *utsbuf);
                                        Returns 0 on success, or –1 on error

utsbuf参数是一个指向utsname结构体的指针,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
#define _UTSNAME_LENGTH 65
struct utsname {
    char sysname[_UTSNAME_LENGTH]; /* Implementation name */
    char nodename[_UTSNAME_LENGTH]; /* Node name on network */
    char release[_UTSNAME_LENGTH]; /* Implementation release level */
    char version[_UTSNAME_LENGTH]; /* Release version level */
    char machine[_UTSNAME_LENGTH]; /* Hardware on which system
                      is running */
#ifdef _GNU_SOURCE /* Following is Linux-specific */
    char domainname[_UTSNAME_LENGTH]; /* NIS domain name of host */
#endif
};


在CentOS6.5 64位系统上,可以在/proc/sys/kernel目录中找到这些相关的信息。可以通过访问/proc/sys/kernel/hostname,/proc/sys/kernel/osrelease,/proc/sys/kernel/version这三个文件来获取这些信息。这些信息都是只读的,我们可以看看这几个文件的内容:

1
2
3
4
5
6
[root@mydevops sysinfo]# cat /proc/sys/kernel/hostname 
mydevops
[root@mydevops sysinfo]# cat /proc/sys/kernel/osrelease 
2.6.32-573.18.1.el6.x86_64
[root@mydevops sysinfo]# cat /proc/sys/kernel/version 
#1 SMP Tue Feb 9 22:46:17 UTC 2016


另外一个文件/proc/version包含了上面三个文件的所有信息,并且还有一些关于内核编译相关的信息(诸如:是谁进行的内核编译工作,使用的是什么机器进行编译的,使用的GCC版本是多少等附加信息)。我们可以看看/proc/version文件的内容:

1
2
[root@mydevops sysinfo]# cat /proc/version 
Linux version 2.6.32-573.18.1.el6.x86_64 (mockbuild@c6b8.bsys.dev.centos.org) (gcc version 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC) ) #1 SMP Tue Feb 9 22:46:17 UTC 2016


utsname结构体中sysname,release,version与machine字段由内核自动设置。接下来看看如何使用uname()函数的使用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[root@mydevops sysinfo]# cat my_uname.c 
#ifdef __linux__
#define _GNU_SOURCE
#endif
 
#include <stdio.h>
#include <stdlib.h>
#include <sys/utsname.h>
 
int main(int argc, char *argv[])
{
    struct utsname uts;
 
    if (uname(&uts) == -1) {
        perror("uname");
        exit(1);
    }
 
    printf("Node name:   %s\n", uts.nodename);
    printf("System name: %s\n", uts.sysname);
    printf("Release:     %s\n", uts.release);
    printf("Version      %s\n", uts.version);
    printf("Machine:     %s\n", uts.machine);
#ifdef _GNU_SOURCE
    printf("Domain name: %s\n", uts.domainname);
#endif
     
    return 0;
}


编译并运行该程序,如下:

1
2
3
4
5
6
7
8
[root@mydevops sysinfo]# gcc -o my_uname my_uname.c 
[root@mydevops sysinfo]# ./my_uname 
Node name:   mydevops
System name: Linux
Release:     2.6.32-573.18.1.el6.x86_64
Version      #1 SMP Tue Feb 9 22:46:17 UTC 2016
Machine:     x86_64
Domain name: (none)

nodename字段是使用sethostname()系统调用作为其返回值的。domainname字段是使用setdomainname()系统调用作为其返回值的,它是该主机的NIS(Network Information Services)域名。


我们可以使用hostname及domainname进行设置我们的主机名及NIS域名,不过我们很少在程序中使用sethostname()及setdomainname()系统调用。一般地,hostname及NIS domain name是在系统启动时由启动脚本进行设置的。


想要查看更多的关于proc相关的信息,我们查看man page的第5章节的proc帮助信息,如下即可:

1
man 5 proc


练习:

  1. 获取系统用户的进程PID及进程名称。该程序需要一个命令行参数,该参数是/etc/passwd中的用户名(如root,mysql等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
cat my_procfs_user_exe.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <ctype.h>
#include <limits.h>
#include <sys/types.h>
#include <pwd.h>
#include <errno.h>
 
#define MAX_LINE 1000
typedef enum {FALSE, TRUE} Boolean;
 
 
char *username_from_id(uid_t uid)
{
    struct passwd *pwd;
 
    pwd = getpwuid(uid);
    return (pwd == NULL) ? NULL : pwd->pw_name;
}
 
uid_t userid_from_name(const char *name)
{
    struct passwd  *pwd;
    uid_t           u;
    char           *endptr;
 
    if (name == NULL || *name == '\0') {
        return -1;
    }
 
    u = strtol(name, &endptr, 10);
    if (*endptr == '\0') {
        return u;
    }
 
    pwd = getpwnam(name);
    if (pwd == NULL) {
        return -1;
    }
 
    return pwd->pw_uid;
}
 
int main(int argc, char *argv[])
{
    DIR            *dirp;
    struct dirent  *dp;
    char            path[PATH_MAX];
    char            line[MAX_LINE], cmd[MAX_LINE];
    FILE           *fp;
    char           *p;
    uid_t           uid, checked_uid;
    Boolean         got_name, got_uid;
     
 
    if (argc < 2 || strcmp(argv[1], "--help") == 0) {
        printf("%s <username>\n", argv[0]);
        exit(1);
    }
 
    checked_uid = userid_from_name(argv[1]);
    if (checked_uid == -1) {
        printf("Bad username: %s\n", argv[1]);
        exit(1);
    }
 
    dirp = opendir("/proc");
    if (dirp == NULL) {
        perror("opendir");
        exit(1);
    }
 
    /* scan entries unser /proc directory */
    for ( ;; ) {
        errno = 0;
        dp = readdir(dirp);
        if (dp == NULL) {
            if (errno != 0) {
                perror("readdir");
                exit(1);
            else {
                break;
            }
        }
 
        /* since we are looking for /proc/PID directories, skip
           entries that are not directories, or don't begin with a
           digit*/
        if (dp->d_type != DT_DIR || !isdigit((unsigned char) dp->d_name[0])) {
            continue;
        }
 
        snprintf(path, PATH_MAX, "/proc/%s/status", dp->d_name);
        fp = fopen(path, "r");
        if (fp == NULL) {
            continue;
        }
 
        got_name = FALSE;
        got_uid = FALSE;
        while (!got_name || !got_uid) {
            if (fgets(line, MAX_LINE, fp) == NULL) {
                break;
            }
 
            /* The "Name:" line contains the name of the command that
               this process is running */
            if (strncmp(line, "Name:", 5) == 0) {
                for (p = line + 5; *p != '\0' && isspace((unsigned char) *p); ) {
                    p++;
                }
                strncpy(cmd, p, MAX_LINE - 1);
                cmd[MAX_LINE -1] = '\0';        /* Ensure null-terminated */
 
                got_name = TRUE;
            }
 
            /* The "Uid:" line contains the real, effective, saved set-,
               and file-system user IDs */
 
            if (strncmp(line, "Uid:", 4) == 0) {
                uid = strtol(line + 4, NULL, 10);
                got_uid = TRUE;
            }
        }
        fclose(fp);
 
        /* If we found a username and a UID, and the UID matches,
           then display the PID and command name */
 
        if (got_name && got_uid && uid == checked_uid)
            printf("%5s %s", dp->d_name, cmd);
    }
 
    return 0;
}

编译并运行,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@mydevops sysinfo]# ./my_procfs_user_exe mysql
 1208 mysqld
[root@mydevops sysinfo]# ./my_procfs_user_exe nginx
[root@mydevops sysinfo]# ./my_procfs_user_exe daemon
[root@mydevops sysinfo]# ./my_procfs_user_exe apache
 3907 httpd
 3908 httpd
 3909 httpd
 3910 httpd
 3911 httpd
 3912 httpd
 3913 httpd
 3914 httpd
 3915 httpd