Linux设备模型(二) - kset/kobj/ktype APIs

news2024/11/18 4:48:47

一,kobject_init_and_add

1,kobject_init_and_add实现

/**
* kobject_init_and_add() - Initialize a kobject structure and add it to
*                          the kobject hierarchy.
* @kobj: pointer to the kobject to initialize
* @ktype: pointer to the ktype for this kobject.
* @parent: pointer to the parent of this kobject.
* @fmt: the name of the kobject.
*
* This function combines the call to kobject_init() and kobject_add().
*
* If this function returns an error, kobject_put() must be called to
* properly clean up the memory associated with the object.  This is the
* same type of error handling after a call to kobject_add() and kobject
* lifetime rules are the same here.
*/
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,
             struct kobject *parent, const char *fmt, ...)
{
    va_list args;
    int retval;

    kobject_init(kobj, ktype);

    va_start(args, fmt);
    retval = kobject_add_varg(kobj, parent, fmt, args);
    va_end(args);

    return retval;
}

2,函数调用流程

error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL, "%s", drv->name);
----kobject_init(kobj, ktype);
--------kobject_init_internal(kobj);
------------kref_init(&kobj->kref);
------------kobj->state_initialized = 1;
----kobject_add_varg(kobj, parent, fmt, args);
--------kobject_set_name_vargs(kobj, fmt, vargs);
--------kobject_add_internal(kobj);
------------parent = kobject_get(kobj->parent);
------------if (kobj->kset)
------------kobj_kset_join(kobj);
----------------list_add_tail(&kobj->entry, &kobj->kset->list);
------------create_dir(kobj);
----------------sysfs_create_dir_ns(kobj, kobject_namespace(kobj));
----------------populate_dir(kobj);
----------------sysfs_create_groups(kobj, ktype->default_groups);

二,kobject_create_and_add

1,kobject_create_and_add实现

/**
* kobject_create_and_add() - Create a struct kobject dynamically and
*                            register it with sysfs.
* @name: the name for the kobject
* @parent: the parent kobject of this kobject, if any.
*
* This function creates a kobject structure dynamically and registers it
* with sysfs.  When you are finished with this structure, call
* kobject_put() and the structure will be dynamically freed when
* it is no longer being used.
*
* If the kobject was not able to be created, NULL will be returned.
*/
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent)
{
    struct kobject *kobj;
    int retval;

    kobj = kobject_create();
    if (!kobj)
        return NULL;

    retval = kobject_add(kobj, parent, "%s", name);
    if (retval) {
        pr_warn("%s: kobject_add error: %d\n", __func__, retval);
        kobject_put(kobj);
        kobj = NULL;
    }
    return kobj;
}
EXPORT_SYMBOL_GPL(kobject_create_and_add);

2,函数调用流程

fw_ctrl->kobj = kobject_create_and_add("fwupdate", &core_data->pdev->dev.kobj);
----kobj = kobject_create();
--------kobj = kzalloc(sizeof(*kobj), GFP_KERNEL);
--------kobject_init(kobj, &dynamic_kobj_ktype);
----kobject_add(kobj, parent, "%s", name);
--------if (!kobj->state_initialized)
--------kobject_add_varg(kobj, parent, fmt, args);//The main kobject add function

3,kobject_add_internal

