Linux 平台上在 Kernel 协调下完成进程之间的相互通信,有多种进程间通信 —— Inter Process Communication(IPC)方式。

1. IPC 分类


按照功能用途来看有三种基本的进程间通信类型,分别用户信息交换(Communication),同步(Synchronization)和信号(Signal)。另外,在基本的 IPC 通信机制之上还存在更为复杂和广泛应用的进程间通信机制,通常提供了丰富和更高层次的封装以便于应用开发,比如 Android 平台采用的 Binder 机制以及广泛应用的 D-Bus。

  • Communication:如 pipe 和 memory mapping
  • Synchronization:如 eventfd
  • Signal:Linux 上的 signal 机制
  • Others:Binder,D-Bus

下面分别来介绍各种类型的 IPC 机制。

2. Communication 类型的 IPC


Linux 平台上最为常见的是用于 Communication 的 IPC 机制,通常从狭义的角度来理解 IPC,可以将其认为是用于 Communication 的这一类,列举如下:

  • Pipes
  • FIFOs
  • Pseudoterminals
  • Sockets:分为 Streams 和 Datagram(以及 Seq)类型,主要的 UNIX 和 Internet domain 套接字
  • POSIX message queues
  • POSIX shared memory
  • System V message queues
  • System V shared memory
  • Shared memory mappings:分为 File 以及 Anonymous 映射
  • Cross-memory attach:proc_vm_readv 和 proc_vm_writev

对他们进行进一步的细分,首先可以分为 data transfer 用途和 shared memory,其中 data transfer 显而易见存在消息或者数据的流动传输,通过 Kernel 提供的机制将数据以中转的方式通常从一个进程到 Kernel 再到另一个进程;而 shared memory 则是通过进程之间共享存储区域的方式实现消息或数据的通信。Data transfer 又分为 byte stream,pseudo-terminal 和 message,广泛应用于各种场景。立体的划分如下图所示:

下面对常见的主要用于 Communication 的 IPC 机制进行分析。

2.1 Pipe


这里将 Pipe 和 FIFO 分开来讨论,首先介绍 Pipe,然后在其基础上再分析 FIFO。Pipe 最常见的应用方式:

$ ls | pr | lpr

这个管道应用实现了输入输出的重定向,ls列当前目录的输出被作为标准输入送到pr程序中,而pr的输出又被作为标准输入送到lpr程序中。Shell 负责在不同进程之间建立临时的管道。

Pipe 在内核中借助文件系统来实现,其源码在 fs/pipe.c 中。在 Kernel 初始化的过程中 fs_initcall 阶段将 pipe_fs_type 文件系统类型注册到文件系统中:

/** pipefs should _never_ be mounted by userland - too much of security hassle,* no real gain from having the whole whorehouse mounted. So we don't need* any operations on the root directory. However, we need a non-trivial* d_name - pipe: will go nicely and kill the special-casing in procfs.*/static int pipefs_init_fs_context(struct fs_context *fc)
{struct pseudo_fs_context *ctx = init_pseudo(fc, PIPEFS_MAGIC);if (!ctx)return -ENOMEM;ctx->ops = &pipefs_ops;ctx->dops = &pipefs_dentry_operations;return 0;
}static struct file_system_type pipe_fs_type = {.name      = "pipefs",.init_fs_context = pipefs_init_fs_context,.kill_sb   = kill_anon_super,
};static int __init init_pipe_fs(void)
{int err = register_filesystem(&pipe_fs_type);if (!err) {pipe_mnt = kern_mount(&pipe_fs_type);if (IS_ERR(pipe_mnt)) {err = PTR_ERR(pipe_mnt);unregister_filesystem(&pipe_fs_type);}}return err;
}fs_initcall(init_pipe_fs);

在 Kernel 中 Pipe 以单向(Unidirectional)字节流缓冲区的形式存在,在 VFS 中有与之关联的临时 inode 表示,通过两个 file 数据结构与其关联,分别用于读和写操作,在用户态分别对应到进程中的两个文件描述符,如下图所示:

通过 pipe 系统调用(glibc 等标准库提供相应的应用调用接口),获得两个文件系统描述符,其中 filedes[0] 用于读,filedes[1] 用于写:

int filedes[2];
pipe(filedes);
...
write(filedes[1], buf, count);
read(filedes[0], buf, count);

在父子进程之间通过 fork 可以将 pipe 进行共享以用于父子进程间的通信,在 fork 过程中父子进程之间共享文件描述符。而多余的打开文件描述符则需要在 fork 执行之后进行关闭:

