------------------------------------------

本文系本站原创,欢迎转载!------------------------------------------我们现在congtty core层的file operations开始分析。

static const struct file_operations tty_fops = {

.llseek        = no_llseek,

.read        = tty_read,

.write        = tty_write,

.poll        = tty_poll,

.unlocked_ioctl    = tty_ioctl,

.compat_ioctl    = tty_compat_ioctl,

.open        = tty_open,

.release    = tty_release,

.fasync        = tty_fasync,

};

先看open函数:

static int tty_open(struct inode *inode, struct file *filp)

{

int ret;

lock_kernel(); //哇哈哈,第一次看到传说中的大内核锁

ret = __tty_open(inode, filp);

unlock_kernel();

return ret;

}

转而调用__tty_open(inode, filp):

static int __tty_open(struct inode *inode, struct file *filp)

{

......

got_driver:

if (!tty) {

/* check whether we're reopening an existing tty */

tty = tty_driver_lookup_tty(driver, inode, index);

if (IS_ERR(tty)) {

mutex_unlock(&tty_mutex);

return PTR_ERR(tty);

}

}

if (tty) {

retval = tty_reopen(tty);

if (retval)

tty = ERR_PTR(retval);

} else

tty = tty_init_dev(driver, index, 0);//初始化一个tty设备,初始化线路规程和打开线路规程, 见1

mutex_unlock(&tty_mutex);

tty_driver_kref_put(driver);

if (IS_ERR(tty))

return PTR_ERR(tty);

filp->private_data = tty;//私有数据设置,read/write函数都将用到这个变量

file_move(filp, &tty->tty_files);

check_tty_count(tty, "tty_open");

if (tty->driver->type == TTY_DRIVER_TYPE_PTY &&

tty->driver->subtype == PTY_TYPE_MASTER)

noctty = 1;

#ifdef TTY_DEBUG_HANGUP

printk(KERN_DEBUG "opening %s...", tty->name);

#endif

if (!retval) {

if (tty->ops->open)

retval = tty->ops->open(tty, filp);//ops对应driver的ops,即uart_ops,也就是调用serial_core中的uart_open,见2

else

retval = -ENODEV;

}

filp->f_flags = saved_flags;

......

}

1,tty_init_dev(driver, index, 0):

struct tty_struct *tty_init_dev(struct tty_driver *driver, int idx,

int first_ok)

{

......

tty = alloc_tty_struct();

if (!tty)

goto fail_no_mem;

initialize_tty_struct(tty, driver, idx);//初始化tty结构,见1-1

retval = tty_driver_install_tty(driver, tty);

if (retval < 0) {

free_tty_struct(tty);

module_put(driver->owner);

return ERR_PTR(retval);

}

/*

* Structures all installed ... call the ldisc open routines.

* If we fail here just call release_tty to clean up.  No need

* to decrement the use counts, as release_tty doesn't care.

*/

retval = tty_ldisc_setup(tty, tty->link);//打开线路规程,在initialize_tty_struct中有对线路规程初始化,见1-2

if (retval)

goto release_mem_out;

return tty;

......

}

1-1,initialize_tty_struct(tty, driver, idx):

void initialize_tty_struct(struct tty_struct *tty,

struct tty_driver *driver, int idx)

{

memset(tty, 0, sizeof(struct tty_struct));

kref_init(&tty->kref);

tty->magic = TTY_MAGIC;

tty_ldisc_init(tty);//线路规程初始化,见1-1-1

tty->session = NULL;

tty->pgrp = NULL;

tty->overrun_time = jiffies;

tty->buf.head = tty->buf.tail = NULL;

tty_buffer_init(tty); //buffer初始化,见1-1-2

mutex_init(&tty->termios_mutex);

mutex_init(&tty->ldisc_mutex);

init_waitqueue_head(&tty->write_wait);

init_waitqueue_head(&tty->read_wait);

INIT_WORK(&tty->hangup_work, do_tty_hangup);

mutex_init(&tty->atomic_read_lock);

mutex_init(&tty->atomic_write_lock);

mutex_init(&tty->output_lock);

mutex_init(&tty->echo_lock);

spin_lock_init(&tty->read_lock);

spin_lock_init(&tty->ctrl_lock);

INIT_LIST_HEAD(&tty->tty_files);

INIT_WORK(&tty->SAK_work, do_SAK_work);

tty->driver = driver;

tty->ops = driver->ops;/*请注意这里的赋值,它将driver的ops赋值给了tty->ops,后面有很多用到该ops的使用,那我们

到时候要想起来它实际是对driver的ops的使用,这里driver就对应serial_core中的注册时的使用的那个driver*/

tty->index = idx;

tty_line_name(driver, idx, tty->name);

}

