转自:https://blog.csdn.net/skyflying2012/article/details/41078349

最近工作在调试usb虚拟串口,让其作为kernel启动的调试串口,以及user空间的输入输出控制台。

利用这个机会,学习下printk如何选择往哪个console输出以及user空间下控制台如何选择,记录与此,与大家共享,也方便自己以后翻阅。

Kernel版本号:3.4.55

依照我的思路(还是时间顺序)分了4部分,指定kernel调试console ,  kernel下printk console的选择 ,kernel下console的注册,user空间console的选择。

一 指定kernel调试console

首先看kernel启动时如何获取和处理指定的console参数。

kernel的启动参数cmdline可以指定调试console,如指定‘console=ttyS0,115200’,

kernel如何解析cmdline,我之前写了一篇博文如下:

http://blog.csdn.net/skyflying2012/article/details/41142801

根据之前的分析,cmdline中有console=xxx,start_kernel中parse_args遍历.init.setup段所有obs_kernel_param。

kernel/printk.c中注册了‘console=’的解析函数console_setup(注册了obs_kernel_param),所以匹配成功,会调用console_setup来解析,如下:

static int __init console_setup(char *str)
{
char buf[sizeof(console_cmdline[0].name) + 4]; /* 4 for index */
char *s, *options, *brl_options = NULL;
int idx; #ifdef CONFIG_A11Y_BRAILLE_CONSOLE
if (!memcmp(str, "brl,", 4)) {
brl_options = "";
str += 4;
} else if (!memcmp(str, "brl=", 4)) {
brl_options = str + 4;
str = strchr(brl_options, ',');
if (!str) {
printk(KERN_ERR "need port name after brl=\n");
return 1;
}
*(str++) = 0;
}
#endif /*
* Decode str into name, index, options.
*/
if (str[0] >= '0' && str[0] <= '9') {
strcpy(buf, "ttyS");
strncpy(buf + 4, str, sizeof(buf) - 5);
} else {
strncpy(buf, str, sizeof(buf) - 1);
}
buf[sizeof(buf) - 1] = 0;
if ((options = strchr(str, ',')) != NULL)
*(options++) = 0;
#ifdef __sparc__
if (!strcmp(str, "ttya"))
strcpy(buf, "ttyS0");
if (!strcmp(str, "ttyb"))
strcpy(buf, "ttyS1");
#endif
for (s = buf; *s; s++)
if ((*s >= '0' && *s <= '9') || *s == ',')
break;
idx = simple_strtoul(s, NULL, 10);
*s = 0; __add_preferred_console(buf, idx, options, brl_options);
console_set_on_cmdline = 1;
return 1;
}
__setup("console=", console_setup);

参数是console=的值字符串,如“ttyS0,115200”,console_setup对console=参数值做解析,以ttyS0,115200为例,最后buf=“ttyS”,idx=0,options="115200",brl_options=NULL。调用__add_preferred_console如下:

/*
* If exclusive_console is non-NULL then only this console is to be printed to.
*/
static struct console *exclusive_console; /*
* Array of consoles built from command line options (console=)
*/
struct console_cmdline
{
char name[8]; /* Name of the driver */
int index; /* Minor dev. to use */
char *options; /* Options for the driver */
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
char *brl_options; /* Options for braille driver */
#endif
}; #define MAX_CMDLINECONSOLES 8 static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];
static int selected_console = -1;
static int preferred_console = -1;
int console_set_on_cmdline;
EXPORT_SYMBOL(console_set_on_cmdline);
static int __add_preferred_console(char *name, int idx, char *options,
char *brl_options)
{
struct console_cmdline *c;
int i; /*
* See if this tty is not yet registered, and
* if we have a slot free.
*/
for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++)
if (strcmp(console_cmdline[i].name, name) == 0 &&
console_cmdline[i].index == idx) {
if (!brl_options)
selected_console = i;
return 0;
}
if (i == MAX_CMDLINECONSOLES)
return -E2BIG;
if (!brl_options)
selected_console = i;
c = &console_cmdline[i];
strlcpy(c->name, name, sizeof(c->name));
c->options = options;
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
c->brl_options = brl_options;
#endif
c->index = idx;
return 0;
}

kernel利用结构体数组console_cmdline[8],最多可支持8个cmdline传入的console参数。

__add_preferred_console将name idx options保存到数组下一个成员console_cmdline结构体中,如果数组中已有重名,则不添加,并置selected_console为最新添加的console_cmdline的下标号。