static int kobject_add_internal(struct kobject *kobj)
{
    int error = 0;
    struct kobject *parent;

    if (!kobj)
        return -ENOENT;

    //kbj的名字不能为空
    if (!kobj->name || !kobj->name[0]) {
        WARN(1,
             "kobject: (%p): attempted to be registered with empty name!\n",
             kobj);
        return -EINVAL;
    }

    //增加kobj->parent的引用计数kref+1
    parent = kobject_get(kobj->parent);

    //如果kobj属于某个kset但是该kobj的parent为空,将kset->kobj作为作为该kobj的parent
    /* join kset if set, use it as parent if we do not already have one */
    if (kobj->kset) {
        if (!parent)
            parent = kobject_get(&kobj->kset->kobj);
        /* add the kobject to its kset's list */
        kobj_kset_join(kobj);
        kobj->parent = parent;
    }

    //打印kobj的名字和kobj parent的名字
    pr_debug("kobject: '%s' (%p): %s: parent: '%s', set: '%s'\n",
         kobject_name(kobj), kobj, __func__,
         parent ? kobject_name(parent) : "<NULL>",
         kobj->kset ? kobject_name(&kobj->kset->kobj) : "<NULL>");
    
    //创建名字为kobject_name(kobj)的目录,使用kobj_type->default_attrs[i]和kobj_type->default_groups在dir中创建文件节点
    error = create_dir(kobj);
    if (error) {
        kobj_kset_leave(kobj);
        kobject_put(parent);
        kobj->parent = NULL;

        /* be noisy on error issues */
        if (error == -EEXIST)
            pr_err("%s failed for %s with -EEXIST, don't try to register things with the same name in the same directory.\n",
                   __func__, kobject_name(kobj));
        else
            pr_err("%s failed for %s (error: %d parent: %s)\n",
                   __func__, kobject_name(kobj), error,
                   parent ? kobject_name(parent) : "'none'");
    } else
        //kobj已经在sysfs中创建了dir和node,设置flag
        kobj->state_in_sysfs = 1;

    return error;
}
/* add the kobject to its kset's list */
static void kobj_kset_join(struct kobject *kobj)
{
    if (!kobj->kset)
        return;

    kset_get(kobj->kset);
    spin_lock(&kobj->kset->list_lock);
    //所有属于该kset的kobj都会挂在kset->list链表上
    list_add_tail(&kobj->entry, &kobj->kset->list);
    spin_unlock(&kobj->kset->list_lock);
}

4,kobject_create_and_add的一种使用

有一种例外,Kobject不再嵌在其它数据结构中,可以单独使用,这个例外就是:开发者只需要在sysfs中创建一个目录,而不需要其它的kset、ktype的操作。这时可以直接调用kobject_create_and_add接口,分配一个kobject结构并把它添加到kernel中。

例如在sysfs device的目录中创建一个文件夹然后在其中创建文件节点:

static int fw_sysfs_init(struct ts_core *core_data,
        struct fw_update_ctrl *fw_ctrl)
{
    int ret = 0, i;

    fw_ctrl->kobj = kobject_create_and_add("fwupdate",
                    &core_data->pdev->dev.kobj);
    if (!fw_ctrl->kobj) {                                              
        ts_err("failed create sub dir for fwupdate");
        return -EINVAL;
    }

    for (i = 0; i < ARRAY_SIZE(fwu_attrs) && !ret; i++)
        ret = sysfs_create_file(fw_ctrl->kobj, fwu_attrs[i]);

    if (ret) {
        ts_err("failed create fwu sysfs files");
        while (--i >= 0)
            sysfs_remove_file(fw_ctrl->kobj, fwu_attrs[i]);

        kobject_put(fw_ctrl->kobj);
        return -EINVAL;
    }

    return ret;
}

三,Kobject引用计数的修改

通过kobject_get和kobject_put可以修改kobject的引用计数,并在计数为0时,调用ktype的release接口,释放占用空间。

1: /* include/linux/kobject.h, line 103 */
2: extern struct kobject *kobject_get(struct kobject *kobj);
3: extern void kobject_put(struct kobject *kobj);
kobject_get,调用kref_get,增加引用计数。
kobject_put,以内部接口kobject_release为参数,调用kref_put。kref模块会在引用计数为零时,调用kobject_release。
==========================内部接口======================================
kobject_release,通过kref结构,获取kobject指针,并调用kobject_cleanup接口继续。
kobject_cleanup,负责释放kobject占用的空间,主要执行逻辑如下:
* 检查该kobject是否有ktype,如果没有,打印警告信息
* 如果该kobject向用户空间发送了ADD uevent但没有发送REMOVE uevent,补发REMOVE uevent
* 如果该kobject有在sysfs文件系统注册,调用kobject_del接口,删除它在sysfs中的注册
* 调用该kobject的ktype的release接口,释放内存空间
* 释放该kobject的name所占用的内存空间

四,kset_create_and_add

1,kset_create_and_add实现