这个函数主要对tty_struct各个部分进行初始化。我们着重分析1-1-1和1-1-2。

1-1-1,tty_ldisc_init(tty):

void tty_ldisc_init(struct tty_struct *tty)

{

struct tty_ldisc *ld = tty_ldisc_get(N_TTY); //得到tty的线路规程,N_TTY是tty的线路规程号,内核启动时,将会注册这个N_TTY对应的ops:tty_ldisc_N_TTY

if (IS_ERR(ld))

panic("n_tty: init_tty");

tty_ldisc_assign(tty, ld); //设置线路规程给tty

}

所以经过这个初始化函数后,tty->ldisc将会被赋值为N_TTY对应的线路规程

1-1-2,tty_buffer_init(tty)

void tty_buffer_init(struct tty_struct *tty)

{//初始化buffer结构

spin_lock_init(&tty->buf.lock);

tty->buf.head = NULL;

tty->buf.tail = NULL;

tty->buf.free = NULL;

tty->buf.memory_used = 0;

INIT_DELAYED_WORK(&tty->buf.work, flush_to_ldisc);//这个工作队列非常重要!主要功能是将buf数据刷到线路规程,见1-1-2-1

}

这个函数主要初始化buf结构,我们主要分析一下tty->buf.work的具体作用。

1-1-2-1,INIT_DELAYED_WORK(&tty->buf.work, flush_to_ldisc)

static void flush_to_ldisc(struct work_struct *work)

{

struct tty_struct *tty =

container_of(work, struct tty_struct, buf.work.work);

unsigned long     flags;

struct tty_ldisc *disc;

disc = tty_ldisc_ref(tty);//得到tty的线路规程引用

if (disc == NULL)    /*  !TTY_LDISC */

return;

spin_lock_irqsave(&tty->buf.lock, flags);

if (!test_and_set_bit(TTY_FLUSHING, &tty->flags)) {//设置flush标记

struct tty_buffer *head;

while ((head = tty->buf.head) != NULL) {

int count;

char *char_buf;

unsigned char *flag_buf;

count = head->commit - head->read;

if (!count) {

if (head->next == NULL)

break;

tty->buf.head = head->next;

tty_buffer_free(tty, head);

continue;

}

/* Ldisc or user is trying to flush the buffers

we are feeding to the ldisc, stop feeding the

line discipline as we want to empty the queue */

if (test_bit(TTY_FLUSHPENDING, &tty->flags))//如果想停止buffer转移到线路规程

break;

if (!tty->receive_room) { //当tty没接收空间的时候,延迟1个jiffies调用tty->buf.work,即flush_to_ldisc,把buf数据移到线路规程

schedule_delayed_work(&tty->buf.work, 1);

break;

}

if (count > tty->receive_room)

count = tty->receive_room;//不能大于接收空间的大小

char_buf = head->char_buf_ptr + head->read;

flag_buf = head->flag_buf_ptr + head->read;

head->read += count;

spin_unlock_irqrestore(&tty->buf.lock, flags);

disc->ops->receive_buf(tty, char_buf,

flag_buf, count);//执行线路规程的receive_buf,把buffer拷贝过来,这里的ops就是上面的tty_ldisc_N_TTY,见1-1-2-1-1

spin_lock_irqsave(&tty->buf.lock, flags);

}

clear_bit(TTY_FLUSHING, &tty->flags);//结束flush,设置结束标记

}

/* We may have a deferred request to flush the input buffer,

if so pull the chain under the lock and empty the queue */

if (test_bit(TTY_FLUSHPENDING, &tty->flags)) {//如果设置停止flush标记,则清空buffer

__tty_buffer_flush(tty);

clear_bit(TTY_FLUSHPENDING, &tty->flags);

wake_up(&tty->read_wait);

}

spin_unlock_irqrestore(&tty->buf.lock, flags);

tty_ldisc_deref(disc);//释放一个线路规程的引用

}

