mirror of
https://github.com/DragonOS-Community/DragonOS.git
synced 2025-06-08 14:16:47 +00:00
实现内核日志系统 (#489)
* 实现写日志和读取日志,并且能够在用户态下执行dmesg命令查看日志 * 通过klogctl实现dmesg * 改用ConstGenericRingBuffer作内核缓冲区 * 更改缓冲区容量 * 将能够输出到控制台的日志级别改为日志级别枚举类,使用SpinLock控制KMSG,使用枚举类定义SYSLOG_ACTION,将do_syslog系统调用接口放在syscall.rs * fix warning * 完善do_syslog注释 * 将KMSG接入kinfo、kdebug等 * fix warning * 修复显示的秒数不正确,·以及无法通过CI的问题
This commit is contained in:
parent
d46c6d2794
commit
8d72b68da9
6
.github/actions/import-toolchain/action.yml
vendored
6
.github/actions/import-toolchain/action.yml
vendored
@ -15,7 +15,8 @@ runs:
|
||||
with:
|
||||
path: |
|
||||
~/opt
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('tools/build_gcc_toolchain.sh') }}-${{ hashFiles('tools/install_musl_gcc.sh') }}
|
||||
~/.bashrc
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('tools/build_gcc_toolchain.sh') }}
|
||||
|
||||
- name: Cache build tools
|
||||
id: cache-build-tools
|
||||
@ -28,7 +29,8 @@ runs:
|
||||
~/.cargo
|
||||
~/.rustup
|
||||
~/.bashrc
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ env.dadk_version }}-${{ hashFiles('.github/workflows/cache-toolchain.yml') }}
|
||||
~/opt
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ env.dadk_version }}-${{ hashFiles('.github/workflows/cache-toolchain.yml') }}-${{ hashFiles('tools/install_musl_gcc.sh') }}
|
||||
|
||||
- uses: ./.github/actions/install-apt-packages
|
||||
|
||||
|
10
.github/workflows/cache-toolchain.yml
vendored
10
.github/workflows/cache-toolchain.yml
vendored
@ -18,14 +18,14 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/opt
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('tools/build_gcc_toolchain.sh') }}-${{ hashFiles('tools/install_musl_gcc.sh') }}
|
||||
~/.bashrc
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('tools/build_gcc_toolchain.sh') }}
|
||||
|
||||
- if: ${{ steps.cache-dragonos-gcc.outputs.cache-hit != 'true' }}
|
||||
name: build dragonos-gcc
|
||||
continue-on-error: true
|
||||
run: |
|
||||
bash tools/build_gcc_toolchain.sh -f
|
||||
bash tools/install_musl_gcc.sh
|
||||
|
||||
- uses: ./.github/actions/install-apt-packages
|
||||
|
||||
@ -40,13 +40,15 @@ jobs:
|
||||
~/.cargo
|
||||
~/.rustup
|
||||
~/.bashrc
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ env.dadk_version }}-${{ hashFiles('.github/workflows/cache-toolchain.yml') }}
|
||||
~/opt
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ env.dadk_version }}-${{ hashFiles('.github/workflows/cache-toolchain.yml') }}-${{ hashFiles('tools/install_musl_gcc.sh') }}
|
||||
|
||||
- if: ${{ steps.cache-build-tools.outputs.cache-hit != 'true' }}
|
||||
name: Install toolchain
|
||||
continue-on-error: false
|
||||
run: |
|
||||
|
||||
USE_GITHUB=1 bash tools/install_musl_gcc.sh
|
||||
|
||||
cargo install cargo-binutils
|
||||
rustup toolchain install nightly-x86_64-unknown-linux-gnu
|
||||
rustup toolchain install nightly-2023-01-21-x86_64-unknown-linux-gnu
|
||||
|
15
.github/workflows/makefile.yml
vendored
15
.github/workflows/makefile.yml
vendored
@ -2,7 +2,7 @@ name: Build Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "patch-add-riscv64-github-workflow" ]
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
@ -50,6 +50,7 @@ jobs:
|
||||
- uses: ./.github/actions/import-toolchain
|
||||
|
||||
- name: Run kernel static test
|
||||
shell: bash -ileo pipefail {0}
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
run: bash -c "source ~/.cargo/env && cd kernel && make test"
|
||||
@ -68,7 +69,14 @@ jobs:
|
||||
- name: build the DragonOS
|
||||
env:
|
||||
ARCH: x86_64
|
||||
run: bash -c "source ~/.cargo/env && export DragonOS_GCC=$HOME/opt/dragonos-gcc/gcc-x86_64-unknown-none/bin && make -j $(nproc) "
|
||||
shell: bash -ileo pipefail {0}
|
||||
|
||||
run: |
|
||||
source ~/.bashrc
|
||||
source ~/.cargo/env
|
||||
export DragonOS_GCC=$HOME/opt/dragonos-gcc/gcc-x86_64-unknown-none/bin
|
||||
|
||||
make -j $(nproc)
|
||||
|
||||
|
||||
build-riscv64:
|
||||
@ -84,8 +92,9 @@ jobs:
|
||||
- uses: ./.github/actions/import-toolchain
|
||||
|
||||
- name: build the DragonOS
|
||||
shell: bash -ileo pipefail {0}
|
||||
env:
|
||||
ARCH: riscv64
|
||||
|
||||
run: bash -c "source ~/.cargo/env && make kernel -j $(nproc)"
|
||||
run: source ~/.bashrc && source ~/.cargo/env && make kernel -j $(nproc)
|
||||
|
||||
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@ -172,7 +172,13 @@
|
||||
"clocksource.h": "c",
|
||||
"ata.h": "c",
|
||||
"barrier": "c",
|
||||
"charconv": "c"
|
||||
"charconv": "c",
|
||||
"printf.h": "c",
|
||||
"klog.h": "c",
|
||||
"sqlite3ext.h": "c",
|
||||
"malloc.h": "c",
|
||||
"*.o": "c",
|
||||
"k_log.h": "c"
|
||||
},
|
||||
"C_Cpp.errorSquiggles": "enabled",
|
||||
"esbonio.sphinx.confDir": "",
|
||||
|
@ -7,6 +7,7 @@ description = "需要导出的依赖项(为保持内核依赖版本与调试
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ringbuffer = "0.15.0"
|
||||
memoffset = "0.9.0"
|
||||
crc = { path = "../crc" }
|
||||
|
||||
|
@ -6,4 +6,6 @@ pub extern crate thingbuf;
|
||||
|
||||
pub extern crate memoffset;
|
||||
|
||||
pub extern crate ringbuffer;
|
||||
|
||||
pub extern crate crc;
|
||||
|
@ -491,7 +491,7 @@ impl SignalArch for X86_64SignalArch {
|
||||
"Error occurred when handling signal: {}, pid={:?}, errcode={:?}",
|
||||
sig_number as i32,
|
||||
ProcessManager::current_pcb().pid(),
|
||||
res.unwrap_err()
|
||||
res.as_ref().unwrap_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ use x86::time::rdtsc;
|
||||
use x86_64::registers::model_specific::EferFlags;
|
||||
|
||||
use crate::driver::tty::serial::serial8250::send_to_default_serial8250_port;
|
||||
use crate::filesystem::procfs::kmsg::kmsg_init;
|
||||
use crate::include::bindings::bindings::{
|
||||
multiboot2_get_load_base, multiboot2_get_memory, multiboot2_iter, multiboot_mmap_entry_t,
|
||||
multiboot_tag_load_base_addr_t,
|
||||
@ -419,6 +420,8 @@ pub fn mm_init() {
|
||||
unsafe { allocator_init() };
|
||||
// enable mmio
|
||||
mmio_init();
|
||||
// enable KMSG
|
||||
kmsg_init();
|
||||
}
|
||||
|
||||
unsafe fn allocator_init() {
|
||||
|
@ -594,8 +594,12 @@ pub extern "C" fn rs_e1000e_init() {
|
||||
|
||||
pub fn e1000e_init() -> () {
|
||||
match e1000e_probe() {
|
||||
Ok(_code) => kinfo!("Successfully init e1000e device!"),
|
||||
Err(_error) => kinfo!("Error occurred!"),
|
||||
Ok(_code) => {
|
||||
kinfo!("Successfully init e1000e device!");
|
||||
}
|
||||
Err(_error) => {
|
||||
kinfo!("Error occurred!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1196,7 +1196,7 @@ impl Drop for FATFileSystem {
|
||||
if r.is_err() {
|
||||
kerror!(
|
||||
"Umount FAT filesystem failed: errno={:?}, FS detail:{self:?}",
|
||||
r.unwrap_err()
|
||||
r.as_ref().unwrap_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
151
kernel/src/filesystem/procfs/kmsg.rs
Normal file
151
kernel/src/filesystem/procfs/kmsg.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use super::log::{LogLevel, LogMessage};
|
||||
|
||||
use crate::libs::spinlock::SpinLock;
|
||||
|
||||
use alloc::{borrow::ToOwned, string::ToString, vec::Vec};
|
||||
|
||||
use kdepends::ringbuffer::{AllocRingBuffer, RingBuffer};
|
||||
|
||||
use system_error::SystemError;
|
||||
|
||||
/// 缓冲区容量
|
||||
const KMSG_BUFFER_CAPACITY: usize = 1024;
|
||||
|
||||
/// 全局环形缓冲区
|
||||
pub static mut KMSG: Option<SpinLock<Kmsg>> = None;
|
||||
|
||||
/// 初始化KMSG
|
||||
pub fn kmsg_init() {
|
||||
let kmsg = SpinLock::new(Kmsg::new());
|
||||
unsafe { KMSG = Some(kmsg) };
|
||||
}
|
||||
|
||||
/// 日志
|
||||
pub struct Kmsg {
|
||||
/// 环形缓冲区
|
||||
buffer: AllocRingBuffer<LogMessage>,
|
||||
/// 缓冲区字节数组
|
||||
data: Vec<u8>,
|
||||
/// 能够输出到控制台的日志级别,当console_loglevel为DEFAULT时,表示可以打印所有级别的日志消息到控制台
|
||||
console_loglevel: LogLevel,
|
||||
/// 判断buffer在上一次转成字节数组之后是否发生变动
|
||||
is_changed: bool,
|
||||
}
|
||||
|
||||
impl Kmsg {
|
||||
pub fn new() -> Self {
|
||||
Kmsg {
|
||||
buffer: AllocRingBuffer::new(KMSG_BUFFER_CAPACITY),
|
||||
data: Vec::new(),
|
||||
console_loglevel: LogLevel::DEFAULT,
|
||||
is_changed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加日志消息
|
||||
pub fn push(&mut self, msg: LogMessage) {
|
||||
self.buffer.push(msg);
|
||||
self.is_changed = true;
|
||||
}
|
||||
|
||||
/// 读取缓冲区
|
||||
pub fn read(&mut self, buf: &mut [u8]) -> Result<usize, SystemError> {
|
||||
self.tobytes();
|
||||
|
||||
match self.console_loglevel {
|
||||
LogLevel::DEFAULT => self.read_all(buf),
|
||||
_ => self.read_level(buf),
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取缓冲区所有日志消息
|
||||
fn read_all(&mut self, buf: &mut [u8]) -> Result<usize, SystemError> {
|
||||
let len = self.data.len().min(buf.len());
|
||||
|
||||
// 拷贝数据
|
||||
let src = &self.data[0..len];
|
||||
buf[0..src.len()].copy_from_slice(src);
|
||||
|
||||
return Ok(src.len());
|
||||
}
|
||||
|
||||
/// 读取缓冲区特定level的日志消息
|
||||
fn read_level(&mut self, buf: &mut [u8]) -> Result<usize, SystemError> {
|
||||
let mut data_level: Vec<u8> = Vec::new();
|
||||
|
||||
for msg in self.buffer.iter() {
|
||||
if msg.level() == self.console_loglevel {
|
||||
data_level.append(&mut msg.to_string().as_bytes().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
let len = data_level.len().min(buf.len());
|
||||
|
||||
// 拷贝数据
|
||||
let src = &data_level[0..len];
|
||||
buf[0..src.len()].copy_from_slice(src);
|
||||
|
||||
// 将控制台输出日志level改回默认,否则之后都是打印特定level的日志消息
|
||||
self.console_loglevel = LogLevel::DEFAULT;
|
||||
|
||||
return Ok(data_level.len());
|
||||
}
|
||||
|
||||
/// 读取并清空缓冲区
|
||||
pub fn read_clear(&mut self, buf: &mut [u8]) -> Result<usize, SystemError> {
|
||||
let r = self.read_all(buf);
|
||||
self.clear()?;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/// 清空缓冲区
|
||||
pub fn clear(&mut self) -> Result<usize, SystemError> {
|
||||
self.buffer.clear();
|
||||
self.data.clear();
|
||||
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
/// 设置输出到控制台的日志级别
|
||||
pub fn set_level(&mut self, log_level: usize) -> Result<usize, SystemError> {
|
||||
let log_level = log_level - 1;
|
||||
|
||||
self.console_loglevel = match log_level {
|
||||
0 => LogLevel::EMERG,
|
||||
1 => LogLevel::ALERT,
|
||||
2 => LogLevel::CRIT,
|
||||
3 => LogLevel::ERR,
|
||||
4 => LogLevel::WARN,
|
||||
5 => LogLevel::NOTICE,
|
||||
6 => LogLevel::INFO,
|
||||
7 => LogLevel::DEBUG,
|
||||
8 => LogLevel::DEFAULT,
|
||||
_ => return Err(SystemError::EINVAL),
|
||||
};
|
||||
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
/// 将环形缓冲区的日志消息转成字节数组以拷入用户buf
|
||||
fn tobytes(&mut self) -> usize {
|
||||
if self.is_changed {
|
||||
self.data.clear();
|
||||
|
||||
if self.console_loglevel == LogLevel::DEFAULT {
|
||||
for msg in self.buffer.iter() {
|
||||
self.data.append(&mut msg.to_string().as_bytes().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
self.is_changed = false;
|
||||
}
|
||||
|
||||
return self.data.len();
|
||||
}
|
||||
|
||||
// 返回内核缓冲区所占字节数
|
||||
pub fn data_size(&mut self) -> Result<usize, SystemError> {
|
||||
return Ok(self.tobytes());
|
||||
}
|
||||
}
|
107
kernel/src/filesystem/procfs/log.rs
Normal file
107
kernel/src/filesystem/procfs/log.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use core::fmt::{Display, Formatter, Result};
|
||||
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::time::TimeSpec;
|
||||
|
||||
// /// 日志类型
|
||||
// #[derive(Default, Clone, Debug)]
|
||||
// pub enum LogType {
|
||||
// /// 启动信息
|
||||
// Startup,
|
||||
// /// 驱动信息
|
||||
// Driver,
|
||||
// /// 系统信息
|
||||
// System,
|
||||
// /// 硬件信息
|
||||
// Hardware,
|
||||
// /// 内核模块信息
|
||||
// KernelModule,
|
||||
// /// 内核调试信息
|
||||
// KernelDebug,
|
||||
// #[default]
|
||||
// Default,
|
||||
// }
|
||||
|
||||
/// 日志级别
|
||||
#[derive(Default, Clone, PartialEq, Debug)]
|
||||
pub enum LogLevel {
|
||||
EMERG = 0,
|
||||
ALERT = 1,
|
||||
CRIT = 2,
|
||||
ERR = 3,
|
||||
WARN = 4,
|
||||
NOTICE = 5,
|
||||
INFO = 6,
|
||||
DEBUG = 7,
|
||||
#[default]
|
||||
DEFAULT = 8,
|
||||
}
|
||||
|
||||
impl From<usize> for LogLevel {
|
||||
fn from(value: usize) -> Self {
|
||||
match value {
|
||||
0 => LogLevel::EMERG,
|
||||
1 => LogLevel::ALERT,
|
||||
2 => LogLevel::CRIT,
|
||||
3 => LogLevel::ERR,
|
||||
4 => LogLevel::WARN,
|
||||
5 => LogLevel::NOTICE,
|
||||
6 => LogLevel::INFO,
|
||||
7 => LogLevel::DEBUG,
|
||||
_ => LogLevel::DEFAULT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志消息
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct LogMessage {
|
||||
/// 时间戳
|
||||
timestamp: TimeSpec,
|
||||
/// 日志级别
|
||||
level: LogLevel,
|
||||
// /// 日志类型
|
||||
// log_type: LogType,
|
||||
/// 日志消息
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl LogMessage {
|
||||
pub fn new(timestamp: TimeSpec, level: LogLevel, message: String) -> Self {
|
||||
LogMessage {
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn level(&self) -> LogLevel {
|
||||
self.level.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LogMessage {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
let timestamp = &self.timestamp;
|
||||
let level = match self.level {
|
||||
LogLevel::EMERG => "EMERG",
|
||||
LogLevel::ALERT => "ALERT",
|
||||
LogLevel::CRIT => "CRIT",
|
||||
LogLevel::ERR => "ERR",
|
||||
LogLevel::WARN => "WARNING",
|
||||
LogLevel::NOTICE => "NOTICE",
|
||||
LogLevel::INFO => "INFO",
|
||||
LogLevel::DEBUG => "DEBUG",
|
||||
LogLevel::DEFAULT => "Default",
|
||||
};
|
||||
|
||||
let message = &self.message;
|
||||
|
||||
let res = format!(
|
||||
"<{}>[{}.{}] : {}\n",
|
||||
level, timestamp.tv_sec, timestamp.tv_nsec, message
|
||||
);
|
||||
return write!(f, "{}", res);
|
||||
}
|
||||
}
|
@ -33,6 +33,10 @@ use super::vfs::{
|
||||
FileSystem, FsInfo, IndexNode, InodeId, Metadata,
|
||||
};
|
||||
|
||||
pub mod kmsg;
|
||||
pub mod log;
|
||||
mod syscall;
|
||||
|
||||
/// @brief 进程文件类型
|
||||
/// @usage 用于定义进程文件夹下的各类文件类型
|
||||
#[derive(Debug)]
|
||||
@ -42,6 +46,8 @@ pub enum ProcFileType {
|
||||
ProcStatus = 0,
|
||||
/// meminfo
|
||||
ProcMeminfo = 1,
|
||||
/// kmsg
|
||||
ProcKmsg = 2,
|
||||
//todo: 其他文件类型
|
||||
///默认文件类型
|
||||
Default,
|
||||
@ -52,6 +58,7 @@ impl From<u8> for ProcFileType {
|
||||
match value {
|
||||
0 => ProcFileType::ProcStatus,
|
||||
1 => ProcFileType::ProcMeminfo,
|
||||
2 => ProcFileType::ProcKmsg,
|
||||
_ => ProcFileType::Default,
|
||||
}
|
||||
}
|
||||
@ -336,6 +343,19 @@ impl ProcFS {
|
||||
panic!("create meminfo error");
|
||||
}
|
||||
|
||||
// 创建kmsg文件
|
||||
let binding = inode.create("kmsg", FileType::File, ModeType::from_bits_truncate(0o444));
|
||||
if let Ok(kmsg) = binding {
|
||||
let kmsg_file = kmsg
|
||||
.as_any_ref()
|
||||
.downcast_ref::<LockedProcFSInode>()
|
||||
.unwrap();
|
||||
kmsg_file.0.lock().fdata.pid = Pid::new(1);
|
||||
kmsg_file.0.lock().fdata.ftype = ProcFileType::ProcKmsg;
|
||||
} else {
|
||||
panic!("create ksmg error");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -456,6 +476,7 @@ impl IndexNode for LockedProcFSInode {
|
||||
match inode.fdata.ftype {
|
||||
ProcFileType::ProcStatus => return inode.proc_read(offset, len, buf, private_data),
|
||||
ProcFileType::ProcMeminfo => return inode.proc_read(offset, len, buf, private_data),
|
||||
ProcFileType::ProcKmsg => (),
|
||||
ProcFileType::Default => (),
|
||||
};
|
||||
|
||||
|
77
kernel/src/filesystem/procfs/syscall.rs
Normal file
77
kernel/src/filesystem/procfs/syscall.rs
Normal file
@ -0,0 +1,77 @@
|
||||
use core::usize;
|
||||
|
||||
use system_error::SystemError;
|
||||
|
||||
use crate::syscall::Syscall;
|
||||
|
||||
use super::kmsg::KMSG;
|
||||
|
||||
/// 操作内核环形缓冲区
|
||||
enum SyslogAction {
|
||||
/// Close the log. Currently a NOP.
|
||||
SyslogActionClose = 0,
|
||||
/// Open the log. Currently a NOP.
|
||||
SyslogActionOpen = 1,
|
||||
/// Read from the log.
|
||||
SyslogActionRead = 2,
|
||||
/// Read and clear all messages remaining in the ring buffer.
|
||||
SyslogActionReadClear = 4,
|
||||
/// Clear ring buffer.
|
||||
SyslogActionClear = 5,
|
||||
/// Set level of messages printed to console.
|
||||
SyslogActionConsoleLevel = 8,
|
||||
/// Return size of the log buffer.
|
||||
SyslogActionSizeBuffer = 10,
|
||||
/// Invalid SyslogAction
|
||||
SyslogActionInval,
|
||||
}
|
||||
|
||||
impl From<usize> for SyslogAction {
|
||||
fn from(value: usize) -> Self {
|
||||
match value {
|
||||
0 => SyslogAction::SyslogActionClose,
|
||||
1 => SyslogAction::SyslogActionOpen,
|
||||
2 => SyslogAction::SyslogActionRead,
|
||||
4 => SyslogAction::SyslogActionReadClear,
|
||||
5 => SyslogAction::SyslogActionClear,
|
||||
8 => SyslogAction::SyslogActionConsoleLevel,
|
||||
10 => SyslogAction::SyslogActionSizeBuffer,
|
||||
_ => SyslogAction::SyslogActionInval,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Syscall {
|
||||
/// # 操作内核环形缓冲区
|
||||
///
|
||||
/// ## 参数
|
||||
/// - syslog_action_type: 操作码
|
||||
/// - buf:用户缓冲区
|
||||
/// - len: 需要从内核环形缓冲区读取的字节数。如果操作码为8,即SyslogActionConsoleLevel,则len为待设置的日志级别
|
||||
///
|
||||
/// ## 返回值
|
||||
/// - 成功,Ok(usize)
|
||||
/// - 失败,Err(SystemError) 操作失败,返回posix错误码
|
||||
///
|
||||
|
||||
pub fn do_syslog(
|
||||
syslog_action_type: usize,
|
||||
buf: &mut [u8],
|
||||
len: usize,
|
||||
) -> Result<usize, SystemError> {
|
||||
let syslog_action = SyslogAction::from(syslog_action_type);
|
||||
|
||||
let mut kmsg_guard = unsafe { KMSG.as_ref().unwrap().lock_irqsave() };
|
||||
|
||||
match syslog_action {
|
||||
SyslogAction::SyslogActionClose => Ok(0),
|
||||
SyslogAction::SyslogActionOpen => Ok(0),
|
||||
SyslogAction::SyslogActionRead => kmsg_guard.read(buf),
|
||||
SyslogAction::SyslogActionReadClear => kmsg_guard.read_clear(buf),
|
||||
SyslogAction::SyslogActionClear => kmsg_guard.clear(),
|
||||
SyslogAction::SyslogActionSizeBuffer => kmsg_guard.data_size(),
|
||||
SyslogAction::SyslogActionConsoleLevel => kmsg_guard.set_level(len),
|
||||
SyslogAction::SyslogActionInval => return Err(SystemError::EINVAL),
|
||||
}
|
||||
}
|
||||
}
|
@ -462,7 +462,7 @@ impl Drop for File {
|
||||
"pid: {:?} failed to close file: {:?}, errno={:?}",
|
||||
ProcessManager::current_pcb().pid(),
|
||||
self,
|
||||
r.unwrap_err()
|
||||
r.as_ref().unwrap_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,17 @@
|
||||
use core::fmt::{self, Write};
|
||||
|
||||
use alloc::string::ToString;
|
||||
|
||||
use super::lib_ui::textui::{textui_putstr, FontColor};
|
||||
|
||||
use crate::{
|
||||
filesystem::procfs::{
|
||||
kmsg::KMSG,
|
||||
log::{LogLevel, LogMessage},
|
||||
},
|
||||
time::TimeSpec,
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ($crate::libs::printk::__printk(format_args!($($arg)*)));
|
||||
@ -30,14 +40,15 @@ macro_rules! printk_color {
|
||||
#[macro_export]
|
||||
macro_rules! kdebug {
|
||||
($($arg:tt)*) => {
|
||||
$crate::libs::printk::Logger.log(7,format_args!("({}:{})\t {}\n", file!(), line!(),format_args!($($arg)*)));
|
||||
$crate::libs::printk::PrintkWriter.__write_fmt(format_args!("[ DEBUG ] ({}:{})\t {}\n", file!(), line!(),format_args!($($arg)*)))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! kinfo {
|
||||
($($arg:tt)*) => {
|
||||
$crate::libs::printk::Logger.log(6,format_args!("({}:{})\t {}\n", file!(), line!(),format_args!($($arg)*)));
|
||||
$crate::libs::printk::PrintkWriter.__write_fmt(format_args!("[ INFO ] ({}:{})\t {}\n", file!(), line!(),format_args!($($arg)*)))
|
||||
}
|
||||
}
|
||||
@ -45,6 +56,7 @@ macro_rules! kinfo {
|
||||
#[macro_export]
|
||||
macro_rules! kwarn {
|
||||
($($arg:tt)*) => {
|
||||
$crate::libs::printk::Logger.log(4,format_args!("({}:{})\t {}\n", file!(), line!(),format_args!($($arg)*)));
|
||||
$crate::libs::printk::PrintkWriter.__write_string_color($crate::libs::lib_ui::textui::FontColor::YELLOW, $crate::libs::lib_ui::textui::FontColor::BLACK, "[ WARN ] ");
|
||||
$crate::libs::printk::PrintkWriter.__write_fmt(format_args!("({}:{})\t {}\n", file!(), line!(),format_args!($($arg)*)));
|
||||
}
|
||||
@ -53,6 +65,7 @@ macro_rules! kwarn {
|
||||
#[macro_export]
|
||||
macro_rules! kerror {
|
||||
($($arg:tt)*) => {
|
||||
$crate::libs::printk::Logger.log(3,format_args!("({}:{})\t {}\n", file!(), line!(),format_args!($($arg)*)));
|
||||
$crate::libs::printk::PrintkWriter.__write_string_color($crate::libs::lib_ui::textui::FontColor::RED, $crate::libs::lib_ui::textui::FontColor::BLACK, "[ ERROR ] ");
|
||||
$crate::libs::printk::PrintkWriter.__write_fmt(format_args!("({}:{})\t {}\n", file!(), line!(),format_args!($($arg)*)));
|
||||
}
|
||||
@ -61,6 +74,7 @@ macro_rules! kerror {
|
||||
#[macro_export]
|
||||
macro_rules! kBUG {
|
||||
($($arg:tt)*) => {
|
||||
$crate::libs::printk::Logger.log(1,format_args!("({}:{})\t {}\n", file!(), line!(),format_args!($($arg)*)));
|
||||
$crate::libs::printk::PrintkWriter.__write_string_color($crate::libs::lib_ui::textui::FontColor::RED, $crate::libs::lib_ui::textui::FontColor::BLACK, "[ BUG ] ");
|
||||
$crate::libs::printk::PrintkWriter.__write_fmt(format_args!("({}:{})\t {}\n", file!(), line!(),format_args!($($arg)*)));
|
||||
}
|
||||
@ -97,3 +111,17 @@ impl fmt::Write for PrintkWriter {
|
||||
pub fn __printk(args: fmt::Arguments) {
|
||||
PrintkWriter.write_fmt(args).unwrap();
|
||||
}
|
||||
|
||||
pub struct Logger;
|
||||
|
||||
impl Logger {
|
||||
pub fn log(&self, log_level: usize, message: fmt::Arguments) {
|
||||
if unsafe { !KMSG.is_none() } {
|
||||
let timestamp: TimeSpec = TimeSpec::now();
|
||||
let log_level = LogLevel::from(log_level.clone());
|
||||
let log_message = LogMessage::new(timestamp, log_level, message.to_string());
|
||||
|
||||
unsafe { KMSG.as_ref().unwrap().lock_irqsave().push(log_message) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -853,10 +853,20 @@ impl Syscall {
|
||||
}
|
||||
SYS_GETTID => Self::gettid().map(|tid| tid.into()),
|
||||
SYS_GETUID => Self::getuid().map(|uid| uid.into()),
|
||||
|
||||
SYS_SYSLOG => {
|
||||
kwarn!("SYS_SYSLOG has not yet been implemented");
|
||||
Ok(0)
|
||||
let syslog_action_type = args[0] as usize;
|
||||
let buf_vaddr = args[1];
|
||||
let len = args[2];
|
||||
let from_user = frame.from_user();
|
||||
let mut user_buffer_writer =
|
||||
UserBufferWriter::new(buf_vaddr as *mut u8, len, from_user)?;
|
||||
|
||||
let user_buf = user_buffer_writer.buffer(0)?;
|
||||
let res = Self::do_syslog(syslog_action_type, user_buf, len);
|
||||
res
|
||||
}
|
||||
|
||||
SYS_GETGID => Self::getgid().map(|gid| gid.into()),
|
||||
SYS_SETUID => {
|
||||
kwarn!("SYS_SETUID has not yet been implemented");
|
||||
|
@ -1,8 +1,11 @@
|
||||
use core::{
|
||||
fmt,
|
||||
intrinsics::unlikely,
|
||||
ops::{self, Sub},
|
||||
};
|
||||
|
||||
use crate::arch::CurrentTimeArch;
|
||||
|
||||
use self::timekeep::ktime_get_real_ns;
|
||||
|
||||
pub mod clocksource;
|
||||
@ -55,6 +58,27 @@ impl TimeSpec {
|
||||
tv_nsec: nsec,
|
||||
};
|
||||
}
|
||||
|
||||
/// 获取当前时间
|
||||
pub fn now() -> Self {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use crate::arch::driver::tsc::TSCManager;
|
||||
let khz = TSCManager::cpu_khz();
|
||||
if unlikely(khz == 0) {
|
||||
return TimeSpec::default();
|
||||
} else {
|
||||
return Self::from(Duration::from_millis(
|
||||
CurrentTimeArch::get_cycles() as u64 / khz,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "riscv64")]
|
||||
{
|
||||
unimplemented!("TimeSpec::now()")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for TimeSpec {
|
||||
|
@ -129,7 +129,7 @@ impl Timer {
|
||||
if unlikely(r.is_err()) {
|
||||
kerror!(
|
||||
"Failed to run timer function: {self:?} {:?}",
|
||||
r.err().unwrap()
|
||||
r.as_ref().err().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,32 @@
|
||||
# 该脚本会自动下载musl交叉编译工具链,并将其添加到PATH中
|
||||
#########################################################################
|
||||
|
||||
export USE_GITHUB=${USE_GITHUB:=0}
|
||||
|
||||
|
||||
|
||||
MUSL_GCC_DATE="231114"
|
||||
MUSL_GCC_VERSION="9.4.0"
|
||||
MUSL_GCC_X86_64_TAR="x86_64-linux-musl-cross-gcc-${MUSL_GCC_VERSION}-${MUSL_GCC_DATE}.tar.xz"
|
||||
MUSL_GCC_RISCV64_TAR="riscv64-linux-musl-cross-gcc-${MUSL_GCC_VERSION}-${MUSL_GCC_DATE}.tar.xz"
|
||||
MUSL_GCC_X86_64_DOWNLOAD_URL="https://mirrors.dragonos.org.cn/pub/third_party/toolchain/gcc/${MUSL_GCC_X86_64_TAR}"
|
||||
MUSL_GCC_RISCV64_DOWNLOAD_URL="https://mirrors.dragonos.org.cn/pub/third_party/toolchain/gcc/${MUSL_GCC_RISCV64_TAR}"
|
||||
MUSL_GCC_X86_64_TAR=
|
||||
MUSL_GCC_RISCV64_TAR=
|
||||
|
||||
MUSL_GCC_X86_64_DOWNLOAD_URL=""
|
||||
MUSL_GCC_RISCV64_DOWNLOAD_URL=""
|
||||
if [ $USE_GITHUB -eq 1 ]; then
|
||||
echo "Download from github"
|
||||
|
||||
MUSL_GCC_X86_64_TAR=x86_64-linux-musl-cross-gcc-${MUSL_GCC_VERSION}.tar.xz
|
||||
MUSL_GCC_RISCV64_TAR=riscv64-linux-musl-cross-gcc-${MUSL_GCC_VERSION}.tar.xz
|
||||
MUSL_GCC_X86_64_DOWNLOAD_URL="https://github.com/DragonOS-Community/musl-cross-make/releases/download/${MUSL_GCC_VERSION}-${MUSL_GCC_DATE}/${MUSL_GCC_X86_64_TAR}"
|
||||
MUSL_GCC_RISCV64_DOWNLOAD_URL="https://github.com/DragonOS-Community/musl-cross-make/releases/download/${MUSL_GCC_VERSION}-${MUSL_GCC_DATE}/${MUSL_GCC_RISCV64_TAR}"
|
||||
https://github.com/DragonOS-Community/musl-cross-make/releases/download/9.4.0-231114/riscv64-linux-musl-cross-gcc-9.4.0.tar.xz
|
||||
else
|
||||
echo "Download from mirrors.dragonos.org.cn"
|
||||
MUSL_GCC_X86_64_TAR="x86_64-linux-musl-cross-gcc-${MUSL_GCC_VERSION}-${MUSL_GCC_DATE}.tar.xz"
|
||||
MUSL_GCC_RISCV64_TAR="riscv64-linux-musl-cross-gcc-${MUSL_GCC_VERSION}-${MUSL_GCC_DATE}.tar.xz"
|
||||
MUSL_GCC_X86_64_DOWNLOAD_URL="https://mirrors.dragonos.org.cn/pub/third_party/toolchain/gcc/${MUSL_GCC_X86_64_TAR}"
|
||||
MUSL_GCC_RISCV64_DOWNLOAD_URL="https://mirrors.dragonos.org.cn/pub/third_party/toolchain/gcc/${MUSL_GCC_RISCV64_TAR}"
|
||||
fi
|
||||
|
||||
|
||||
INSTALL_POS="$HOME/opt/"
|
||||
|
@ -51,9 +51,8 @@ endif
|
||||
.PHONY: dadk_run
|
||||
dadk_run: install_dadk
|
||||
mkdir -p $(DADK_CACHE_DIR)
|
||||
# 之所以在这里临时设置ARCH为空,是因为如果要设置这个环境变量,应当在DADK的配置文件中设置
|
||||
ARCH= dadk --config-dir dadk/config --cache-dir $(DADK_CACHE_DIR) --dragonos-dir $(ROOT_PATH)/bin/sysroot build
|
||||
ARCH= dadk --config-dir dadk/config --cache-dir $(DADK_CACHE_DIR) --dragonos-dir $(ROOT_PATH)/bin/sysroot install
|
||||
dadk --config-dir dadk/config --cache-dir $(DADK_CACHE_DIR) --dragonos-dir $(ROOT_PATH)/bin/sysroot build
|
||||
dadk --config-dir dadk/config --cache-dir $(DADK_CACHE_DIR) --dragonos-dir $(ROOT_PATH)/bin/sysroot install
|
||||
|
||||
.PHONY: dadk_clean
|
||||
dadk_clean: install_dadk
|
||||
|
1
user/apps/dmesg/.gitignore
vendored
Normal file
1
user/apps/dmesg/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
dmesg
|
17
user/apps/dmesg/Makefile
Normal file
17
user/apps/dmesg/Makefile
Normal file
@ -0,0 +1,17 @@
|
||||
ifeq ($(ARCH), x86_64)
|
||||
export PREFIX=x86_64-linux-musl-
|
||||
else ifeq ($(ARCH), riscv64)
|
||||
export PREFIX=riscv64-linux-musl-
|
||||
endif
|
||||
|
||||
|
||||
export CC=$(PREFIX)gcc
|
||||
|
||||
all: dmesg
|
||||
mv dmesg $(DADK_CURRENT_BUILD_DIR)
|
||||
|
||||
dmesg: main.c
|
||||
$(CC) -static -o dmesg main.c dmesg.c
|
||||
|
||||
clean:
|
||||
rm dmesg *.o
|
86
user/apps/dmesg/dmesg.c
Normal file
86
user/apps/dmesg/dmesg.c
Normal file
@ -0,0 +1,86 @@
|
||||
#include "dmesg.h"
|
||||
|
||||
/**
|
||||
* @brief 识别dmesg程序的第一个选项参数
|
||||
*
|
||||
* @param arg dmesg命令第一个选项参数
|
||||
* @return int 有效时返回对应选项码,无效时返回 -1
|
||||
*/
|
||||
int getopt(char *arg)
|
||||
{
|
||||
if (!strcmp(arg, "-h") || !strcmp(arg, "--help"))
|
||||
return 0;
|
||||
else if (!strcmp(arg, "-c") || !strcmp(arg, "--read-clear"))
|
||||
return 4;
|
||||
else if (!strcmp(arg, "-C") || !strcmp(arg, "--clear"))
|
||||
return 5;
|
||||
else if (!strcmp(arg, "-l") || !strcmp(arg, "--level"))
|
||||
return 8;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 识别dmesg程序的第二个选项参数
|
||||
*
|
||||
* @param arg dmesg命令第一个选项参数
|
||||
* @return int 有效时返回设置的日志级别,无效时返回 -1
|
||||
*/
|
||||
int getlevel(char *arg)
|
||||
{
|
||||
if (!strcmp(arg, "EMERG") || !strcmp(arg, "emerg"))
|
||||
return 0;
|
||||
else if (!strcmp(arg, "ALERT") || !strcmp(arg, "alert"))
|
||||
return 1;
|
||||
else if (!strcmp(arg, "CRIT") || !strcmp(arg, "crit"))
|
||||
return 2;
|
||||
else if (!strcmp(arg, "ERR") || !strcmp(arg, "err"))
|
||||
return 3;
|
||||
else if (!strcmp(arg, "WARN") || !strcmp(arg, "warn"))
|
||||
return 4;
|
||||
else if (!strcmp(arg, "NOTICE") || !strcmp(arg, "notice"))
|
||||
return 5;
|
||||
else if (!strcmp(arg, "INFO") || !strcmp(arg, "info"))
|
||||
return 6;
|
||||
else if (!strcmp(arg, "DEBUG") || !strcmp(arg, "debug"))
|
||||
return 7;
|
||||
else
|
||||
{
|
||||
printf("dmesg: unknown level '%s'\n", arg);
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 打印dmesg手册
|
||||
*/
|
||||
void print_help_msg()
|
||||
{
|
||||
const char *help_msg = "Usage:\n"
|
||||
" dmesg [options]\n\n"
|
||||
"Display or control the kernel ring buffer.\n\n"
|
||||
"Options:\n"
|
||||
" -C, --clear clear the kernel ring buffer\n"
|
||||
" -c, --read-clear read and clear all messages\n"
|
||||
" -l, --level <list> restrict output to defined levels\n"
|
||||
" -h, --help display this help\n\n"
|
||||
"Supported log levels (priorities):\n"
|
||||
" emerg - system is unusable\n"
|
||||
" alert - action must be taken immediately\n"
|
||||
" crit - critical conditions\n"
|
||||
" err - error conditions\n"
|
||||
" warn - warning conditions\n"
|
||||
" notice - normal but significant condition\n"
|
||||
" info - informational\n"
|
||||
" debug - debug-level messages\n";
|
||||
printf("%s\n", help_msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 打印dmesg错误使用的信息
|
||||
*/
|
||||
void print_bad_usage_msg()
|
||||
{
|
||||
const char *bad_usage_msg = "dmesg: bad usage\nTry 'dmesg --help' for more information.";
|
||||
printf("%s\n", bad_usage_msg);
|
||||
}
|
31
user/apps/dmesg/dmesg.h
Normal file
31
user/apps/dmesg/dmesg.h
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <malloc.h>
|
||||
#include <string.h>
|
||||
|
||||
/**
|
||||
* @brief 识别dmesg程序的第一个选项参数
|
||||
*
|
||||
* @param arg dmesg命令第一个选项参数
|
||||
* @return int 有效时返回对应选项码,无效时返回 -1
|
||||
*/
|
||||
int getopt(char *arg);
|
||||
|
||||
/**
|
||||
* @brief 识别dmesg程序的第二个选项参数
|
||||
*
|
||||
* @param arg dmesg命令第一个选项参数
|
||||
* @return int 有效时返回设置的日志级别,无效时返回 -1
|
||||
*/
|
||||
int getlevel(char *arg);
|
||||
|
||||
/**
|
||||
* @brief 打印dmesg手册
|
||||
*/
|
||||
void print_help_msg();
|
||||
|
||||
/**
|
||||
* @brief 打印dmesg错误使用的信息
|
||||
*/
|
||||
void print_bad_usage_msg();
|
113
user/apps/dmesg/main.c
Normal file
113
user/apps/dmesg/main.c
Normal file
@ -0,0 +1,113 @@
|
||||
#include "dmesg.h"
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
unsigned int len = 1;
|
||||
char *buf = NULL;
|
||||
int opt;
|
||||
unsigned int color = 65280;
|
||||
|
||||
// 获取内核缓冲区大小
|
||||
len = klogctl(10, buf, len);
|
||||
|
||||
if (len < 16 * 1024)
|
||||
len = 16 * 1024;
|
||||
if (len > 16 * 1024 * 1024)
|
||||
len = 16 * 1024 * 1024;
|
||||
|
||||
buf = malloc(len);
|
||||
if (buf == NULL)
|
||||
{
|
||||
perror("");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (argc == 1)
|
||||
{
|
||||
// 无选项参数,默认打印所有日志消息
|
||||
len = klogctl(2, buf, len);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 获取第一个选项参数
|
||||
opt = getopt(argv[1]);
|
||||
|
||||
// 无效参数
|
||||
if (opt == -1)
|
||||
{
|
||||
print_bad_usage_msg();
|
||||
return -1;
|
||||
}
|
||||
// 打印帮助手册
|
||||
else if (opt == 0)
|
||||
{
|
||||
print_help_msg();
|
||||
return 0;
|
||||
}
|
||||
// 4 -> 读取内核缓冲区后,清空缓冲区
|
||||
// 5 -> 清空内核缓冲区
|
||||
else if (opt == 4 || opt == 5)
|
||||
{
|
||||
len = klogctl(opt, buf, len);
|
||||
}
|
||||
// 读取特定日志级别的消息
|
||||
else if (opt == 8)
|
||||
{
|
||||
// 无指定日志级别参数,打印错误使用信息
|
||||
if (argc < 3)
|
||||
{
|
||||
print_bad_usage_msg();
|
||||
return -1;
|
||||
}
|
||||
|
||||
int level = -1;
|
||||
|
||||
// 获取日志级别
|
||||
// 这里加1的原因是:如果klogctl的第三个参数是0,不会发生系统调用
|
||||
level = getlevel(argv[2]) + 1;
|
||||
|
||||
if (level == -1)
|
||||
return -1;
|
||||
|
||||
klogctl(8, buf, level);
|
||||
len = klogctl(2, buf, len);
|
||||
}
|
||||
}
|
||||
|
||||
// 当前打印内容
|
||||
// 0: 日志级别
|
||||
// 1: 时间戳
|
||||
// 2: 代码行号
|
||||
// 3: 日志消息
|
||||
unsigned int content = 0;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
char c[2];
|
||||
c[0] = buf[i];
|
||||
c[1] = '\0';
|
||||
syscall(100000, &c[0], color, 0);
|
||||
if (content == 0 && buf[i] == '>')
|
||||
{
|
||||
content++;
|
||||
}
|
||||
else if (content == 1 && buf[i] == ']')
|
||||
{
|
||||
color = 16744448;
|
||||
content++;
|
||||
}
|
||||
else if (content == 2 && buf[i] == ')')
|
||||
{
|
||||
color = 16777215;
|
||||
content++;
|
||||
}
|
||||
else if (content == 3 && buf[i] == '\n')
|
||||
{
|
||||
color = 65280;
|
||||
content = 0;
|
||||
}
|
||||
}
|
||||
|
||||
free(buf);
|
||||
|
||||
return 0;
|
||||
}
|
23
user/dadk/config/dmesg-0.1.0.dadk
Normal file
23
user/dadk/config/dmesg-0.1.0.dadk
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "dmesg",
|
||||
"version": "0.1.0",
|
||||
"description": "查看日志",
|
||||
"task_type": {
|
||||
"BuildFromSource": {
|
||||
"Local": {
|
||||
"path": "apps/dmesg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"depends": [ ],
|
||||
"build": {
|
||||
"build_command": "make"
|
||||
},
|
||||
"install": {
|
||||
"in_dragonos_path": "/bin"
|
||||
},
|
||||
"clean": {
|
||||
"clean_command": "make clean"
|
||||
},
|
||||
"envs": []
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user