比如cmdline中有“console=ttyS0,115200 console=ttyS1,9600”

则在console_cmdline[8]数组中console_cmdline[0]代表ttyS0,console_cmdline[1]代表ttyS1,而selected_console=1.

二 kernel下printk console的选择

kernel下调试信息是通过printk输出,如果要kernel正常打印,则需要搞明白printk怎么选择输出的设备。

关于printk的实现原理,我在刚工作的时候写过一篇博文,kernel版本是2.6.21的,但是原理还是一致的,可供参考:

http://blog.csdn.net/skyflying2012/article/details/7970341

printk首先将输出内容添加到一个kernel缓冲区中,叫log_buf,log_buf相关代码如下:

#define MAX_CMDLINECONSOLES 8

static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];
static int selected_console = -1;
static int preferred_console = -1;
int console_set_on_cmdline;
EXPORT_SYMBOL(console_set_on_cmdline); /* Flag: console code may call schedule() */
static int console_may_schedule; #ifdef CONFIG_PRINTK static char __log_buf[__LOG_BUF_LEN];
static char *log_buf = __log_buf;
static int log_buf_len = __LOG_BUF_LEN;
static unsigned logged_chars; /* Number of chars produced since last read+clear operation */
static int saved_console_loglevel = -1;

log_buf的大小由kernel menuconfig配置,我配置的CONFIG_LOG_BUF_SHIFT为17,则log_buf为128k。

printk内容会一直存在log_buf中,log_buf满了之后则会从头在开始存,覆盖掉原来的数据。

根据printk的实现原理,printk最后调用console_unlock实现log_buf数据刷出到指定设备。

这里先不关心printk如何处理log buf数据(比如添加内容级别),只关心printk如何一步步找到指定的输出设备,根据printk.c代码,可以找到如下线索。

printk->vprintk->console_unlock->call_console_drivers->_call_console_drivers->_call_console_drivers->__call_console_drivers

看线索最底层__call_console_drivers代码。如下:

/*
* Call the console drivers on a range of log_buf
*/
static void __call_console_drivers(unsigned start, unsigned end)
{
struct console *con; for_each_console(con) {
if (exclusive_console && con != exclusive_console)
continue;
if ((con->flags & CON_ENABLED) && con->write &&
(cpu_online(smp_processor_id()) ||
(con->flags & CON_ANYTIME)))
con->write(con, &LOG_BUF(start), end - start);
}
}

for_each_console定义如下:

/*
* for_each_console() allows you to iterate on each console
*/
#define for_each_console(con) \
for (con = console_drivers; con != NULL; con = con->next)

遍历console_drivers链表所有console struct,如果有exclusive_console,则调用与exclusive_console一致console的write,

如果exclusive_console为NULL,则调用所有ENABLE的console的write方法将log buf中start到end的内容发出。

可以看出,execlusive_console来指定printk输出唯一console,如果未指定,则向所有enable的console写。

默认情况下execlusive_console=NULL,所以printk默认是向所有enable的console写!

只有一种情况是指定execlusive_console,就是在console注册时,下面会讲到。

到这里就很明了了,kernel下每次printk打印,首先存log_buf,然后遍历console_drivers,找到合适console(execlusive_console或所有enable的),刷出log。

console_drivers链表的成员是哪里来的,谁会指定execulsive_console?接着来看下一部分,kernel下console的注册

三 kernel下console的注册

上面分析可以看出,作为kernel移植最基本的一步,kernel下printk正常输出,最重要的一点是在console_drivers链表中添加console struct。那谁来完成这个工作?

答案是register_console函数,在printk.c中,下面来分析下该函数。