可以看出其主要作用是将tty core的buffer刷到线路规程。

1-1-2-1-1,disc->ops->receive_buf(tty, char_buf, flag_buf, count):

struct tty_ldisc_ops tty_ldisc_N_TTY = {

.magic           = TTY_LDISC_MAGIC,

.name            = "n_tty",

.open            = n_tty_open,

.close           = n_tty_close,

.flush_buffer    = n_tty_flush_buffer,

.chars_in_buffer = n_tty_chars_in_buffer,

.read            = n_tty_read,

.write           = n_tty_write,

.ioctl           = n_tty_ioctl,

.set_termios     = n_tty_set_termios,

.poll            = n_tty_poll,

.receive_buf     = n_tty_receive_buf,

.write_wakeup    = n_tty_write_wakeup

};

可以看出tty_ldisc_N_TTY->receive_buf对应于n_tty_receive_buf:

static void n_tty_receive_buf(struct tty_struct *tty, const unsigned char *cp,

char *fp, int count)

{

const unsigned char *p;

char *f, flags = TTY_NORMAL;

int    i;

char    buf[64];

unsigned long cpuflags;

if (!tty->read_buf)

return;

if (tty->real_raw) {

spin_lock_irqsave(&tty->read_lock, cpuflags);

i = min(N_TTY_BUF_SIZE - tty->read_cnt,

N_TTY_BUF_SIZE - tty->read_head);

i = min(count, i);//还是先确定要拷贝数据的长度小

memcpy(tty->read_buf + tty->read_head, cp, i);//拷贝到read buf

tty->read_head = (tty->read_head + i) & (N_TTY_BUF_SIZE-1);

tty->read_cnt += i;

cp += i;

count -= i;

i = min(N_TTY_BUF_SIZE - tty->read_cnt,

N_TTY_BUF_SIZE - tty->read_head);

i = min(count, i);

memcpy(tty->read_buf + tty->read_head, cp, i);

tty->read_head = (tty->read_head + i) & (N_TTY_BUF_SIZE-1);

tty->read_cnt += i;

spin_unlock_irqrestore(&tty->read_lock, cpuflags);

/*这里你可能奇怪为什么要拷贝两次呢?是因为数据读取缓存read_buf(0~N_TTY_BUF_SIZE)为一环形缓冲区。tty->read_tail, tty->read_tail指向第一个未被读取的数据,

tty->read_cnt缓存中的数据,tty->read_head指向 第一个未被占用的空间。由于是环形缓存tty->read_cnt不一定等于tty->read_head - tty->read_tail。

tty->read_head可能小于tty->read_tail所以可能有以下关系:

tty->read_cnt = N_TTY_BUF_SIZE -  tty->read_tail + tty->read_head。

所以将read_buf中的值考到用户空间需要考两次,*nr的值可能大于N_TTY_BUF_SIZE -  tty->read_tail

而小于tty->read_cnt。拷数据时是从tty->read_tail开始,第一次考取N_TTY_BUF_SIZE -  tty->read_tail, 第二次在read_buf的开始位置到tty->read_head之间获取还需的数据。*/

} else {

for (i = count, p = cp, f = fp; i; i--, p++) {

if (f)

flags = *f++;

switch (flags) {

case TTY_NORMAL:

n_tty_receive_char(tty, *p);

break;

case TTY_BREAK:

n_tty_receive_break(tty);

break;

case TTY_PARITY:

case TTY_FRAME:

n_tty_receive_parity_error(tty, *p);

break;

case TTY_OVERRUN:

n_tty_receive_overrun(tty);

break;

default:

printk(KERN_ERR "%s: unknown flag %d\n",

tty_name(tty, buf), flags);

break;

}

}

if (tty->ops->flush_chars)

tty->ops->flush_chars(tty);

}

n_tty_set_room(tty);//将剩下的空间赋值给tty->receive_room,以备查询

if (!tty->icanon && (tty->read_cnt >= tty->minimum_to_wake)) {

kill_fasync(&tty->fasync, SIGIO, POLL_IN);//向用户空间发一个异步信号

if (waitqueue_active(&tty->read_wait))

wake_up_interruptible(&tty->read_wait);//唤醒读进程

}

/*

* Check the remaining room for the input canonicalization

* mode.  We don't want to throttle the driver if we're in

* canonical mode and don't have a newline yet!

*/

if (tty->receive_room < TTY_THRESHOLD_THROTTLE)

tty_throttle(tty);//指示空间低于预定阀值

}