int filedes[2];
pipe(filedes);
child_pid = fork();
if (child_pid == 0) {close(filedes[1]);/* Child now reads */
} else {close(filedes[0]);/* Parent now writes */
}

在 Linux 的 Manual 页有 sample:https://man7.org/tlpi/code/online/dist/pipes/simple_pipe.c.html

当写入进程对管道写时,字节被拷贝到共享数据页面中,当读取进程从管道中读时,字节从共享数据页面中拷贝出来。同时 Pipe 缓冲区有大小限制 PIPE_BUF,在 Linux 平台为 4096B,POSIX 标准要求最低 512B。Linux必须同步对管道的访问,它必须保证读者和写者以确定的步骤执行,并且可能出现缓冲区为空或者已满以及读者或写者不存在的情况,为此需要使用锁、等待队列和信号等同步机制。同步机制在文章后面进行介绍。

Pipe 支持 dup2 和 fcntl 操作,不能进行 lseek 操作。

2.2 FIFO


First-In-First-Out, 简称问 FIFO,中文用“命名管道”表述,也即 pipe with name in file system。它建立在 Pipe 基础之上,通过 mkfifo 创建,然后可以以文件的形式进行打开和读写操作。

相比 Pipe,FIFO 在相应的文件系统中存在对应的 inode 节点,相当于实际存在于对应的文件系统中,而实际的数据缓冲区域则存在于内存中。因此可以用于非关联进程之间的通信。

2.3 POSIX Message Queue


POSIX Message Queues 实现了 POSIX 标准的消息队列,是一种基于消息 Message 的通信方式,可以存在多个消息发送和读取者,每次读取一个消息进行处理,并且支持消息的优先级别和消息通知。

在用户空间有专门的 POSIX MQ APIs 支持消息操作:

  • Queue management (analogous to files)
  1. mq_open(): open/create MQ, set attributes
  2. mq_close(): close MQ
  3. mq_unlink(): remove MQ pathname
  • I/O
  1. mq_send(): send message
  2. mq_receive(): receive message
  • Other
  1. mq_setattr(), mq_getattr(): set/get MQ attributes
  2. mq_notify(): request notification of msg arrival

通过 mq_open 可以打开或者创建新的 MQ,其中 name 为其对应的名字,在伪终端文件系统上可以看到 /somename:

int mqd = mq_open(name, flags [, mode, &attr]);

其中 flags 类似 open 操作:

  • O_CREAT – create MQ if it doesn’t exist
  • O_EXCL – create MQ exclusively
  • O_RDONLY, O_WRONLY, O_RDWR – just like file open
  • O_NONBLOCK – non-blocking I/O

通过 attributes 可以控制 MQ 的属性:

struct mq_attr {long mq_flags; // MQ description flags, 0 or O_NONBLOCK, [mq_getattr(), mq_setattr()]long mq_maxmsg; // Max. # of msgs on queue, [mq_open(), mq_getattr()]long mq_msgsize; // Max. msg size (bytes), [mq_open(), mq_getattr()]long mq_curmsgs; // # of msgs currently in queue, [mq_getattr()]
};

比如 MQ 的大小等。

Message Queue 在 Kernel 中也是以文件系统的形式存在,其实现在 ipc/mqueue.c 文件中,在 Kernel 启动的 device_initcall 阶段进行了初始化:

static const struct inode_operations mqueue_dir_inode_operations = {.lookup = simple_lookup,.create = mqueue_create,.unlink = mqueue_unlink,
};static const struct file_operations mqueue_file_operations = {.flush = mqueue_flush_file,.poll = mqueue_poll_file,.read = mqueue_read_file,.llseek = default_llseek,
};static const struct super_operations mqueue_super_ops = {.alloc_inode = mqueue_alloc_inode,.free_inode = mqueue_free_inode,.evict_inode = mqueue_evict_inode,.statfs = simple_statfs,
};static const struct fs_context_operations mqueue_fs_context_ops = {.free     = mqueue_fs_context_free,.get_tree = mqueue_get_tree,
};static struct file_system_type mqueue_fs_type = {.name           = "mqueue",.init_fs_context  = mqueue_init_fs_context,.kill_sb      = kill_litter_super,.fs_flags      = FS_USERNS_MOUNT,
};int mq_init_ns(struct ipc_namespace *ns)
{struct vfsmount *m;ns->mq_queues_count  = 0;ns->mq_queues_max    = DFLT_QUEUESMAX;ns->mq_msg_max       = DFLT_MSGMAX;ns->mq_msgsize_max   = DFLT_MSGSIZEMAX;ns->mq_msg_default   = DFLT_MSG;ns->mq_msgsize_default  = DFLT_MSGSIZE;m = mq_create_mount(ns);if (IS_ERR(m))return PTR_ERR(m);ns->mq_mnt = m;return 0;
}void mq_clear_sbinfo(struct ipc_namespace *ns)
{ns->mq_mnt->mnt_sb->s_fs_info = NULL;
}void mq_put_mnt(struct ipc_namespace *ns)
{kern_unmount(ns->mq_mnt);
}static int __init init_mqueue_fs(void)
{int error;mqueue_inode_cachep = kmem_cache_create("mqueue_inode_cache",sizeof(struct mqueue_inode_info), 0,SLAB_HWCACHE_ALIGN|SLAB_ACCOUNT, init_once);if (mqueue_inode_cachep == NULL)return -ENOMEM;/* ignore failures - they are not fatal */mq_sysctl_table = mq_register_sysctl_table();error = register_filesystem(&mqueue_fs_type);if (error)goto out_sysctl;spin_lock_init(&mq_lock);error = mq_init_ns(&init_ipc_ns);if (error)goto out_filesystem;return 0;out_filesystem:unregister_filesystem(&mqueue_fs_type);
out_sysctl:if (mq_sysctl_table)unregister_sysctl_table(mq_sysctl_table);kmem_cache_destroy(mqueue_inode_cachep);return error;
}device_initcall(init_mqueue_fs);

关于 Message Queue 的详细用法可以参考 https://man7.org/tlpi/code/online/dist/pmsg/ 中的实例。

2.4 Shared Memory


共享内存的最大特点就是高效,通过减少数据在存储区域之间的拷贝来实现高性能的进程间通信操作,广泛应用于如多媒体等大量数据通信和处理的应用中。具体来讲,通常的数据传输通信中数据的经过了 user space ==> kernel ==> user space 处理流程,从而存在两次拷贝操作,在 Kernel 中 copy_from_user 和 copy_to_user 就是用来完成相应的数据拷贝操作的。而 Shared Memory 则只需要将数据拷贝到特定的内存中,并将其映射到不同的进程地址空间,以实现对数据的访问。

当然这样的高效率特性同样要求 Shared Memory 的使用者采取其他的机制来实现同步来避免错误。

Shared Memory 有如下三种类型:

  • Shared Anonymous Mappings:用于关联进程之间
  • Shared File Mappings:用于非关联进程之间,采用文件备份
  • POSIX Shared Memory:用于非关联进程之间,且不采用传统的文件备份

三者采用相同的系统调用,实际使用则填写不同的参数:

void *mmap(void *daddr, size_t len, int prot, int flags, int fd, off_t offset);

使用方式:

void *addr = mmap(daddr, len, prot, flags, fd, offset);

各项参数的含义:

  • daddr – choose where to place mapping;
  • Best to use NULL, to let kernel choose
  • len – size of mapping
  • prot – memory protections (read, write, exec)
  • flags – control behavior of call
  • MAP_SHARED, MAP_ANONYMOUS
  • fd – file descriptor for file mappings
  • offset – starting offset for mapping from file
  • addr – returns address used for mapping

其中 addr 作为返回参数相当于普通的 C 指针,其指向的内容的修改对所有的相关进程可见。

2.4.1 Shared Anonymous Mapping


用于关联进程,在 mmap 调用中 fd 和 offset 不会用到,即不会涉及文件信息,目的地址一般也不需要指定,直接写入 NULL,这样 Kernel 会帮忙查找对应的地址空间中可以匹配的地址进行映射并在成功后返回给 addr。需要特别注意指定 MAP_ANONYMOUS 标识。

addr = mmap(NULL, length,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1, 0);
pid = fork();

length 标识需要映射的地址长度,在关联进程吉间即共享了 addr:length 内存。

2.4.2 Shared File Mapping


又称为“文件映射”共享内存,用于非关联进程之间,以文件备份共享,所以涉及到文件操作。

int fd = open(...);

void *addr = mmap(..., fd, offset);

而内存内容从打开的文件初始化,存在 “memory-mapped I/O”,因此对于内存的更新同样会更新到文件,而不同的进程如果映射到相同文件的相同区域,则相互共享映射到的内存。

fd = open(pathname, O_RDWR);
addr = mmap(NULL, length,PROT_READ | PROT_WRITE,MAP_SHARED,fd, 0);
...
close(fd); /* No longer need 'fd' */