void register_console(struct console *newcon)
{
int i;
unsigned long flags;
struct console *bcon = NULL; //如果注册的是bootconsole(kernel早期启动打印),需要检查console_drivers中
//没有“real console”也就是说bootconsole必须是第一个注册的console。
if (console_drivers && newcon->flags & CON_BOOT) {
/* find the last or real console */
for_each_console(bcon) {
if (!(bcon->flags & CON_BOOT)) {
printk(KERN_INFO "Too late to register bootconsole %s%d\n",
newcon->name, newcon->index);
return;
}
}
} if (console_drivers && console_drivers->flags & CON_BOOT)
bcon = console_drivers; //preferred console为console_cmdline中最后一个console
if (preferred_console < 0 || bcon || !console_drivers)
preferred_console = selected_console; if (newcon->early_setup)
newcon->early_setup(); if (preferred_console < 0) {
if (newcon->index < 0)
newcon->index = 0;
if (newcon->setup == NULL ||
newcon->setup(newcon, NULL) == 0) {
newcon->flags |= CON_ENABLED;
if (newcon->device) {
newcon->flags |= CON_CONSDEV;
preferred_console = 0;
}
}
} //检查newcon是否是cmdline指定的console,如果是,则使能(CON_ENABLE)并初始化该console
for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0];
i++) {
if (strcmp(console_cmdline[i].name, newcon->name) != 0)
continue;
if (newcon->index >= 0 &&
newcon->index != console_cmdline[i].index)
continue;
if (newcon->index < 0)
newcon->index = console_cmdline[i].index;
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
if (console_cmdline[i].brl_options) {
newcon->flags |= CON_BRL;
braille_register_console(newcon,
console_cmdline[i].index,
console_cmdline[i].options,
console_cmdline[i].brl_options);
return;
}
#endif
if (newcon->setup &&
newcon->setup(newcon, console_cmdline[i].options) != 0)
break;
newcon->flags |= CON_ENABLED;
newcon->index = console_cmdline[i].index;
if (i == selected_console) {
//如果newcon是cmdline指定的最新的console,则置位CONSDEV
newcon->flags |= CON_CONSDEV;
preferred_console = selected_console;
}
break;
} //该console没有使能,退出
if (!(newcon->flags & CON_ENABLED))
return; //如果有bootconsole,则newcon不需要输出register之前的log,因为如果bootconsole和newcon是同一个设备
//则之前的log就输出2次
if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV))
newcon->flags &= ~CON_PRINTBUFFER; //把newcon加入console_drivers链表,对于置位CON_CONSDEV的con,放在链表首
console_lock();
if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) {
newcon->next = console_drivers;
console_drivers = newcon;
if (newcon->next)
newcon->next->flags &= ~CON_CONSDEV;
} else {
newcon->next = console_drivers->next;
console_drivers->next = newcon;
}
if (newcon->flags & CON_PRINTBUFFER) {
//如果newcon置位PRINTBUFFER,则将log全部刷出
raw_spin_lock_irqsave(&logbuf_lock, flags);
con_start = log_start;
raw_spin_unlock_irqrestore(&logbuf_lock, flags);
//修改printk输出的指定唯一exclusive_console为newcon
//保证将之前的log只输出到newcon
exclusive_console = newcon;
}
//解锁console,刷出log到newcon
console_unlock();
console_sysfs_notify(); //如果有bootconsole,则unregister bootconsole(从console_drivers中删掉)
//并告诉使用者现在console切换
if (bcon &&
((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV) &&
!keep_bootcon) {
/* we need to iterate through twice, to make sure we print
* everything out, before we unregister the console(s)
*/
printk(KERN_INFO "console [%s%d] enabled, bootconsole disabled\n",
newcon->name, newcon->index);
for_each_console(bcon)
if (bcon->flags & CON_BOOT)
unregister_console(bcon);
} else {
printk(KERN_INFO "%sconsole [%s%d] enabled\n",
(newcon->flags & CON_BOOT) ? "boot" : "" ,
newcon->name, newcon->index);
}
}

如果之前注册了bootconsole,则不会将该次register之前的log刷出,防止bootconsole和该次注册的newcon是同一个物理设备时,log打印2次。

如果没有bootconsole,则会指定exclusive_console=newcon,console_unlock时,刷新全部log到该指定exclusive console。

console_unlock结束时会将exclusive_console置NULL,所以exclusive console默认情况下就是NULL。

最后会unregister bootconsole,是将bootconsole从console_drivers中删除,这样之后的printk就不会想bootconsole输出了。

有意思的一个地方是,在unregister bootconsole之前的printk:

printk(KERN_INFO "console [%s%d] enabled, bootconsole disabled\n",
newcon->name, newcon->index);

因为此时bootconsole还没删掉,而newconsole已经加入console_drivers,如果bootconsole和newconsole是同一个物理设备,我们会看到这句printk会出现2次哦!

如果在cmdline指定2个I/O设备,如“console==ttyS0,115200 console=ttyS1,115200”,因ttyS设备都是serial driver中注册的real console,所以会看到kernel的打印分别出现在2个串口上!

boot console和real console差别在于bootconsole注册于kernel启动早期,方便对于kernel早期启动进行调试打印。

那这些console是在哪里调用register_console进行注册的?

bootconsole的注册,如arch/arm/kernel/early_printk.c,是在parse_args参数解析阶段注册bootconsole。