/**
* kset_create_and_add() - Create a struct kset dynamically and add it to sysfs.
*
* @name: the name for the kset
* @uevent_ops: a struct kset_uevent_ops for the kset
* @parent_kobj: the parent kobject of this kset, if any.
*
* This function creates a kset structure dynamically and registers it
* with sysfs.  When you are finished with this structure, call
* kset_unregister() and the structure will be dynamically freed when it
* is no longer being used.
*
* If the kset was not able to be created, NULL will be returned.
*/
struct kset *kset_create_and_add(const char *name,
                 const struct kset_uevent_ops *uevent_ops,
                 struct kobject *parent_kobj)
{
    struct kset *kset;
    int error;

    kset = kset_create(name, uevent_ops, parent_kobj);
    if (!kset)
        return NULL;
    error = kset_register(kset);
    if (error) {
        kfree(kset);
        return NULL;
    }
    return kset;
}
EXPORT_SYMBOL_GPL(kset_create_and_add);

2,函数调用流程

bus_kset = kset_create_and_add("bus", &bus_uevent_ops, NULL);
----kset = kset_create(name, uevent_ops, parent_kobj);
--------kset = kzalloc(sizeof(*kset), GFP_KERNEL);
--------retval = kobject_set_name(&kset->kobj, "%s", name);
--------kset->kobj.ktype = &kset_ktype;
----kset_register(kset);
--------kset_init(k);
------------kobject_init_internal(&k->kobj);
----------------kobj->state_initialized = 1;
--------kobject_add_internal(&k->kobj);
--------kobject_uevent(&k->kobj, KOBJ_ADD);
------------kobject_uevent_env(kobj, action, NULL);
----------------if (uevent_ops && uevent_ops->filter)
----------------kobject_uevent_net_broadcast(kobj, env, action_string, devpath);
--------------------uevent_net_broadcast_untagged(env, action_string, devpath);
------------------------skb = alloc_uevent_skb(env, action_string, devpath);
----------------------------skb_put_data(skb, env->buf, env->buflen);
------------------------netlink_broadcast(uevent_sock, skb_get(skb), 0, 1, GFP_KERNEL);
----------------------------netlink_broadcast_filtered(ssk, skb, portid, group, allocation, NULL, NULL);

使用示例可以参考下一节的“kset/kobj/ktype使用示例"。

五,总结,Ktype以及整个Kobject机制的理解

Kobject的核心功能是:保持一个引用计数,当该计数减为0时,自动释放(由本文所讲的kobject模块负责) Kobject所占用的meomry空间。这就决定了Kobject必须是动态分配的(只有这样才能动态释放)。

而Kobject大多数的使用场景,是内嵌在大型的数据结构中(如Kset、device_driver等),因此这些大型的数据结构,也必须是动态分配、动态释放的。那么释放的时机是什么呢?是内嵌的Kobject释放时。但是Kobject的释放是由Kobject模块自动完成的(在引用计数为0时),那么怎么一并释放包含自己的大型数据结构呢?

这时Ktype就派上用场了。我们知道,Ktype中的release回调函数负责释放Kobject(甚至是包含Kobject的数据结构)的内存空间,那么Ktype及其内部函数,是由谁实现呢?是由上层数据结构所在的模块!因为只有它,才清楚Kobject嵌在哪个数据结构中,并通过Kobject指针以及自身的数据结构类型,找到需要释放的上层数据结构的指针,然后释放它。

讲到这里,就清晰多了。所以,每一个内嵌Kobject的数据结构,例如kset、device、device_driver等等,都要实现一个Ktype,并定义其中的回调函数。同理,sysfs相关的操作也一样,必须经过ktype的中转,因为sysfs看到的是Kobject,而真正的文件操作的主体,是内嵌Kobject的上层数据结构!

顺便提一下,Kobject是面向对象的思想在Linux kernel中的极致体现,但C语言的优势却不在这里,所以Linux kernel需要用比较巧妙(也很啰嗦)的手段去实现。

1,kset_ktype