2.4.3 POSIX Shared Memory


用于非关联进程之间内存共享,并不需要创建文件,所以省去了 I/O 相关的性能损耗,具有更高的效率。

POSIX SHM 有其对应的 APIs:

  • Object management
  1. shm_open(): open/create SHM object
  2. mmap(): map SHM object
  3. shm_unlink(): remove SHM object pathname
  • Operations on SHM object via fd returned by shm_open():
  1. fstat(): retrieve info (size, ownership, permissions)
  2. ftruncate(): change size
  3. fchown(): fchmod(): change ownership, permissions

SHM 的操作及其特性类似于 FIFO,存在对应的文件名可以在 tmpfs(在 /dev/shm)中查看到,并且其大小可以进行修改,O_TRUNC flag 位以及 ftruncate 系统调用。

SHM 在内核中的实现在 mm/shmem.c,以 shmem_fs_type 文件系统形式存在,在早期版本中为 tmpfs_fs_type 形式:

static struct file_system_type shmem_fs_type = {.owner      = THIS_MODULE,.name        = "tmpfs",.init_fs_context = shmem_init_fs_context,
#ifdef CONFIG_TMPFS.parameters  = shmem_fs_parameters,
#endif.kill_sb  = kill_litter_super,.fs_flags  = FS_USERNS_MOUNT | FS_THP_SUPPORT,
};int __init shmem_init(void)
{int error;shmem_init_inodecache();error = register_filesystem(&shmem_fs_type);if (error) {pr_err("Could not register tmpfs\n");goto out2;}shm_mnt = kern_mount(&shmem_fs_type);if (IS_ERR(shm_mnt)) {error = PTR_ERR(shm_mnt);pr_err("Could not kern_mount tmpfs\n");goto out1;}#ifdef CONFIG_TRANSPARENT_HUGEPAGEif (has_transparent_hugepage() && shmem_huge > SHMEM_HUGE_DENY)SHMEM_SB(shm_mnt->mnt_sb)->huge = shmem_huge;elseshmem_huge = 0; /* just in case it was patched */
#endifreturn 0;out1:unregister_filesystem(&shmem_fs_type);
out2:shmem_destroy_inodecache();shm_mnt = ERR_PTR(error);return error;
}

其初始化过程发生在 mnt_init  阶段:

void __init mnt_init(void)
{...shmem_init();init_rootfs();init_mount_tree();
}

调用过程为:

start_kernel -> vfs_caches_init -> mnt_init -> hmem_init

关于 SHM 的实例参考:https://man7.org/tlpi/code/online/dist/pshm

3. Synchronization 类型的 IPC


用于 Synchronization 的 IPC 机制主要完成较为同步过程,比如辅助 Communication 类型的 IPC(如 SHM)实现更为复杂的功能。列举如下:

  • Eventfd
  • Futexes
  • Record locks
  • File locks
  • Mutexes
  • Condition variables
  • Barriers
  • Read-write locks
  • POSIX semaphores:分为 Named 和 Unnamed 类型
  • System V semaphores

归类来讲分为 semaphore,eventfd,file lock,futex 和 thread-related 类型。相关归类如下图所示:

就同步本身来讲是为了实现对资源的访问同步,包括内存以及文件资源:

  • Shared Memory - semaphores
  • File - file locks

下面分别对主要的 Synchronization IPC 机制进行分析。

3.1 POSIX Semaphores


POSIX Semaphores 实质为在 Kernel 中维护的整数,获取过程中会对该值减一操作,如果小于零则 Kernel 会阻塞操作,因此可能会导致进程进入睡眠状态。POSIX Semaphore 可以分为两种,Named 和 Unnamed 类型,其中 Unnamed 类型嵌入在共享的内存中,而 Named 类型则为独立的命名实体。

3.1.1 Unnamed Semaphores


与 Named Semaphore 的接口不同,采用 sem_init 完成初始化,然后使用:

  • sem_init(semp, pshared, value): initialize semaphore pointed to by semp to value
  1. sem_t *semp
  2. pshared: 0, thread sharing; != 0, process sharing
  • sem_post(semp): add 1 to value
  • sem_wait(semp): subtract 1 from value
  • sem_destroy(semp): free semaphore, release resources back to system

用于读写进程之间的数据同步,通过 POSIX Shared Memory 来完成数据的发送来完成 SHM 的共享访问:

