我们知道,在Android系统中,提供了一个轻量级的日志系统,这个日志系统是以驱动程序的形式实现在内核空间的,而在用户空间分别提供了Java接口和C/C++接口来使用这个日志系统,取决于你编写的是Android应用程序还是系统组件。在前面的文章浅谈Android系统开发中LOG的使用中,已经简要地介绍了在Android应用程序开发中Log的使用方法,在这一篇文章中,我们将更进一步地分析Logger驱动程序的源代码,使得我们对Android日志系统有一个深刻的认识。
既然Android 日志系统是以驱动程序的形式实现在内核空间的,我们就需要获取Android内核源代码来分析了,请参照前面在Ubuntu上下载、编译和安装Android最新源代码和在Ubuntu上下载、编译和安装Android最新内核源代码(Linux Kernel)两篇文章,下载好Android源代码工程。Logger驱动程序主要由两个文件构成,分别是:
kernel/common/drivers/staging/android/logger.h
kernel/common/drivers/staging/android/logger.c
接下来,我们将分别介绍Logger驱动程序的相关数据结构,然后对Logger驱动程序源代码进行情景分析,分别日志系统初始化情景、日志读取情景和日志写入情景。
一. Logger驱动程序的相关数据结构。
我们首先来看logger.h头文件的内容:
#ifndef _LINUX_LOGGER_H
#define _LINUX_LOGGER_H
#include <linux/types.h>
#include <linux/ioctl.h>
struct logger_entry {
__u16 len; /* length of the payload */
__u16 __pad; /* no matter what, we get 2 bytes of padding */
__s32 pid; /* generating process's pid */
__s32 tid; /* generating process's tid */
__s32 sec; /* seconds since Epoch */
__s32 nsec; /* nanoseconds */
char msg[0]; /* the entry's payload */
};
#define LOGGER_LOG_RADIO "log_radio" /* radio-related messages */
#define LOGGER_LOG_EVENTS "log_events" /* system/hardware events */
#define LOGGER_LOG_MAIN "log_main" /* everything else */
#define LOGGER_ENTRY_MAX_LEN (4*1024)
#define LOGGER_ENTRY_MAX_PAYLOAD \
(LOGGER_ENTRY_MAX_LEN - sizeof(struct logger_entry))
#define __LOGGERIO 0xAE
#define LOGGER_GET_LOG_BUF_SIZE _IO(__LOGGERIO, 1) /* size of log */
#define LOGGER_GET_LOG_LEN _IO(__LOGGERIO, 2) /* used log len */
#define LOGGER_GET_NEXT_ENTRY_LEN _IO(__LOGGERIO, 3) /* next entry len */
#define LOGGER_FLUSH_LOG _IO(__LOGGERIO, 4) /* flush log */
#endif /* _LINUX_LOGGER_H */
struct logger_entry是一个用于描述一条Log记录的结构体。len成员变量记录了这条记录的有效负载的长度,有效负载指定的日志记录本身的长度,但是不包括用于描述这个记录的struct logger_entry结构体。回忆一下我们调用android.util.Log接口来使用日志系统时,会指定日志的优先级别Priority、Tag字符串以及Msg字符串,Priority + Tag + Msg三者内容的长度加起来就是记录的有效负载长度了。__pad成员变量是用来对齐结构体的。pid和tid成员变量分别用来记录是哪条进程写入了这条记录。sec和nsec成员变量记录日志写的时间。msg成员变量记录的就有效负载的内容了,它的大小由len成员变量来确定。
接着定义两个宏:
(LOGGER_ENTRY_MAX_LEN - sizeof(struct logger_entry))
从这两个宏可以看出,每条日志记录的有效负载长度加上结构体logger_entry的长度不能超过4K个字节。
logger.h文件中还定义了其它宏,读者可以自己分析,在下面的分析中,碰到时,我们也会详细解释。
再来看logger.c文件中,其它相关数据结构的定义:
/*
* struct logger_log - represents a specific log, such as 'main' or 'radio'
*
* This structure lives from module insertion until module removal, so it does
* not need additional reference counting. The structure is protected by the
* mutex 'mutex'.
*/
struct logger_log {
unsigned char * buffer; /* the ring buffer itself */
struct miscdevice misc; /* misc device representing the log */
wait_queue_head_t wq; /* wait queue for readers */
struct list_head readers; /* this log's readers */
struct mutex mutex; /* mutex protecting buffer */
size_t w_off; /* current write head offset */
size_t head; /* new readers start here */
size_t size; /* size of the log */
};
/*
* struct logger_reader - a logging device open for reading
*
* This object lives from open to release, so we don't need additional
* reference counting. The structure is protected by log->mutex.
*/
struct logger_reader {
struct logger_log * log; /* associated log */
struct list_head list; /* entry in logger_log's list */
size_t r_off; /* current read head offset */
};
/* logger_offset - returns index 'n' into the log via (optimized) modulus */
#define logger_offset(n) ((n) & (log->size - 1))
结构体struct logger_log就是真正用来保存日志的地方了。buffer成员变量变是用保存日志信息的内存缓冲区,它的大小由size成员变量确定。从misc成员变量可以看出,logger驱动程序使用的设备属于misc类型的设备,通过在Android模拟器上执行cat /proc/devices命令(可参考在Ubuntu上下载、编译和安装Android最新内核源代码(Linux Kernel)一文),可以看出,misc类型设备的主设备号是10。关于主设备号的相关知识,可以参考Android学习启动篇一文中提到的Linux Driver Development一书。wq成员变量是一个等待队列,用于保存正在等待读取日志的进程。readers成员变量用来保存当前正在读取日志的进程,正在读取日志的进程由结构体logger_reader来描述。mutex成员变量是一个互斥量,用来保护log的并发访问。可以看出,这里的日志系统的读写问题,其实是一个生产者-消费者的问题,因此,需要互斥量来保护log的并发访问。 w_off成员变量用来记录下一条日志应该从哪里开始写。head成员变量用来表示打开日志文件中,应该从哪一个位置开始读取日志。
结构体struct logger_reader用来表示一个读取日志的进程,log成员变量指向要读取的日志缓冲区。list成员变量用来连接其它读者进程。r_off成员变量表示当前要读取的日志在缓冲区中的位置。
struct logger_log结构体中用于保存日志信息的内存缓冲区buffer是一个循环使用的环形缓冲区,缓冲区中保存的内容是以struct logger_entry为单位的,每个单位的组成为:
struct logger_entry | priority | tag | msg
由于是内存缓冲区buffer是一个循环使用的环形缓冲区,给定一个偏移值,它在buffer中的位置由下logger_offset来确定:
二. Logger驱动程序模块的初始化过程分析。
继续看logger.c文件,定义了三个日志设备:
/*
* Defines a log structure with name 'NAME' and a size of 'SIZE' bytes, which
* must be a power of two, greater than LOGGER_ENTRY_MAX_LEN, and less than
* LONG_MAX minus LOGGER_ENTRY_MAX_LEN.
*/
#define DEFINE_LOGGER_DEVICE(VAR, NAME, SIZE) \
static unsigned char _buf_ ## VAR[SIZE]; \
static struct logger_log VAR = { \
.buffer = _buf_ ## VAR, \
.misc = { \
.minor = MISC_DYNAMIC_MINOR, \
.name = NAME, \
.fops = &logger_fops, \
.parent = NULL, \
}, \
.wq = __WAIT_QUEUE_HEAD_INITIALIZER(VAR .wq), \
.readers = LIST_HEAD_INIT(VAR .readers), \
.mutex = __MUTEX_INITIALIZER(VAR .mutex), \
.w_off = 0, \
.head = 0, \
.size = SIZE, \
};
DEFINE_LOGGER_DEVICE(log_main, LOGGER_LOG_MAIN, 64*1024)
DEFINE_LOGGER_DEVICE(log_events, LOGGER_LOG_EVENTS, 256*1024)
DEFINE_LOGGER_DEVICE(log_radio, LOGGER_LOG_RADIO, 64*1024)
分别是log_main、log_events和log_radio,名称分别LOGGER_LOG_MAIN、LOGGER_LOG_EVENTS和LOGGER_LOG_RADIO,它们的次设备号为MISC_DYNAMIC_MINOR,即为在注册时动态分配。在logger.h文件中,有这三个宏的定义:
注释说明了这三个日志设备的用途。注册的日志设备文件操作方法为logger_fops:
static struct file_operations logger_fops = {
.owner = THIS_MODULE,
.read = logger_read,
.aio_write = logger_aio_write,
.poll = logger_poll,
.unlocked_ioctl = logger_ioctl,
.compat_ioctl = logger_ioctl,
.open = logger_open,
.release = logger_release,
};
日志驱动程序模块的初始化函数为logger_init:
static int __init logger_init(void)
{
int ret;
ret = init_log(&log_main);
if (unlikely(ret))
goto out;
ret = init_log(&log_events);
if (unlikely(ret))
goto out;
ret = init_log(&log_radio);
if (unlikely(ret))
goto out;
out:
return ret;
}
device_initcall(logger_init);
logger_init函数通过调用init_log函数来初始化了上述提到的三个日志设备:
static int __init init_log(struct logger_log *log)
{
int ret;
ret = misc_register(&log->misc);
if (unlikely(ret)) {
printk(KERN_ERR "logger: failed to register misc "
"device for log '%s'!\n", log->misc.name);
return ret;
}
printk(KERN_INFO "logger: created %luK log '%s'\n",
(unsigned long) log->size >> 10, log->misc.name);
return 0;
}
init_log函数主要调用了misc_register函数来注册misc设备,misc_register函数定义在kernel/common/drivers/char/misc.c文件中:
/**
* misc_register - register a miscellaneous device
* @misc: device structure
*
* Register a miscellaneous device with the kernel. If the minor
* number is set to %MISC_DYNAMIC_MINOR a minor number is assigned
* and placed in the minor field of the structure. For other cases
* the minor number requested is used.
*
* The structure passed is linked into the kernel and may not be
* destroyed until it has been unregistered.
*
* A zero is returned on success and a negative errno code for
* failure.
*/
int misc_register(struct miscdevice * misc)
{
struct miscdevice *c;
dev_t dev;
int err = 0;
INIT_LIST_HEAD(&misc->list);
mutex_lock(&misc_mtx);
list_for_each_entry(c, &misc_list, list) {
if (c->minor == misc->minor) {
mutex_unlock(&misc_mtx);
return -EBUSY;
}
}
if (misc->minor == MISC_DYNAMIC_MINOR) {
int i = DYNAMIC_MINORS;
while (--i >= 0)
if ( (misc_minors[i>>3] & (1 << (i&7))) == 0)
break;
if (i<0) {
mutex_unlock(&misc_mtx);
return -EBUSY;
}
misc->minor = i;
}
if (misc->minor < DYNAMIC_MINORS)
misc_minors[misc->minor >> 3] |= 1 << (misc->minor & 7);
dev = MKDEV(MISC_MAJOR, misc->minor);
misc->this_device = device_create(misc_class, misc->parent, dev, NULL,
"%s", misc->name);
if (IS_ERR(misc->this_device)) {
err = PTR_ERR(misc->this_device);
goto out;
}
/*
* Add it to the front, so that later devices can "override"
* earlier defaults
*/
list_add(&misc->list, &misc_list);
out:
mutex_unlock(&misc_mtx);
return err;
}
注册完成后,通过device_create创建设备文件节点。这里,将创建/dev/log/main、/dev/log/events和/dev/log/radio三个设备文件,这样,用户空间就可以通过读写这三个文件和驱动程序进行交互。
三. Logger驱动程序的日志记录读取过程分析。
继续看logger.c 文件,注册的读取日志设备文件的方法为logger_read:
/*
* logger_read - our log's read() method
*
* Behavior:
*
* - O_NONBLOCK works
* - If there are no log entries to read, blocks until log is written to
* - Atomically reads exactly one log entry
*
* Optimal read size is LOGGER_ENTRY_MAX_LEN. Will set errno to EINVAL if read
* buffer is insufficient to hold next entry.
*/
static ssize_t logger_read(struct file *file, char __user *buf,
size_t count, loff_t *pos)
{
struct logger_reader *reader = file->private_data;
struct logger_log *log = reader->log;
ssize_t ret;
DEFINE_WAIT(wait);
start:
while (1) {
prepare_to_wait(&log->wq, &wait, TASK_INTERRUPTIBLE);
mutex_lock(&log->mutex);
ret = (log->w_off == reader->r_off);
mutex_unlock(&log->mutex);
if (!ret)
break;
if (file->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
break;
}
if (signal_pending(current)) {
ret = -EINTR;
break;
}
schedule();
}
finish_wait(&log->wq, &wait);
if (ret)
return ret;
mutex_lock(&log->mutex);
/* is there still something to read or did we race? */
if (unlikely(log->w_off == reader->r_off)) {
mutex_unlock(&log->mutex);
goto start;
}
/* get the size of the next entry */
ret = get_entry_len(log, reader->r_off);
if (count < ret) {
ret = -EINVAL;
goto out;
}
/* get exactly one entry from the log */
ret = do_read_log_to_user(log, reader, buf, ret);
out:
mutex_unlock(&log->mutex);
return ret;
}
注意,在函数开始的地方,表示读取日志上下文的struct logger_reader是保存在文件指针的private_data成员变量里面的,这是在打开设备文件时设置的,设备文件打开方法为logger_open:
/*
* logger_open - the log's open() file operation
*
* Note how near a no-op this is in the write-only case. Keep it that way!
*/
static int logger_open(struct inode *inode, struct file *file)
{
struct logger_log *log;
int ret;
ret = nonseekable_open(inode, file);
if (ret)
return ret;
log = get_log_from_minor(MINOR(inode->i_rdev));
if (!log)
return -ENODEV;
if (file->f_mode & FMODE_READ) {
struct logger_reader *reader;
reader = kmalloc(sizeof(struct logger_reader), GFP_KERNEL);
if (!reader)
return -ENOMEM;
reader->log = log;
INIT_LIST_HEAD(&reader->list);
mutex_lock(&log->mutex);
reader->r_off = log->head;
list_add_tail(&reader->list, &log->readers);
mutex_unlock(&log->mutex);
file->private_data = reader;
} else
file->private_data = log;
return 0;
}
新打开日志设备文件时,是从log->head位置开始读取日志的,保存在struct logger_reader的成员变量r_off中。
start标号处的while循环是在等待日志可读,如果已经没有新的日志可读了,那么就要读进程就要进入休眠状态,等待新的日志写入后再唤醒,这是通过prepare_wait和schedule两个调用来实现的。如果没有新的日志可读,并且设备文件不是以非阻塞O_NONBLOCK的方式打开或者这时有信号要处理(signal_pending(current)),那么就直接返回,不再等待新的日志写入。判断当前是否有新的日志可读的方法是:
ret = (log->w_off == reader->r_off);
即判断当前缓冲区的写入位置和当前读进程的读取位置是否相等,如果不相等,则说明有新的日志可读。
继续向下看,如果有新的日志可读,那么就,首先通过get_entry_len来获取下一条可读的日志记录的长度,从这里可以看出,日志读取进程是以日志记录为单位进行读取的,一次只读取一条记录。get_entry_len的函数实现如下:
/*
* get_entry_len - Grabs the length of the payload of the next entry starting
* from 'off'.
*
* Caller needs to hold log->mutex.
*/
static __u32 get_entry_len(struct logger_log *log, size_t off)
{
__u16 val;
switch (log->size - off) {
case 1:
memcpy(&val, log->buffer + off, 1);
memcpy(((char *) &val) + 1, log->buffer, 1);
break;
default:
memcpy(&val, log->buffer + off, 2);
}
return sizeof(struct logger_entry) + val;
}
上面我们提到,每一条日志记录是由两大部分组成的,一个用于描述这条日志记录的结构体struct logger_entry,另一个是记录体本身,即有效负载。结构体struct logger_entry的长度是固定的,只要知道有效负载的长度,就可以知道整条日志记录的长度了。而有效负载的长度是记录在结构体struct logger_entry的成员变量len中,而len成员变量的地址与struct logger_entry的地址相同,因此,只需要读取记录的开始位置的两个字节就可以了。又由于日志记录缓冲区是循环使用的,这两个节字有可能是第一个字节存放在缓冲区最后一个字节,而第二个字节存放在缓冲区的第一个节,除此之外,这两个字节都是连在一起的。因此,分两种情况来考虑,对于前者,分别通过读取缓冲区最后一个字节和第一个字节来得到日志记录的有效负载长度到本地变量val中,对于后者,直接读取连续两个字节的值到本地变量val中。这两种情况是通过判断日志缓冲区的大小和要读取的日志记录在缓冲区中的位置的差值来区别的,如果相差1,就说明是前一种情况了。最后,把有效负载的长度val加上struct logger_entry的长度就得到了要读取的日志记录的总长度了。
接着往下看,得到了要读取的记录的长度,就调用do_read_log_to_user函数来执行真正的读取动作:
static ssize_t do_read_log_to_user(struct logger_log *log,
struct logger_reader *reader,
char __user *buf,
size_t count)
{
size_t len;
/*
* We read from the log in two disjoint operations. First, we read from
* the current read head offset up to 'count' bytes or to the end of
* the log, whichever comes first.
*/
len = min(count, log->size - reader->r_off);
if (copy_to_user(buf, log->buffer + reader->r_off, len))
return -EFAULT;
/*
* Second, we read any remaining bytes, starting back at the head of
* the log.
*/
if (count != len)
if (copy_to_user(buf + len, log->buffer, count - len))
return -EFAULT;
reader->r_off = logger_offset(reader->r_off + count);
return count;
}
这个函数简单地调用copy_to_user函数来把位于内核空间的日志缓冲区指定的内容拷贝到用户空间的内存缓冲区就可以了,同时,把当前读取日志进程的上下文信息中的读偏移r_off前进到下一条日志记录的开始的位置上。
四. Logger驱动程序的日志记录写入过程分析。
继续看logger.c 文件,注册的写入日志设备文件的方法为logger_aio_write:
/*
* logger_aio_write - our write method, implementing support for write(),
* writev(), and aio_write(). Writes are our fast path, and we try to optimize
* them above all else.
*/
ssize_t logger_aio_write(struct kiocb *iocb, const struct iovec *iov,
unsigned long nr_segs, loff_t ppos)
{
struct logger_log *log = file_get_log(iocb->ki_filp);
size_t orig = log->w_off;
struct logger_entry header;
struct timespec now;
ssize_t ret = 0;
now = current_kernel_time();
header.pid = current->tgid;
header.tid = current->pid;
header.sec = now.tv_sec;
header.nsec = now.tv_nsec;
header.len = min_t(size_t, iocb->ki_left, LOGGER_ENTRY_MAX_PAYLOAD);
/* null writes succeed, return zero */
if (unlikely(!header.len))
return 0;
mutex_lock(&log->mutex);
/*
* Fix up any readers, pulling them forward to the first readable
* entry after (what will be) the new write offset. We do this now
* because if we partially fail, we can end up with clobbered log
* entries that encroach on readable buffer.
*/
fix_up_readers(log, sizeof(struct logger_entry) + header.len);
do_write_log(log, &header, sizeof(struct logger_entry));
while (nr_segs-- > 0) {
size_t len;
ssize_t nr;
/* figure out how much of this vector we can keep */
len = min_t(size_t, iov->iov_len, header.len - ret);
/* write out this segment's payload */
nr = do_write_log_from_user(log, iov->iov_base, len);
if (unlikely(nr < 0)) {
log->w_off = orig;
mutex_unlock(&log->mutex);
return nr;
}
iov++;
ret += nr;
}
mutex_unlock(&log->mutex);
/* wake up any blocked readers */
wake_up_interruptible(&log->wq);
return ret;
}
输入的参数iocb表示io上下文,iov表示要写入的内容,长度为nr_segs,表示有nr_segs个段的内容要写入。我们知道,每个要写入的日志的结构形式为:
struct logger_entry | priority | tag | msg
其中, priority、tag和msg这三个段的内容是由iov参数从用户空间传递下来的,分别对应iov里面的三个元素。而logger_entry是由内核空间来构造的:
struct logger_entry header; struct timespec now;
now = current_kernel_time();
header.pid = current->tgid; header.tid = current->pid; header.sec = now.tv_sec; header.nsec = now.tv_nsec; header.len = min_t(size_t, iocb->ki_left, LOGGER_ENTRY_MAX_PAYLOAD);
然后调用do_write_log首先把logger_entry结构体写入到日志缓冲区中:
/*
* do_write_log - writes 'len' bytes from 'buf' to 'log'
*
* The caller needs to hold log->mutex.
*/
static void do_write_log(struct logger_log *log, const void *buf, size_t count)
{
size_t len;
len = min(count, log->size - log->w_off);
memcpy(log->buffer + log->w_off, buf, len);
if (count != len)
memcpy(log->buffer, buf + len, count - len);
log->w_off = logger_offset(log->w_off + count);
}
由于logger_entry是内核堆栈空间分配的,直接用memcpy拷贝就可以了。
接着,通过一个while循环把iov的内容写入到日志缓冲区中,也就是日志的优先级别priority、日志Tag和日志主体Msg:
while (nr_segs-- > 0) {
size_t len;
ssize_t nr;
/* figure out how much of this vector we can keep */
len = min_t(size_t, iov->iov_len, header.len - ret);
/* write out this segment's payload */
nr = do_write_log_from_user(log, iov->iov_base, len);
if (unlikely(nr < 0)) {
log->w_off = orig;
mutex_unlock(&log->mutex);
return nr;
}
iov++;
ret += nr;
}
由于iov的内容是由用户空间传下来的,需要调用do_write_log_from_user来写入:
static ssize_t do_write_log_from_user(struct logger_log *log,
const void __user *buf, size_t count)
{
size_t len;
len = min(count, log->size - log->w_off);
if (len && copy_from_user(log->buffer + log->w_off, buf, len))
return -EFAULT;
if (count != len)
if (copy_from_user(log->buffer, buf + len, count - len))
return -EFAULT;
log->w_off = logger_offset(log->w_off + count);
return count;
}
这里,我们还漏了一个重要的步骤:
/*
* Fix up any readers, pulling them forward to the first readable
* entry after (what will be) the new write offset. We do this now
* because if we partially fail, we can end up with clobbered log
* entries that encroach on readable buffer.
*/
fix_up_readers(log, sizeof(struct logger_entry) + header.len);
为什么要调用fix_up_reader这个函数呢?这个函数又是作什么用的呢?是这样的,由于日志缓冲区是循环使用的,即旧的日志记录如果没有及时读取,而缓冲区的内容又已经用完时,就需要覆盖旧的记录来容纳新的记录。而这部分将要被覆盖的内容,有可能是某些reader的下一次要读取的日志所在的位置,以及为新的reader准备的日志开始读取位置head所在的位置。因此,需要调整这些位置,使它们能够指向一个新的有效的位置。我们来看一下fix_up_reader函数的实现:
/*
* fix_up_readers - walk the list of all readers and "fix up" any who were
* lapped by the writer; also do the same for the default "start head".
* We do this by "pulling forward" the readers and start head to the first
* entry after the new write head.
*
* The caller needs to hold log->mutex.
*/
static void fix_up_readers(struct logger_log *log, size_t len)
{
size_t old = log->w_off;
size_t new = logger_offset(old + len);
struct logger_reader *reader;
if (clock_interval(old, new, log->head))
log->head = get_next_entry(log, log->head, len);
list_for_each_entry(reader, &log->readers, list)
if (clock_interval(old, new, reader->r_off))
reader->r_off = get_next_entry(log, reader->r_off, len);
}
判断log->head和所有读者reader的当前读偏移reader->r_off是否在被覆盖的区域内,如果是,就需要调用get_next_entry来取得下一个有效的记录的起始位置来调整当前位置:
/*
* get_next_entry - return the offset of the first valid entry at least 'len'
* bytes after 'off'.
*
* Caller must hold log->mutex.
*/
static size_t get_next_entry(struct logger_log *log, size_t off, size_t len)
{
size_t count = 0;
do {
size_t nr = get_entry_len(log, off);
off = logger_offset(off + nr);
count += nr;
} while (count < len);
return off;
}
而判断log->head和所有读者reader的当前读偏移reader->r_off是否在被覆盖的区域内,是通过clock_interval函数来实现的:
/*
* clock_interval - is a < c < b in mod-space? Put another way, does the line
* from a to b cross c?
*/
static inline int clock_interval(size_t a, size_t b, size_t c)
{
if (b < a) {
if (a < c || b >= c)
return 1;
} else {
if (a < c && b >= c)
return 1;
}
return 0;
}
最后,日志写入完毕,还需要唤醒正在等待新日志的reader进程:
/ wake up any blocked readers / wake_up_interruptible(&log->wq);
至此, Logger驱动程序的主要逻辑就分析完成了,还有其它的一些接口,如logger_poll、 logger_ioctl和logger_release函数,比较简单,读取可以自行分析。这里还需要提到的一点是,由于Logger驱动程序模块在退出系统时,是不会卸载的,所以这个模块没有module_exit函数,而对于模块里面定义的对象,也没有用对引用计数技术。
这篇文章着重介绍了Android日志系统在内核空间的实现,在下一篇文章中,我们将接着介绍在用户空间中,提供给Android应用程序使用的Java和C/C++ LOG调用接口的实现过程,敬请关注。
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。