//定义
static struct kobj_type kset_ktype = {
    .sysfs_ops    = &kobj_sysfs_ops,
    .release    = kset_release,
    .get_ownership    = kset_get_ownership,
};
static void kset_release(struct kobject *kobj)
{
    struct kset *kset = container_of(kobj, struct kset, kobj);
    pr_debug("kobject: '%s' (%p): %s\n",
         kobject_name(kobj), kobj, __func__);
    kfree(kset);
}

//使用
static struct kset *kset_create(const char *name,
                const struct kset_uevent_ops *uevent_ops,
                struct kobject *parent_kobj)
{
    struct kset *kset;
    int retval;

    kset = kzalloc(sizeof(*kset), GFP_KERNEL);
    if (!kset)
        return NULL;
    retval = kobject_set_name(&kset->kobj, "%s", name);
    if (retval) {
        kfree(kset);
        return NULL;
    }
    kset->uevent_ops = uevent_ops;
    kset->kobj.parent = parent_kobj;

    /*
     * The kobject of this kset will have a type of kset_ktype and belong to
     * no kset itself.  That way we can properly free it when it is
     * finished being used.
     */
    kset->kobj.ktype = &kset_ktype;
    kset->kobj.kset = NULL;

    return kset;
}

2,bus_ktype

//定义
static struct kobj_type bus_ktype = {
    .sysfs_ops    = &bus_sysfs_ops,
    .release    = bus_release,
};

static void bus_release(struct kobject *kobj)
{
    struct subsys_private *priv = to_subsys_private(kobj);
    struct bus_type *bus = priv->bus;

    kfree(priv);
    bus->p = NULL;
}

static const struct sysfs_ops bus_sysfs_ops = {
    .show    = bus_attr_show,
    .store    = bus_attr_store,
};

//使用
int bus_register(struct bus_type *bus)
{
    int retval;
    struct subsys_private *priv;
    struct lock_class_key *key = &bus->lock_key;

    priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->bus = bus;
    bus->p = priv;

    BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);

    retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);
    if (retval)
        goto out;

    priv->subsys.kobj.kset = bus_kset;
    priv->subsys.kobj.ktype = &bus_ktype;
    priv->drivers_autoprobe = 1;
    ... ...
}

3,device_ktype

//定义
static struct kobj_type device_ktype = {
    .release    = device_release,
    .sysfs_ops    = &dev_sysfs_ops,
    .namespace    = device_namespace,
    .get_ownership    = device_get_ownership,
};
static void device_release(struct kobject *kobj)
{
    struct device *dev = kobj_to_dev(kobj);
    struct device_private *p = dev->p;

    /*
     * Some platform devices are driven without driver attached
     * and managed resources may have been acquired.  Make sure
     * all resources are released.
     *
     * Drivers still can add resources into device after device
     * is deleted but alive, so release devres here to avoid
     * possible memory leak.
     */
    devres_release_all(dev);

    kfree(dev->dma_range_map);

    if (dev->release)
        dev->release(dev);
    else if (dev->type && dev->type->release)
        dev->type->release(dev);
    else if (dev->class && dev->class->dev_release)
        dev->class->dev_release(dev);
    else
        WARN(1, KERN_ERR "Device '%s' does not have a release() function, it is broken and must be fixed. See Documentation/core-api/kobject.rst.\n",
            dev_name(dev));
    kfree(p);
}

//使用
void device_initialize(struct device *dev)
{
    dev->kobj.kset = devices_kset;
    kobject_init(&dev->kobj, &device_ktype);
    INIT_LIST_HEAD(&dev->dma_pools);
    mutex_init(&dev->mutex);
#ifdef CONFIG_PROVE_LOCKING
    mutex_init(&dev->lockdep_mutex);
#endif
    ... ...
}

4,driver_ktype

//定义
static struct kobj_type driver_ktype = {
    .sysfs_ops    = &driver_sysfs_ops,
    .release    = driver_release,
};
static void driver_release(struct kobject *kobj)
{
    struct driver_private *drv_priv = to_driver(kobj);

    pr_debug("driver: '%s': %s\n", kobject_name(kobj), __func__);
    kfree(drv_priv);
}

