上一篇简单介绍了Linux系统编程的一些概念知识,从本篇文章开始,从解释系统命令的功能入手,由浅入深,逐步讲解Linux系统编程。
建议学习者最好具有一定的C语言基础,了解数组、结构体、指针和链表的概念。
代码实验环境
操作系统:Ubuntu 18.04 LTS
编译器gcc版本:gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
学习目标
通过分析who指令,来学习Linux的读文件操作。
who指令介绍
Linux为多用户操作系统,有时候需要查看系统是否繁忙,某人是否正在使用系统等,可以使用who指令来查看Linux系统中活动用户的情况。
命令也是程序。Linux系统中,几乎所有的命令都是人为编写的程序。在Linux系统的中增加新的命令很简单,把可执行文件放到以下任意一个目录即可:/bin、/usr/bin、/usr/local/bin,这些目录存放着很多系统命令。
如果想知道谁在使用系统,输入who指令,输出如下:
$ who
user :0 2021-10-31 21:42 (:0)
test pts/1 2021-10-31 23:19 (192.168.0.104)
每一行代表一个已经登陆的用户,第一列是用户名,第二列是终端名,第三列是登陆时间,第四列是用户的登陆地址。
who指令详解
我们可以通过联机帮助指令man,来查看who的使用方法和详细解释。查看who的帮助可输入:
$ man who
Linux系统的联机帮助内容:
名字(NAME):命令的名字以及对这个命令的简短说明。
概要(SYNOPSIS):给出命令的用法说明,包括命令格式、参数和选项列表。方括号([OPTION])为可选项。选项为短线 - 加上abdHlmpqrstTu这些字母的任意组合,命令末尾还可以有一个文件参数或者给定两个参数。
描述(DESCRIPTION):关于指令的详细阐述。根据指令和平台的不同,描述的内容也不同。
选项(OPTIONS):给出命令行中每一个选项的说明。
作者(AUTHOR):命令的作者。
参阅(SEE ALSO):包含这个命令相关的其他主题。
who指令如何工作
向下翻阅 man who
指令看到的帮助信息,有以下信息
圈出的内容说明,如果who命令没有指定文件,通常用 /var/run/utmp
和/var/log/wtmp
作为选项文件。
/var/run/utmp
文件保存当前登陆系统的用户信息
/var/log/utmp
文件保存登陆过本系统的用户信息
who通过读取文件/var/run/utmp
获得当前系统登陆的用户信息。
utmp这个文件里保存的是结构体数组,数组元素是utmp类型的结构,可以utmp.h
中找到utmp类型的定义。文件utmp.h
存放在/usr/include
目录下。
文件**/usr/include/utmp.h
**部分内容如下(已删除无关代码):
#ifndef _UTMP_H
#define _UTMP_H 1
#include <features.h>
#include <sys/types.h>
__BEGIN_DECLS
/* Get system dependent values and data structures. */
#include <bits/utmp.h>
/* Compatibility names for the strings of the canonical file names. */
#define UTMP_FILE _PATH_UTMP
#define UTMP_FILENAME _PATH_UTMP
#define WTMP_FILE _PATH_WTMP
#define WTMP_FILENAME _PATH_WTMP
#endif /* Use misc. */
__END_DECLS
#endif /* utmp.h */
utmp的具体结构定义在 bits/utmp.h
文件中。如下:
#define EMPTY 0
#define BOOT_TIME 2
#define NEW_TIME 3
#define OLD_TIME 4
#define INIT_PROCESS 5
#define LOGIN_PROCESS 6
#define USER_PROCESS 7
#define DEAD_PROCESS 8
#define ACCOUNTING 9
#define UT_LINESIZE 32
#define UT_NAMESIZE 32
#define UT_HOSTSIZE 256
struct exit_status {
short int e_termination;
short int e_exit;
};
struct utmp {
short ut_type;
pid_t ut_pid;
char ut_line[UT_LINESIZE];
char ut_id[4];
char ut_user[UT_NAMESIZE];
struct exit_status ut_exit;
#if __WORDSIZE == 64 && defined __WORDSIZE_COMPAT32
int32_t ut_session;
struct {
int32_t tv_sec;
int32_t tv_usec;
} ut_tv;
#else
long ut_session;
struct timeval ut_tv;
#endif
int32_t ut_addr_v6[4];
char __unused[20];
};
/* 向后兼容定义 */
#define ut_name ut_user
#ifndef _NO_UT_TIME
#define ut_time ut_tv.tv_sec
#endif
#define ut_xtime ut_tv.tv_sec
#define ut_addr ut_addr_v6[0]
由以上分析可知,who通过读文件来获取需要的信息,而每个登陆的用户在文件中都有对应的记录。who的工作流程可以用下图表示:
/var/run/utmp
文件中的结构数组存放已登陆用户的信息,who指令的实现,是不是把记录一个一个地读出并显示出来呢?让我们继续分析。
实现who命令
编写who程序时,需要做两件事:
- 从文件(/var/run/utmp)中读取数据结构信息
- 以合适的形式将结构中的信息显示出来
第一步:读取信息
从某个文件中读取数据,Linux系统提供了三个系统函数:open()、read()、close()。
- open() —— 打开一个文件
open在Linux下的定义以及调用函数所需的头文件如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
函数第一个参数pathname,是要打开的文件的路径名或者文件名。
第二个参数flags,表示打开文件的操作模式(有3种):只读(O_RDONLY)、只写(O_WRONLY)、可读可写(O_RDWR),调用此函数时,必须指定其中一种。还有其他可选模式,暂不做介绍。
第三个参数mode,表示设置文件访问权限的初始值,和用户掩码umask有关。此文暂时不用这个参数。
打开文件时,如果操作成功,内核会返回一个正整数的值,这个数值叫做文件描述符。如果内核检测到任务错误,这个系统调用会返回-1。
要对一个文件进行操作(读或者写),必须先打开文件。文件打开成功后,可以通过文件描述符对文件进行操作。
- read() —— 从文件读取数据
read在Linux下的定义以及调用函数所需的头文件如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
函数第一参数fd,为文件描述符,由open函数返回。
第二个参数buf,存放读取数据的内存空间。
第三个参数count,希望读取的数据的个数。
如果读取成功,返回所读取数据的字节个数;否则,返回-1。
注意:最终读取的数据可能没有要求的多。例如,文件中剩余的数据少于要求读取的个数,则程序只能读取文件中剩余的数据个数。当读到文件末尾时,函数会返回0。
- close() —— 关闭文件
clsoe在Linux下的定义以及调用函数所需的头文件如下:
#include <unistd.h>
int close(int fd);
close 这个系统函数会关闭已经打开的文件,fd为open()函数打开文件返回的描述符。如果关闭出错,函数返回-1。关闭成功,则返回0。
对文件的操作完成后,需要关闭文件,以减少内存资源占用。
第二步:显示信息
通过printf函数利用定宽度的格式显示utmp记录信息。
第三步:代码实现
初步代码实现如下:
#include<stdio.h>
#include<utmp.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#define SHOWHOST
void show_info(struct utmp *utbufp);
int main()
{
struct utmp current_record;
int utmpfd;
int reclen = sizeof(current_record);
if((utmpfd = open(UTMP_FILE, O_RDONLY)) == -1)
{
perror(UTMP_FILE);
exit(1);
}
while(read(utmpfd, ¤t_record, reclen) == reclen)
{
show_info(¤t_record);
}
close(utmpfd);
return 0;
}
// 显示信息
void show_info(struct utmp *utbufp)
{
printf("%-8.8s", utbufp->ut_name);
printf(" ");
printf("%-8.8s", utbufp->ut_line);
printf("%10d", utbufp->ut_time);
printf(" ");
#ifdef SHOWHOST
printf("(%s)", utbufp->ut_host);
#endif
printf("\n");
}
编译
$gcc who1.c -o who1
运行结果如下
$./who1
reboot ~ 1635728912 (4.15.0-161-generic)runlevel ~ 1635729058 (4.15.0-161-generic)
user :0 1635729148 (:0)
test pts/2 1635763291 (192.168.0.104)
将上述输出结果与系统who命令输出做对比:
$ who
user :0 2021-11-01 09:12 (:0)
test pts/2 2021-11-01 18:41 (192.168.0.104)
自己编写的who已经可以工作了,可以显示用户名、终端名、远程主机名。但是,根系统的who相比较还不完善。存在两处内容需要改进:
(1)消除空白记录
(2)正确显示登陆时间
程序代码优化
- 消除空白记录
系统who命令只列出已登陆用户的信息。而我们编写的代码,除了列出已登录的用户,还会显示utmp文件中的其他信息。实际上utmp包含所有终端的信息,那些尚未用到的终端信息也会存放在utmp中。
utmp结构中有一个成员ut_type
,当它的值为7(USER_PROCESS)时,表示这是一个已经登陆的用户。据此,对原来的程序显示信息函数 show_info() 函数开头添用户类型判断,即可消除空白记录:
if(utbufp->ut_type != USER_PROCESS)
{
return;
}
- 使得显示登陆时间可读
Linux中的时间是用一个整数来表示的,类型为 time_t ,它的数值是从1970年1月1日0时开始经过的秒数。存储时间的结构 time_t 实际上就是 long int 。类型 time_t 定义为
typedef long int time_t;
需要将时间的整数值转换为易于理解的形式。实验环境系统中who指令显示的时间格式如下
2021-10-31 23:19
我们需要由存储的时间的秒数值得到:年、月、日、时、分等信息。即需要将Linux存储的时间秒数转换为分解时间。分解时间存储结构类型为tm
,其结构定义如下
struct tm
{
int tm_sec; /* 秒 (0-59) */
int tm_min; /* 分 (0-59) */
int tm_hour; /* 小时 (0-23) */
int tm_mday; /* 一个月中第几天 (1-31) */
int tm_mon; /* 月份 (0-11) */
int tm_year; /* 自 1900 年起的年数 */ int tm_wday; /* 一周中第几天 (0-6, Sunday = 0) */
int tm_yday; /* 一年中第几天 (0-365, 1 Jan = 0) */
int tm_isdst; /* 夏令时 */
};
用localtime()
函数将时间秒数转换为分解时间,并用本地时区表示,其定义如下
#include <time.h>
struct tm *localtime(const time_t *timep);
函数的参数为一个指向 time_t 的指针,返回一个指向 tm 结构的指针。
- 代码优化
综合以上两点对代码进行优化。
优化信息显示函数show_info
如下:
void show_info(struct utmp *utbufp)
{
if(utbufp->ut_type != USER_PROCESS)
{
return;
}
printf("%-8.8s", utbufp->ut_name);
printf(" ");
printf("%-8.8s", utbufp->ut_line);
show_time(utbufp->ut_time); printf(" ");
#ifdef SHOWHOST
printf("(%s)", utbufp->ut_host);
#endif
printf("\n");
}
添加时间显示函数show_time
如下
void show_time(time_t timeval)
{
struct tm *info = NULL;
info = localtime(&timeval);
printf("%4d-%2d-%02d %02d:%02d", (info->tm_year + 1900), (info->tm_mon + 1), \
info->tm_mday, info->tm_hour, info->tm_min);
}
编译后,运行结果如下
$./who2
user :0 2021-11-01 09:12 (:0)
user pts/2 2021-11-01 18:41 (192.168.0.104)
显示的结果与系统的who命令对比,显示结果基本一致。
小结
本篇文章介绍了Linux系统中who命令的工作原理,并通过自己实现who指令,来学习Linux编程对文件的读操作。并学习了登陆信息utmp文件结构,学习了Linux的时间处理。
涉及到的系统函数:open、read、close、localtime
相关指令:man、who
后续
接下来学习Linux文件操作之写文件操作。
————————————————————————————
公众号【一起学嵌入式】,一起学习、一起成长