#define BUF_SIZE 1024
struct shmbuf { // Buffer in shared memorysem_t wsem; // Writer semaphoresem_t rsem; // Reader semaphoreint cnt; // Number of bytes used in 'buf'char buf[BUF_SIZE]; // Data being transferred
}

上面的 shmbuf 定义了用于数据传输的结构,包括共享的存储内容和 POSIX Semaphore,下面是 Writer 的实例:

fd = shm_open(SHM_PATH, O_CREAT|O_EXCL|O_RDWR, OBJ_PERMS);
ftruncate(fd, sizeof(struct shmbuf));
shmp = mmap(NULL, sizeof(struct shmbuf),PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);sem_init(&shmp->rsem, 1, 0);
sem_init(&shmp->wsem, 1, 1); // Writer gets first turnfor (xfrs = 0, bytes = 0; ; xfrs++, bytes += shmp->cnt) {sem_wait(&shmp->wsem); // Wait for our turnshmp->cnt = read(STDIN_FILENO, shmp->buf, BUF_SIZE);sem_post(&shmp->rsem); // Give reader a turnif (shmp->cnt == 0) // EOF on stdin?break;
}
sem_wait(&shmp->wsem); // Wait for reader to finish// Clean up

Reader 的实例如下:

fd = shm_open(SHM_PATH, O_RDWR, 0);
shmp = mmap(NULL, sizeof(struct shmbuf),PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);for (xfrs = 0, bytes = 0; ; xfrs++) {sem_wait(&shmp->rsem); // Wait for our turn */if (shmp->cnt == 0) // Writer encountered EOF */break;bytes += shmp->cnt;write(STDOUT_FILENO, shmp->buf, shmp->cnt) != shmp->cnt);sem_post(&shmp->wsem); // Give writer a turn */
}sem_post(&shmp->wsem); // Let writer know we're finished

打开共享内存地址,将对应的内容映射到 shmp 从而可以进行访问,以对应的数据结构访问其中的读写信号量,实现同步的功能

3.1.2 Named Semaphores


其处理过程涉及到 name 信息,为信号量的路径,以 tmpfs(最新 shmem_fs_type)形式存在,通过 sem_open 打开或创建,而 sem_unlink 来删除

3.2 Eventfd


Eventfd 是一种简化形式的文件系统实现,其在 Kernel 中的源码为 fs/eventfd.c,并没有实现具体的文件系统,只是定义了特定的一些系统调用,在系统调用内部安装了文件描述符信息以及对应的文件操作接口,实现 eventfd 特定的文件操作:

static const struct file_operations eventfd_fops = {
#ifdef CONFIG_PROC_FS.show_fdinfo   = eventfd_show_fdinfo,
#endif.release  = eventfd_release,.poll        = eventfd_poll,.read_iter  = eventfd_read,.write      = eventfd_write,.llseek        = noop_llseek,
};

系统调用的定义:

SYSCALL_DEFINE2(eventfd2, unsigned int, count, int, flags)
{return do_eventfd(count, flags);
}SYSCALL_DEFINE1(eventfd, unsigned int, count)
{return do_eventfd(count, 0);
}

Eventfd 主要用于内核与用户态进程之间的事件通知,以实现资源的高效利用,在用户空间通过如下方式调用:

int eventfd(unsigned int initval, int flags);

内核维护对应的 64 位整形计数器,初始化为 initval 值。而 flags 有如下标识位:

  • EFD_CLOEXEC:FD_CLOEXEC,简单说就是fork子进程时不继承,对于多线程的程序设上这个值不会有错的
  • EFD_NONBLOCK:文件会被设置成O_NONBLOCK,一般要设置
  • EFD_SEMAPHORE:(2.6.30以后支持)支持semophore语义的read,简单说就值递减1

对其进行 read 操作就是将对应的 counter 置零,如果是 semophore 则减去一,write 则设置 counter 的值,支持 epoll/poll/select 操作,如上面的 eventfd_fops 定义。

在 Android 等平台上 eventfd 被用来作为基础的通信机制,在其 Native 层实现上采用其作为 Message 的送达通知机制。

3.3 File Lock


File lock 参考 http://blog.hongxiaolong.com/posts/flock-and-lockf.html

3.4 Futex


Futex 为 File lock 的互斥类型。

4. Socket


Socket 较为特殊,特别列出来单独讨论。

5. Binder


Android 平台的 Binder 机制是非常重要且复杂的内容,这里也特别列出来单独讨论。

6. D-Bus


D-Bus 是目前最广泛应用于各大 Linux 常用平台的进程间通信机制,下面单独分析。