这个函数主要是将tty core层的数据刷到read_buf环形缓冲区中,下面我们回到tty_init_dev中的1-2部分。

1-2,tty_ldisc_setup(tty, tty->link)

int tty_ldisc_setup(struct tty_struct *tty, struct tty_struct *o_tty)

{

struct tty_ldisc *ld = tty->ldisc;//tty结构初始化的时候已经对它赋值,故这里可以使用

int retval;

retval = tty_ldisc_open(tty, ld);//如果是N_TTY,则对应的ops是tty_ldisc_N_TTY

if (retval)

return retval;

if (o_tty) {

retval = tty_ldisc_open(o_tty, o_tty->ldisc);//打开配对的tty

if (retval) {

tty_ldisc_close(tty, ld);

return retval;

}

tty_ldisc_enable(o_tty); //使能线路规程

}

tty_ldisc_enable(tty);

return 0;

}

这里通过线路规程的ops的open函数打开该tty。

2,tty->ops->open(tty, filp)

从initialize_tty_struct分析中知道,这里的ops就是driver对应的ops,也就是serial中的uart_ops,那我们看下uart_ops的open的函数:

static int uart_open(struct tty_struct *tty, struct file *filp)

{

......

/*

* Start up the serial port.

*/

retval = uart_startup(state, 0);//这是open的核心操作,见2-1

......

}

2-1,uart_startup(state, 0)

static int uart_startup(struct uart_state *state, int init_hw)

{

......

retval = port->ops->startup(port);//调用port口的ops,即mxc_ops,主要是对uart初始化,见2-1-1

if (retval == 0) {

if (init_hw) {//从open函数传下来的是0,这里不执行,但其它情况不一定

/*

* Initialise the hardware port settings.

*/

uart_change_speed(state, NULL);

/*

* Setup the RTS and DTR signals once the

* port is open and ready to respond.

*/

if (info->port.tty->termios->c_cflag & CBAUD)

uart_set_mctrl(port, TIOCM_RTS | TIOCM_DTR);

}

if (info->flags & UIF_CTS_FLOW) {

spin_lock_irq(&port->lock);

if (!(port->ops->get_mctrl(port) & TIOCM_CTS))

info->port.tty->hw_stopped = 1;

spin_unlock_irq(&port->lock);

}

info->flags |= UIF_INITIALIZED;

clear_bit(TTY_IO_ERROR, &info->port.tty->flags);

}

if (retval && capable(CAP_SYS_ADMIN))

retval = 0;

return retval;

}

2-1-1,port->ops->startup(port)

port口子的ops在mxc_uart中,

static struct uart_ops mxc_ops = {

.tx_empty = mxcuart_tx_empty,

.set_mctrl = mxcuart_set_mctrl,

.get_mctrl = mxcuart_get_mctrl,

.stop_tx = mxcuart_stop_tx,

.start_tx = mxcuart_start_tx,

.stop_rx = mxcuart_stop_rx,

.enable_ms = mxcuart_enable_ms,

.break_ctl = mxcuart_break_ctl,

.startup = mxcuart_startup,

.shutdown = mxcuart_shutdown,

.set_termios = mxcuart_set_termios,

.type = mxcuart_type,

.pm = mxcuart_pm,

.release_port = mxcuart_release_port,

.request_port = mxcuart_request_port,

.config_port = mxcuart_config_port,

.verify_port = mxcuart_verify_port,

.send_xchar = mxcuart_send_xchar,

};

我们看startup对应到了mxcuart_startup:

static int mxcuart_startup(struct uart_port *port)