在start_kernel中console_init函数也会遍历.con_initcall.init段中所有注册函数,而这些注册函数也可以来注册bootconsole。

.con_initcall.init段中函数的注册可以使用宏定义console_initcall。这些函数中调用register_console,方便在kernel初期实现printk打印。

realconsole的注册,是在各个driver,如serial加载时完成。

经过上面分析,对于一个新实现的输入输出设备,如果要将其作为kernel下的printk调试输出设备,需要2步:

(1)register console,console struct如下:

struct console {
char name[16];
void (*write)(struct console *, const char *, unsigned);
int (*read)(struct console *, char *, unsigned);
struct tty_driver *(*device)(struct console *, int *);
void (*unblank)(void);
int (*setup)(struct console *, char *);
int (*early_setup)(void);
short flags;
short index;
int cflag;
void *data;
struct console *next;
};

定义一个console,因为kernel调试信息是单向的,没有交互,所以只需要实现write即可,还需要实现setup函数,进行设备初始化(如设置波特率等),以及标志位flags(将所有log刷出),举个例子,如下:

static struct console u_console =
{
.name = "ttyS",
.write = u_console_write,
.setup = u_console_setup,
.flags = CON_PRINTBUFFER,
.index = 0,
.data = &u_reg,
};static int __init
u_console_init(void)
{
register_console(&u_console);
return 0;
}

为了调试方便,可以在console_init调用该函数进行注册,则需要

console_initcall(u_console_init);

也可以在kernel加载driver时调用,则需要在driver的probe时调用u_console_init,但是这样只能等driver调register_console之后,console_unlock才将所有log刷出,之前的log都会存在log buf中。

(2)cmdline指定调试console,在kernel的cmdline添加参数console=ttyS0,115200

四 user空间console的选择

用户空间的输入输出依赖于其控制台使用的哪个,这里有很多名词,如控制台,tty,console等,这些名字我也很晕,不用管他们的真正含义,搞嵌入式,直接找到它的实现,搞明白从最上层软件,到最底层硬件,如何操作,还有什么会不清楚呢。

在start_kernel中最后起内核init进程时,如下:

/* Open the /dev/console on the rootfs, this should never fail */
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n"); (void) sys_dup(0);
(void) sys_dup(0);

去打开console设备,console设备做了控制台。

console设备文件的创建在driver/tty/tty_io.c中,如下:

static const struct file_operations console_fops = {
.llseek = no_llseek,
.read = tty_read,
.write = redirected_tty_write,
.poll = tty_poll,
.unlocked_ioctl = tty_ioctl,
.compat_ioctl = tty_compat_ioctl,
.open = tty_open,
.release = tty_release,
.fasync = tty_fasync,
};
int __init tty_init(void)
{
cdev_init(&tty_cdev, &tty_fops);
if (cdev_add(&tty_cdev, MKDEV(TTYAUX_MAJOR, 0), 1) ||
register_chrdev_region(MKDEV(TTYAUX_MAJOR, 0), 1, "/dev/tty") < 0)
panic("Couldn't register /dev/tty driver\n");
device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 0), NULL, "tty"); cdev_init(&console_cdev, &console_fops);
if (cdev_add(&console_cdev, MKDEV(TTYAUX_MAJOR, 1), 1) ||
register_chrdev_region(MKDEV(TTYAUX_MAJOR, 1), 1, "/dev/console") < 0)
panic("Couldn't register /dev/console driver\n");
consdev = device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 1), NULL,
"console");
if (IS_ERR(consdev))
consdev = NULL;
else
WARN_ON(device_create_file(consdev, &dev_attr_active) < 0); #ifdef CONFIG_VT
vty_init(&console_fops);
#endif
return 0;
}

console的操作函数都是使用的tty的操作函数,看open的实现,如何找到具体的操作设备:

static int tty_open(struct inode *inode, struct file *filp)
{
struct tty_struct *tty;
int noctty, retval;
struct tty_driver *driver = NULL;
int index;
dev_t device = inode->i_rdev;
unsigned saved_flags = filp->f_flags; nonseekable_open(inode, filp); retry_open:
retval = tty_alloc_file(filp);
if (retval)
return -ENOMEM; noctty = filp->f_flags & O_NOCTTY;
index = -1;
retval = 0; mutex_lock(&tty_mutex);
tty_lock(); tty = tty_open_current_tty(device, filp);
if (IS_ERR(tty)) {
retval = PTR_ERR(tty);
goto err_unlock;
} else if (!tty) {
driver = tty_lookup_driver(device, filp, &noctty, &index);
if (IS_ERR(driver)) {
retval = PTR_ERR(driver);
goto err_unlock;
} /* check whether we're reopening an existing tty */
tty = tty_driver_lookup_tty(driver, inode, index);
if (IS_ERR(tty)) {
retval = PTR_ERR(tty);
goto err_unlock;
}
}

}