Linux 进程间通信-IPC 机制相关推荐

  1. Linux的IPC机制(三):Binder

    1. 动态内核可加载模块 && 内存映射 正如上一章所说, 跨进程通信是需要内核空间做支持的. 传统的 IPC 机制如 管道, Socket, 都是内核的一部分, 因此通过内核支持来实 ...

  2. linux进程间通信 ipc,进程间通信IPC (InterProcess Communication)

    一.进程间通信的概念 每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区, ...

  3. Linux的IPC机制(二):Socket/管道/消息队伍/信号量

    每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核. Linux 内核提供了不少进程间通信的机制,我们来一起瞧瞧有哪些? 管道 如 ...

  4. 3,进程间通信IPC机制,线程,线程通信,互斥锁

    今日内容: 1,进程间的相互通信 2,生产者消费者模型 3,线程 4,线程间通信 5,线程互斥锁 1,进程间相互通信 IPC 机制 1,基于队列来实现通信Queue,队列就相当于管道+锁 队列:先进先 ...

  5. linux各种IPC机制

    原帖发表在IBM的developerworks网站上,是一个系列的文章,作者郑彦兴,通过讲解和例子演示了Linux中几种IPC的使用方式,我觉得很好,在这里做一个保留,能看完的话Linux IPC的基 ...

  6. Linux 进程间通信(IPC)---大总结

    目录 前言 进程间通信方式 管道 匿名管道(PIPE) 如何创建匿名管道 管道的读写特性 命令行管道符的实现 命名管道(FIFO) 管道特性 共享内存 共享内存的操作流程 如何创建共享内存 如何将共享 ...

  7. Linux进程间通信(IPC)-------消息队列

    消息队列是进程间通信的一种方法,他有两个操作,一个进程来发送消息(也就是向内存中写入数据),另一个是获取消息(也就是另外一个进程在内存中读取数据) 下面来看消息队列的 创建,写入,读取等需要用到的函数 ...

  8. Linux的IPC机制(一):共享内存

    0. 共享内存 比喻 本质 多个进程访问同一个逻辑内存 直接访问内存,不用read()/write()非常方便 1. POSIX 共享内存 资料:unpv22e-ch13 查看: man shm_ov ...

  9. linux进程间通信(IPC) ---无名管道

    管道概述 管道(pipe)又称无名管道 无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符 任何一个进程在创建的时候,系统都会,给它分配4G的虚拟内存,分为3G的用户空间和1G的内核空间 ...

最新文章

  1. 同时起两个mysql 起不了_到底是谁!让你在冬天的早晨起不了床?
  2. 程序员的自我修养六可执行文件的装载与进程
  3. 34988 Happy Reversal(二进制去取反)
  4. flash代码_Flash如何对制作文件进行优化
  5. springboot @datetimeformat 标注在参数上无效_Spring Boot 中必须掌握的 45 个注解
  6. shell编程:笔记*
  7. 什么视频会议系统好?
  8. TXT文本 本地词典
  9. metasploit framework的一些使用姿势(持续更新)
  10. Java二维码生成工具类
  11. python人脸识别项目_face++与python实现人脸识别签到(考勤)功能
  12. 支付宝余额提现收手续费了
  13. 平面设计学习之四(PS-计算磨皮法)
  14. error C2041: illegal digit ‘9‘ for base ‘8‘ | error C2059: syntax error: ‘bad suffix on number‘
  15. 【UE4 005】自定义人物角色(Charactor) 替换小白人
  16. jQuery延迟加载(懒加载)
  17. 怎么实现抓取同行网站访客号码
  18. 西航计算机学院学生会,“智”行千里,逐梦远航丨西航职院人工智能学院召开2020年度总结表彰暨新年联欢会...
  19. 研发效能度量框架解读
  20. matlab矩阵创建

热门文章

  1. 自媒体人必不可少的多平台同步、一文多发小助手
  2. 2.8 Multisim应用举例
  3. matlab nntool 使用步骤: (以p4.3 为例),Matlab_nntool_应用实例
  4. 论文阅读:FASTEMIT: LOW-LATENCY STREAMING ASR WITH SEQUENCE-LEVEL EMISSION REGULARIZATION
  5. java中实现的十进制转换成二进制的简单代码
  6. word、wps图文复制一键粘贴到富文本编辑器
  7. 海思Hi3136芯片怎么样?Hi3136处理器参数介绍
  8. Java微信公众平台开发之获取地理位置
  9. NSX-T 恢复DFW策略
  10. C语言int型数据范围