{

uart_mxc_port *umxc = (uart_mxc_port *) port;

int retval;

volatile unsigned int cr, cr1 = 0, cr2 = 0, ufcr = 0;

/*

* Some UARTs need separate registrations for the interrupts as

* they do not take the muxed interrupt output to the ARM core

*/

if (umxc->ints_muxed == 1) {//从设备定义看,这个值为1

retval = request_irq(umxc->port.irq, mxcuart_int, 0,//定义中断函数

"mxcintuart", umxc);

if (retval != 0) {

return retval;

}

} else {

retval = request_irq(umxc->port.irq, mxcuart_tx_int,

0, "mxcintuart", umxc);

if (retval != 0) {

return retval;

} else {

retval = request_irq(umxc->irqs[0], mxcuart_rx_int,

0, "mxcintuart", umxc);

if (retval != 0) {

free_irq(umxc->port.irq, umxc);

return retval;

} else {

retval =

request_irq(umxc->irqs[1], mxcuart_mint_int,

0, "mxcintuart", umxc);

if (retval != 0) {

free_irq(umxc->port.irq, umxc);

free_irq(umxc->irqs[0], umxc);

return retval;

}

}

}

}

/* Initialize the DMA if we need SDMA data transfer */

if (umxc->dma_enabled == 1) {//是否需要dma传输

retval = mxcuart_initdma(dma_list + umxc->port.line, umxc);

if (retval != 0) {

printk

(KERN_ERR

"MXC UART: Failed to initialize DMA for UART %d\n",

umxc->port.line);

mxcuart_free_interrupts(umxc);

return retval;

}

/* Configure the GPR register to receive SDMA events */

config_uartdma_event(umxc->port.line);

}

/*

* Clear Status Registers 1 and 2

*/

writel(0xFFFF, umxc->port.membase + MXC_UARTUSR1);

writel(0xFFFF, umxc->port.membase + MXC_UARTUSR2);

/* Configure the IOMUX for the UART */

gpio_uart_active(umxc->port.line, umxc->ir_mode);

/*

* Set the transceiver invert bits if required

*/

if (umxc->ir_mode == IRDA) {

echo_cancel = 1;

writel(umxc->ir_rx_inv | MXC_UARTUCR4_IRSC, umxc->port.membase

+ MXC_UARTUCR4);

writel(umxc->rxd_mux | umxc->ir_tx_inv,

umxc->port.membase + MXC_UARTUCR3);

} else {

writel(umxc->rxd_mux, umxc->port.membase + MXC_UARTUCR3);

}

/*

* Initialize UCR1,2 and UFCR registers

*/

if (umxc->dma_enabled == 1) {

cr2 = (MXC_UARTUCR2_TXEN | MXC_UARTUCR2_RXEN);

} else {

cr2 =

(MXC_UARTUCR2_ATEN | MXC_UARTUCR2_TXEN | MXC_UARTUCR2_RXEN);

}

writel(cr2, umxc->port.membase + MXC_UARTUCR2);

/* Wait till we are out of software reset */

do {

cr = readl(umxc->port.membase + MXC_UARTUCR2);

} while (!(cr & MXC_UARTUCR2_SRST));

if (umxc->mode == MODE_DTE) {

ufcr |= ((umxc->tx_threshold << MXC_UARTUFCR_TXTL_OFFSET) |

MXC_UARTUFCR_DCEDTE | MXC_UARTUFCR_RFDIV | umxc->

rx_threshold);

} else {

ufcr |= ((umxc->tx_threshold << MXC_UARTUFCR_TXTL_OFFSET) |

MXC_UARTUFCR_RFDIV | umxc->rx_threshold);

}

writel(ufcr, umxc->port.membase + MXC_UARTUFCR);

/*

* Finally enable the UART and the Receive interrupts

*/

if (umxc->ir_mode == IRDA) {

cr1 |= MXC_UARTUCR1_IREN;

}

if (umxc->dma_enabled == 1) {

cr1 |= (MXC_UARTUCR1_RXDMAEN | MXC_UARTUCR1_ATDMAEN |

MXC_UARTUCR1_UARTEN);

} else {

cr1 |= (MXC_UARTUCR1_RRDYEN | MXC_UARTUCR1_UARTEN);

}

writel(cr1, umxc->port.membase + MXC_UARTUCR1);

return 0;

}

我们看下这个mxcuart_startup,主要是根据device定义对uart的初始化。

综上,tty_open的流程是tty_core->tty_ldisc和tty_core->serial_core->mxc_uart。它主要执行了tty_init_dev(driver, index, 0),这个函数主要是初始化一个tty设备,初始化线路规程和打开线路规程。还有执行了tty->ops->open(tty, filp),主要初始化了要使用的uart口子,有了这个基础,我们就可以对tty进行读写了。

阅读(491) | 评论(0) | 转发(0) |