首先tty_open_current_tty找该进程所对应的tty,因为init进程我们并没有制定tty,所以该函数返回NULL。

接下来调用tty_lookup_driver,如下:

static struct tty_driver *tty_lookup_driver(dev_t device, struct file *filp,
int *noctty, int *index)
{
struct tty_driver *driver; switch (device) {
#ifdef CONFIG_VT
case MKDEV(TTY_MAJOR, 0): {
extern struct tty_driver *console_driver;
driver = tty_driver_kref_get(console_driver);
*index = fg_console;
*noctty = 1;
break;
}
#endif
case MKDEV(TTYAUX_MAJOR, 1): {
struct tty_driver *console_driver = console_device(index);
if (console_driver) {
driver = tty_driver_kref_get(console_driver);
if (driver) {
/* Don't let /dev/console block */
filp->f_flags |= O_NONBLOCK;
*noctty = 1;
break;
}
}
return ERR_PTR(-ENODEV);
}
default:
driver = get_tty_driver(device, index);
if (!driver)
return ERR_PTR(-ENODEV);
break;
}
return driver;
}

console设备文件,次设备号是1,根据代码,会调用console_device来获取对应的tty_driver,如下:

struct tty_driver *console_device(int *index)
{
struct console *c;
struct tty_driver *driver = NULL; console_lock();
for_each_console(c) {
if (!c->device)
continue;
driver = c->device(c, index);
if (driver)
break;
}
console_unlock();
return driver;
}

又遇到了熟悉的for_each_console,遍历console_drivers链表,对于存在device成员的console,调用device方法,获取tty_driver,退出遍历。

之后对于该console设备的读写操作都是基于该tty_driver。

所有的输入输出设备都会注册tty_driver。

所以,对于一个新实现的输入输出设备,如果想让其即作为kernel的printk输出设备,也作为user空间的控制台,则需要在上面u_console基础上再实现device方法成员,来返回该设备的tty_driver。

那么还有一个问题:

如果cmdline指定2个I/O设备,“console=ttyS0,115200 console=ttyS1,115200”,user空间选择哪个作为console?

用户空间console open时,console_device遍历console_drivers,找到有device成员的console,获取tty_driver,就会退出遍历。

所以哪个console放在console_drivers前面,就会被选择为user空间的console。

在分析register_console时,如果要注册的newcon是cmdline指定的最新的console(i = selected_console),则置位CON_CONSDEV,

而在后面newcon加入console_drivers时,判断该置位,置位CON_CONSDEV,则将newcon加入到console_drivers的链表头,否则插入到后面。

所以这里user空间会选择ttyS1作为用户控件的console!

总结下,kernel和user空间下都有一个console,关系到kernel下printk的方向和user下printf的方向,实现差别还是很大的。

kernel下的console是输入输出设备driver中实现的简单的输出console,只实现write函数,并且是直接输出到设备。

user空间下的console,实际就是tty的一个例子,所有操作函数都继承与tty,全功能,可以打开 读写 关闭,所以对于console的读写,都是由kernel的tty层来最终发送到设备。

kernel的tty层之下还有ldisc线路规程层,线路规程层之下才是具体设备的driver。

ldisc层处理一些对于控制台来说有意义的输入输出字符,比如输入的crtl+C,输出的‘\n‘进过线路规程会变为’\n\r‘。

所以对于kernel下console的write方法,不要忘记,对于log buf中'\n'的处理,实现一个简单的线路规程!