//使用
int bus_add_driver(struct device_driver *drv)
{
    struct bus_type *bus;
    struct driver_private *priv;
    int error = 0;

    bus = bus_get(drv->bus);
    if (!bus)
        return -EINVAL;

    pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);

    priv = kzalloc(sizeof(*priv), GFP_KERNEL);
    if (!priv) {
        error = -ENOMEM;
        goto out_put_bus;
    }
    klist_init(&priv->klist_devices, NULL, NULL);
    priv->driver = drv;
    drv->p = priv;
    priv->kobj.kset = bus->p->drivers_kset;
    error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,
                     "%s", drv->name);
    if (error)
        goto out_unregister;
    ... ...
}

参考:

Linux设备模型(2)_Kobject

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1467062.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

架构篇36:微服务架构最佳实践 - 基础设施篇

文章目录 自动化测试自动化部署配置中心接口框架API 网关服务发现服务路由服务容错服务监控服务跟踪服务安全小结每项微服务基础设施都是一个平台、一个系统、一个解决方案,如果要自己实现,其过程和做业务系统类似,都需要经过需求分析、架构设计、开发、测试、部署上线等步骤…

多线程访问共享对象的线程安全问题

如下所示 主线程有一个类A&#xff0c;并且主线程启动后创建了一个子线程t1子线程t1需要访问主线程类A的方法func class A { public:A(){cout<<"A()"<<endl;}~A(){cout<<"~A()"<<endl;}void func(){cout<<"this is a…

了解网络延迟-MDN文档学习笔记

了解延迟 查看更多学习笔记&#xff1a;GitHub&#xff1a;LoveEmiliaForever MDN中文官网 CDN CDN (内容分发网络) 指的是一组分布在各个地区的服务器 这些服务器存储着数据的副本&#xff0c;因此服务器可以根据哪些服务器与用户距离最近&#xff0c;来满足数据的请求 CD…

PostgreSQL索引篇 | BTree

B-Tree索引 &#xff08;本文为《PostgreSQL数据库内核分析》一书的总结笔记&#xff0c;需要电子版的可私信我&#xff09; B树特点&#xff1a; 非叶子节点含一个或多个关键字值和子节点指针&#xff0c;不指向实际数据的存储位置所有关键字都是叶子节点&#xff0c;每个叶…

RabbitMQ(一):消息队列MQ

目录 1 消息队列MQ1.1 MQ简介1、什么是MQ2、MQ的优势流量削峰应用解耦异常处理数据分发分布式事务 3、消息中间件的弊端4、常用的MQ 1.2 MQ中几个基本概念1.3 MQ的通信模式1.4 消息的发布策略1.5 常用消息中间件协议1、AMQP协议2、MQTT协议3、OpenMessage协议4、kafaka协议 1 消…

网络原理-UDP/TCP协议

协议 在网络通信中,协议是非常重要的一个概念,在下面,我将从不同层次对协议进行分析. 应用层 IT职业者与程序打交道最多的一层,调用系统提供的API写出的代码都是属于应用层的. 应用层中有很多现成的协议,但是更多的,我们需要根据实际情况来进行制作自定义协议. 自定义协议…

TensorFlow2.x 精选笔记(1)数据基本操作与线性代数

学习参考&#xff1a; 动手学深度学习2.0Deep-Learning-with-TensorFlow-bookpytorchlightning 一、数组与张量 虽然张量看起来是复杂的对象&#xff0c;但它们可以理解为向量和矩阵的集合。理解向量和矩阵对于理解张量至关重要。 向量是元素的一维列表&#xff0c;向量是一…

mysql和sql server 中如何创建和管理用户

阅读本文之前请参阅----MySQL 数据库安装教程详解&#xff08;linux系统和windows系统&#xff09; 在MySQL和SQL Server中创建和管理用户的过程有所不同。下面分别介绍这两种数据库系统中用户管理的常见步骤。 MySQL 创建用户 在MySQL中创建用户的语法通常如下…

【Java程序设计】【C00262】基于Springboot的会员制医疗预约服务管理系统(有论文)

基于Springboot的会员制医疗预约服务管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的会员制医疗预约服务管理信息系统&#xff0c;本系统分为三种角色&#xff1a;管理员、医生和会员&#xff1b; 在系统…

