Orphan, Zombie and Docker

孤儿进程的产生 孤儿进程: 父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。通常,孤儿进程将被进程号为1的进程(进程号为 1 的是 init 进程)所收养,并由该进程调用 wait 对孤儿进程收尸。 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> int main() { pid_t pid; pid = fork(); if (pid == 0) { printf("I'm child process, pid:%d ppid:%d\n", getpid(), getppid()); sleep(5); printf("I'm child process, pid:%d ppid:%d\n", getpid(), getppid()); } else { printf("I'm father process, pid:%d ppid:%d\n", getpid(), getppid()); sleep(1); printf("father process is exited.\n"); } return 0; } 运行结果如下所示: I'm father process, pid:25354 ppid:13981 I'm child process, pid:25355 ppid:25354 father process is exited....

2019-06-16 · Me

nginx 文件锁、自旋锁的实现

在上一篇博客 Linux 共享内存以及 nginx 中的实现的示例中,我们看到每次多个进程同时对共享内存中的 count 加一,导致每次运行结果都不一样,那么解决的方法就是对临界区加锁了,所以本文就来研究一下 nginx 中的几种加锁方式。 文件锁 文件锁的原理就是在磁盘上创建一个文件(操作系统创建一个文件描述符),然后多个进程竞争去获取这个文件的访问权限,因此同一时刻只有一个进程能够访问临界区。 可以看出,进程并不会真正在这个文件中写什么东西,我们只是想要一个文件描述符 FD 而已,因此 nginx 会在创建了文件后把这个文件删除,只留下文件描述符。 多个进程打开同一个文件,各个进程看到的文件描述 FD 值可能会不一样。例如文件 test.txt 在 进程1 中是 101, 而在进程2中是 201 使用文件锁举例 使用文件锁主要用到两个 libc 提供的结构体和函数。 struct flock; 提供一些锁的基本信息,比如读锁 F_RDLCK, 还是写锁 F_WRLCK fcntl(): 对文件描述符进行操作的库函数。 那么如何用这个函数来实现锁的功能呢? 先看一个加锁的代码: void mtx_file_lock(struct fdmutex *m) { struct flock fl; memset(&fl, 0, sizeof(struct flock)); fl.l_type = F_WRLCK; fl.l_whence = SEEK_SET; if (fcntl(m->fd, F_SETLKW, &fl) == -1) { printf("[-] PID %d, lock failed (%s)....

2018-09-18 · Me

Linux 共享内存以及 nginx 中的实现

共享内存方法简介 Linux/Unix系统中,共享内存可以通过两个系统调用来获得,mmap 和 shmget/shm_open,其中 shmget 和 shm_open 分别属于不同的标准: POSIX 共享内存(shm_open()、shm_unlink()) System V 共享内存(shmget()、shmat()、shmdt()) shmget 和 shm_open 类似的地方在于都是创建一个共享内存,挂载到 /dev/shm 目录下,并且返回一个文件描述符,fd。 区别是 POSIX 没有提供将 fd 映射到进程地址空间的方法,而 System V 方式则直接提供了 shmat(),之后再 nginx 的实现中会再次看到。 mmap 语义上比 shmget 更通用,因为它最一般的做法,是将一个打开的实体文件,映射到一段连续的内存中,各个进程可以根据各自的权限对该段内存进行相应的读写操作,其他进程则可以看到其他进程写入的结果。 而 shmget 在语义上相当于是匿名的 mmap,即不关注实体文件,直接在内存中开辟这块共享区域,mmap 通过设置调用时的参数,也可达到这种效果,一种方法是映射/dev/zero 设备,另一种是使用MAP_ANON选项。 mmap() 的函数原型如下,具体参数含义在最后的参考资料中给出。 void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); nginx 中的实现 nginx 中是怎么实现的呢? 我们看一下源码 src/os/unix/ngx_shmem.c。 一目了然,简单粗暴有木有! 分三种情况 如果mmap系统调用支持 MAP_ANON选项,则使用 MAP_ANON 如果1不满足,如果mmap系统调用支持映射/dev/zero设备,则映射/dev/zero来实现。 如果1和2都不满足,且如果支持shmget的话,则使用该shmget来实现。 #if (NGX_HAVE_MAP_ANON) ngx_int_t ngx_shm_alloc(ngx_shm_t *shm) { shm->addr = (u_char *) mmap(NULL, shm->size, PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0); return NGX_OK; } #elif (NGX_HAVE_MAP_DEVZERO) ngx_int_t ngx_shm_alloc(ngx_shm_t *shm) { fd = open("/dev/zero", O_RDWR); shm->addr = (u_char *) mmap(NULL, shm->size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); return (shm->addr == MAP_FAILED) ?...

2018-09-04 · Me

使用 socketpair 实现进程间通信

socketpair 牛刀小试 int socketpair(int d, int type, int protocol, int sv[2]); 第1个参数d,表示协议族,只能为 AF_LOCAL 或者 AF_UNIX; 第2个参数 type,表示类型,只能为0。 第3个参数 protocol,表示协议,可以是 SOCK_STREAM 或者 SOCK_DGRAM AF_UNIX 指的就是 Unix Domain socket,那么它与通常网络编程里面的 TCP socket 有什么区别呢? 查阅了资料后发现: Unix Domain socket 是同一台机器上不同进程间的通信机制。 IP(TCP/IP) socket 是网络上不同主机之间进程的通讯机制。 socketpair() 只支持 AF_LOCAL 或者 AF_UNIX,不支持 TCP/IP,也就是 AF_INET, 所以用 socketpair() 的话无法进行跨主机的进程间通信。 先看一个简单的示例: int main() { int fd[2], retpid; int pid , status; char input[MAX_LEN]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0) { printf("call socketpair() failed, exit\n"); return -1; } pid = fork(); if (pid) { /* parent */ printf("Parent process, pid = %d\n", getpid()); while (1) { fgets(input, MAX_LEN, stdin); write(fd[0], input, MAX_LEN); } } else { /* child */ printf("Child process, pid = %d\n", getpid()); int nread = 0; while (1) { nread = read(fd[1], input, MAX_LEN); input[nread] = '\0'; printf("Child: nread = %d, data = %s\n", nread, input); } } retpid = wait(&status); if (retpid) { printf("Parent: reap child process pid = %d\n", retpid); } return 0; } 编译后运行,可以看到每次在终端输入信息,子进程都会回显到屏幕上。...

2018-08-28 · Me

SO_REUSEPORT 和 epoll 的 Thundering Herd

SO_REUSEPORT 顾名思义就是重用端口,是指不同的 socket 可以 bind 到同一个端口上。 Linux 内核 3.9 版本引入了这个新特性,有兴趣的同学可以移步到这个链接查看更加详细的内容。 https://lwn.net/Articles/542629/ Reuse Port 我们先通过一段简单的代码来看看怎么使用这个选项(完整的代码在这里下载)。 int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 一定要在 bind() 函数之前设定好 SO_REUSEPORT setsockopt(serv_sock, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(int)); bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); listen(serv_sock, 20); accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); 将上面的代码编译生成两个可执行文件,分别启动运行,并监听相同的端口。 ./port_reuse1 127.0.0.1 1234 再用 telnet/nc 等工具发送请求到 1234 端口上,多重复几次,会看到两个进程轮流的处理客户端发来的请求。 这里说一个题外话,上面的例子是手动启动两个进程。而我发现如果是进程自动 fork() 生成 2 个进程的话,似乎不用设置 SO_REUSEPORT 也能自动监听同一个端口。这是为什么? Thundering Herd / 惊群现象 The thundering herd problem occurs when a large number of processes waiting for an event are awoken when that event occurs, but only one process is able to proceed at a time....

2018-06-23 · Me

Linux 内核在 x86-64 上的内存分区

如果稍微了解过 Linux 内核的内存管理,那么对内存分区的概念一定不陌生,Linux内核把物理内存分成了3个区, 0 – 16M 为ZONE_DMA区, 16M – 896M 为ZONE_NORMAL区, 高于896M 为ZONE_HIGHMEM区 我没有去考证过为什么要取896这个数字,但是可以肯定的是这样的划分在当时看来是合理的,然而计算机发展今非昔比,现在4G的物理内存已经成为PC的标配了,CPU也进入了64位时代,很多事情都发生着改变。 在CPU还是32位的时代,CPU最大的物理寻址范围是0-4G, 在这里为了方便讨论,我们不考虑物理地址扩展(PAE)。进程的虚拟地址空间也是 4G,Linux内核把 0-3G虚拟地址空间作为用户空间,3G-4G虚拟地址空间作为内核空间。 目前几乎所有介绍Linux内存管理的书籍还是停留在32位寻址的时代,所以大家对下面这张图一定很熟悉! (这个图画得非常详细,本篇文章我们关注的重点是 3个分区 以及最右边的线性地址空间,也就是虚拟地址空间之间的关系,另外,应该是ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM, 图中把ZONE 写成了ZUNE) 然而,现在是64位的时代了, 64位CPU的寻址空间是多大呢? 16EB, 1EB = 1024 TB = 1024 * 1024 GB,我想很多人这辈子还没见过大于1TB的内存吧,事实上也是这样,几乎没有哪个服务器能有16EB的内存,实现64位长的地址只会增加系统的复杂度和地址转换的成本,所以目前的x86_64架构CPU都遵循AMD的 Canonical Form, 即只有虚拟地址的最低48位才会在地址转换时被使用, 且任何虚拟地址的48位至63位必须与47位一致, 也就是说总的虚拟地址空间为256TB。 那么在64位架构下,如何分配虚拟地址空间的呢? 0000000000000000 – 00007fffffffffff(128TB)为用户空间, ffff800000000000 – ffffffffffffffff(128TB)为内核空间。 而且内核空间中有很多空洞, 越过第一个空洞后, ffff880000000000 – ffffc7ffffffffff(64TB) 才是直接映射物理内存的区域, 也就是说默认的PAGE_OFFSET为 ffff880000000000. 请关注下图的最左边,这就是目前64位的虚拟地址布局。 在本文的一开头提到的物理内存分区 ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM 就是与内核虚拟地址的直接映射有关的,如果读者不了解 内核直接映射物理地址这个概念的话,建议你去google一下,这个很简单的一一映射的概念。 既然现在内核直接映射的物理内存区域有64TB, 而且一般情况下,极少有计算机的内存能达到64TB(别说64TB了,1TB内存的也很少很少),所以整个内核虚拟地址空间都能够一一映射到计算机的物理内存上,因此,不再需要 ZONE_HIGHMEM这个分区了,现在对物理内存的划分,只有ZONE_DMA, ZONE_NORMAL。 如果你想有个更加直观的了解的话,请打开 /boot/config*** 文件,例如我的是 /boot/config-3....

2014-01-02 · Me