linux kernel下输入输出console如何实现【转】的更多相关文章

  1. linux kernel下输入输出console怎样实现

    近期工作在调试usb虚拟串口,让其作为kernel启动的调试串口,以及user空间的输入输出控制台. 利用这个机会,学习下printk怎样选择往哪个console输出以及user空间下控制台怎样选择. ...

  2. linux 环境下 eas console的运行

    1)访问 http://<HOST>:19000/easconsole/ 2)然后下载 jnlp 文件. 3)找个jre, 用javaws 运行 jnlp文件

  3. <摘录>Linux 环境下编译 0.11版本内核 kernel

    系统环境:Fedora 13 + gcc-4.4.5 最近在看<linux内核0.11完全注释>一书,由于书中涉及汇编语言的地方众多,本人在大学时汇编语言学得一塌糊涂,所以实在看不下去了, ...

  4. Linux kernel 2.6下的modules编译与KBuild

    转载:http://blog.sina.com.cn/s/blog_602f87700100dq1u.html Sam之前在Linux kernel 2.4下写过一些driver.但自从转到kerne ...

  5. .NET Core学习笔记(1)——在Linux下运行Console APP

    都说.NET Core可以跨平台,说实话Linux咱也不太懂,咱也不敢问.怎样把一个简单的Console App在Linux下跑起来,真是费了我一番功夫.特做此篇以供指北. .NET Core的大饼我 ...

  6. Linux Kernel之flush_cache_all在ARM平台下是如何实现的【转】

    转自:http://blog.csdn.net/u011461299/article/details/10199989 版权声明:本文为博主原创文章,未经博主允许不得转载. 在驱动程序的设计中,我们可 ...

  7. Ubantu下编译Linux Kernel

    wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.3.tar.gztar -xzf linux-3.9.3.tar.gzcd li ...

  8. linux 终端下敲ctrl-c时,到底发生了什么?(转)

    通过telnet登录到单板,然后按ctrl-c会发生什么情况,流程是怎么样的? 在分析之前,先介绍tty的相关知识.我们可以认为,所有跟输入输出相关的操作,最终都由tty来接管.举例来说,当我们敲 l ...

  9. 【嵌入式开发】 Linux Kernel 下载 配置 编译 安装 及 驱动简介

    作者 : 韩曙亮 转载请出名出处 : http://blog.csdn.net/shulianghan/article/details/38636827 一. Linux 内核简介 1. 内核功能简介 ...

随机推荐

  1. SpringCloud路由网关Zuul

    一.什么是网关 Zuul的主要功能是路由转发和过滤器.路由功能是微服务的一部分,比如/api/user转发到到user服务,/api/shop转发到到shop服务.zuul默认和Ribbon结合实现了 ...

  2. Python中删除空白字符

    主要参考 Stackoverflow答案总结. 空白字符一般指以下几种字符: space,tab, linefeed, return, formfeed, and vertical tab中英文对照表 ...

  3. IT兄弟连 HTML5教程 DIV+CSS网页标准化布局的优势

    标准的网页都需要对内容进行布局,以前都是采用表格的定位技术,从2005年开始逐步转向DIV+CSS的布局方式,目前绝大多数的网站都是采用这种布局方式.使用DIV+CSS对网站进行布局符合W3C标准,采 ...

  4. 每个Web开发者都该了解的12条命令行

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者.原文出处:https://tutorialzine.com/2017/08/12-terminal-comma ...

  5. Java对象 POJO和JavaBean的区别

    转载自https://www.jianshu.com/p/224489dfdec8 这篇博客很通俗易懂的讲明白了什么是POJO,对于刚开始学开发做java项目的菜鸟来说,很有帮助,网课老师是不会讲这些 ...

  6. Consul初探-在深交之前先认识

    Consul 是什么? Consul 官方站点:https://www.consul.io/ 首先,官方介绍是:Consul 是一种服务网格的解决方案,在 Consul 中,提供了服务发现.配置.分段 ...

  7. Ligg.EasyWinApp-102-Ligg.EasyWinForm:Function--ControlBox、Tray、Resize、Menu

    首先请在VS里打开下面的文件,我们将对源码分段进行说明: Function(功能):一个应用的功能界面,一个应用对应多个Function(功能):如某应用可分为管理员界面.用户界面. 首先我们来看一下 ...

  8. GitLab-使用SSH的方式拉取和推送项目

    场景 Docker Compose部署GitLab服务,搭建自己的代码托管平台(图文教程): https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/det ...

  9. cocos2d游戏jsc文件格式解密,SpideMonkey大冒险

    “ 介绍cocos2d游戏中常用的jsc格式文件的解密.” 01 — 在破解游戏应用中,经常会碰到后缀为jsc的文件,这是基于cocos2d开发的游戏的加密代码,本质上是js文件,只是被加密了. 例如 ...

  10. Oracle 定时备份数据库

    [操作说明] 在前面的博客中,学习了如何Oracle如何备份数据库,实际开发过程中数据库应该每隔一段时间就要备份一次,所以我们就需要一个定时执行这个代码的功能,同时备份的文件可能进行一些处理,比如压缩 ...