Shiro 1.2.4反序列化漏洞

一、shiro描述 Apache Shiro是一个强大且易用的Java安全框架&#xff0c;执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API&#xff0c;可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序 二、漏洞原理 AES加密的密钥Key被硬…

NATS学习笔记(一)

NATS是什么&#xff1f; NATS是一个开源的、轻量级、高性能的消息传递系统&#xff0c;它基于发布/订阅模式&#xff0c;由Apcera公司开发和维护。 NATS的功能 发布/订阅&#xff1a;NATS的核心是一个发布/订阅消息传递系统&#xff0c;允许消息生产者发布消息到特定的主题…

每日五道java面试题之spring篇(四)

目录&#xff1a; 第一题 Spring框架的设计目标&#xff0c;设计理念&#xff0c;和核心是什么&#xff1f;第二题. Spring由哪些模块组成&#xff1f;第三题. 详细讲解一下核心容器&#xff08;spring context应用上下文) 模块第四题.Spring框架中有哪些不同类型的事件第五题.…

QT-串口工具

一、演示效果 二、关键程序 &#xff1a; #include "mainwindow.h" #include "ui_mainwindow.h"#include <QMessageBox>MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow),listPlugins(QList<TabPluginInt…

C++笔记:二叉搜索树(Binary Search Tree)

文章目录 二叉搜索树的概念二叉搜索树操作1. 框架搭建2. 遍历3. 查找迭代实现递归实现 4. 插入迭代实现递归实现 5. 删除迭代实现递归实现 6. 析构与销毁7. 拷贝构造与赋值重载 二叉搜索树的应用二叉搜索树的性能分析二叉搜索树模拟实现源码 二叉搜索树的概念 二叉搜索树又称二…

5G网络(接入网+承载网+核心网)

5G网络&#xff08;接入网承载网核心网&#xff09; 一、5G网络全网架构图 这张图分为左右两部分&#xff0c;右边为无线侧网络架构&#xff0c;左边为固定侧网络架构。 无线侧&#xff1a;手机或者集团客户通过基站接入到无线接入网&#xff0c;在接入网侧可以通过RTN或者IP…

欢迎 Gemma: Google 最新推出开源大语言模型

今天&#xff0c;Google 发布了一系列最新的开放式大型语言模型 —— Gemma&#xff01;Google 正在加强其对开源人工智能的支持&#xff0c;我们也非常有幸能够帮助全力支持这次发布&#xff0c;并与 Hugging Face 生态完美集成。 Gemma 提供两种规模的模型&#xff1a;7B 参数…

js之事件代理/事件委托

事件代理也叫事件委托&#xff0c;原理&#xff1a;利用DOM元素的事件冒泡&#xff0c;指定一个事件的处理程序就可以管理某一类型的所有事件。 事件冒泡和事件捕获 如上图所示&#xff0c;事件传播分成三个阶段&#xff1a; 捕获阶段&#xff1a;从window对象传导到目标节点&…

【Qt】信号和槽机制

目录 一、认识信号和槽 二、connect函数 三、自定义槽函数 四、自定义信号 五、带参数的信号和槽 六、信号和槽断开连接 七、信号和槽存在的意义 八、Lambda表达式定义槽函数 一、认识信号和槽 概述 在Qt中&#xff0c;用户和控件的每次交互过程称为一个事件。如"…

【Spring】SpringBoot 单元测试

目 录 一.什么是单元测试&#xff1f;二.单元测试有哪些好处&#xff1f;三.Spring Boot 单元测试使用单元测试的实现步骤 一.什么是单元测试&#xff1f; 单元测试&#xff08;unit testing&#xff09;&#xff0c;是指对软件中的最小可测试单元进行检查和验证的过程就叫单元…

如何查看电脑使用记录?保障个人隐私和安全

查看电脑使用记录是了解电脑活动的一种重要方式&#xff0c;可以帮助用户追踪应用程序的使用情况、登录和关机时间、文件的访问记录等。在本文中&#xff0c;我们将介绍如何查看电脑使用记录的三个方法&#xff0c;以分步骤详细说明如何查看电脑使用记录&#xff0c;帮助用户更…