linux在tty3创建用户,我对linux理解之tty三相关推荐

  1. Linux操作系统——批量创建用户

    Linux操作系统--批量创建用户 文章目录 Linux操作系统--批量创建用户 第一步:创建组群GID为650的是student的组群 第二步:创建用户信息文件students.txt,并用vim编 ...

  2. 在linux系统中 创建用户账户的同时,在Linux系统中大批量建立帐户

    在Linux系统中大批量建立帐户 企业如果想在Linux操作系统上部署文件的话,可能需要一次性建立大量的帐户.如为了加强文件的管理力度,需要为每个员工配置一个帐户.如此的话,就可以针对员工进行权限控制 ...

  3. 如何创建一个linux用户名和密码,Linux下如何创建用户 | Soo Smart!

    Linux下如何创建用户? 初步接触linux时要学会用户账号的添加.删除与修改.用户口令的管理.用户组的管理方法,这里列出来供大家参考使用吧. user的create, delete, modify ...

  4. linux批量创建用户1000,Linux下批量创建用户

    Linux下批量创建用户主要有以下两种方法: 方法一: 1,新建一个文件user.txt,以/etc/passwd 为模板 2,再次新建一个文件passwd..txt,以/etc/shadow 为模板 ...

  5. Linux:如何创建用户

    概述 下面将演示创建用户 zyq01 (1)输入命令:sudo useradd zyq01,回车,创建用户: (2)输入命令:ls,回车,查看用户是否创建成功(可以看到用户已经创建成功了): (3)输 ...

  6. linux怎么创建用户教程,在Linux中如何手动创建一个用户

    1.首先要明白用useradd创建用户的时候会更改添加5个地方的内容 (1)/etc/passwd             //比如创建useradd  111 // [root@localhost ...

  7. suse linux 创建用户密码,suse linux上创建用户方式

    当需要数据共享时,在suse linux上创建用户需要注意以下两点: 1. 所有服务器相同的用户名具备相同的id号. 2. 所有用户属于同一个组(如users组). 如同一台机器上: 1. 创建一个I ...

  8. linux批量创建系统,linux系统批量创建用户

    脚本目的:批量创建linux系统用户 说明:要创建用户的主机密码写入到ip.txt文件中 [root@thsf02 scripts]# cat ip.txt 10.165.123.0 10.172.4 ...

  9. Linux shell简单创建用户脚本

    前面介绍简单的shell编写规则. 现在开始编写一个简单的shell脚本. Linux shell介绍 编写shell脚本    1.创建脚本文件    2.根据需求,编写脚本    3.测试执行脚本 ...

最新文章

  1. 【PHPWord】TextRun
  2. ios JSON 解析流程(转)
  3. python正则表达式实例教程_Python正则表达式经典入门教程
  4. redis订阅执行一段时间自动停止_面试系列 redis 分布式锁amp;数据一致性
  5. 【强化学习】A3C原理
  6. mysql 阿里云 版本_阿里云虚拟主机mysql已经支持版本切换,支持MySQL 5.7.25
  7. NGUI_2.6.3_系列教程三
  8. 《高效能程序员的修炼》一第2章 把一堆烂事搞定的艺术
  9. 13.TCP/IP 详解卷1 --- IGMP : Internet 管理组协议
  10. oracle分组取第N条,ROW_NUMBER() OVER的用法
  11. 优秀网站源码、编程源码大全
  12. 手把手教你Excel数据处理!
  13. linux版百度导航软件,百度导航2019新版
  14. 什么是实体关系图(ERD)?
  15. Android 文件管理器的列表界面
  16. 从pdf复制文字到word中的问题
  17. gridsome(三)——plugins
  18. Pandas数据分析库(2)Python数据分析
  19. OpenStack T版服务组件之Nova计算服务
  20. 【虹科分享】在容器上使用 ntop 工具的最佳实践

热门文章

  1. oracle 斜杠的转义,apos 转义字符
  2. 微信小程序实现展开/收起的效果
  3. 室内设计常用的涂料清单
  4. 2022年度最佳开源软件榜单出炉!
  5. 根据excel里面的内容寻找文件
  6. 基于canny边缘检测、形态学、区域统计实现MATLAB的纽扣计数
  7. 专家称HTML5使浏览器成为移动互联网主要入口
  8. 华为开源自研AI框架昇思MindSpore应用案例:Colorization自动着色
  9. python工程项目管理
  10. JavaScript循环控制语句及函数