0%

在网络编程中,经常会提到同步/异步,阻塞/非阻塞的概念,记得一开始的时候我总是分不清它们之间有什么区别,所以经常混淆。其实它们之间是有着一层包含与被包含的关系,其中同步包含了阻塞与非阻塞,而异步则是另一种情况。可以划分为三类:

  • 同步阻塞
  • 同步非阻塞
  • 异步

同步阻塞

Linux上的IO默认情况下均为阻塞IO(aio系列除外),所有的套接字也默认为阻塞套接字。下图以UDP数据报为例,展示了阻塞IO的基本过程:

调用recvfrom,其系统调用直到数据报到达并且被复制到用户缓冲区中或者发生错误才返回。我们所说的「阻塞」是指系统调用等待数据报达到这一行为。

这个过程就相当于一个人去书店买书,但是书卖完了,于是他就在书店一直等到书店到货并买到书之后才回家。

阅读全文 »

对于程序来说,如果主进程在子进程还未结束时就已经退出,那么Linux内核会将子进程的父进程ID改为1(也就是init进程),当子进程结束后会由init进程来回收该子进程。

那如果是把进程换成线程的话,会怎么样呢?假设主线程在子线程结束前就已经退出,子线程会发生什么?

在一些论坛上看到许多人说子线程也会跟着退出,其实这是错误的,原因在于他们混淆了线程退出和进程退出概念。实际的答案是主线程退出后子线程的状态依赖于它所在的进程,如果进程没有退出的话子线程依然正常运转。如果进程退出了,那么它所有的线程都会退出,所以子线程也就退出了。

主线程先退出

先来看一个主线程先退出的例子:

#include <pthread.h>
#include <unistd.h>

#include <stdio.h>

void* func(void* arg)
{
pthread_t main_tid = *static_cast<pthread_t*>(arg);
pthread_cancel(main_tid);
while (true)
{
//printf("child loops\n");
}
return NULL;
}

int main(int argc, char* argv[])
{
pthread_t main_tid = pthread_self();
pthread_t tid = 0;
pthread_create(&tid, NULL, func, &main_tid);
while (true)
{
printf("main loops\n");
}
sleep(1);
printf("main exit\n");
return 0;
}

把主线程的线程号传给子线程,在子线程中通过pthread_cancel终止主线程使其退出。运行程序,可以发现在打印了一定数量的「main loops」之后程序就挂起了,但却没有退出。
阅读全文 »

我们知道程序在运行时的机器码(程序代码)、栈、堆都是在内存(存储器)上的一段空间,然后CPU依着一定的规则读取、翻译指令,「指挥」计算机如何存取内存上的数据。本文以Linux上的C程序为例,简单介绍一下这其中的运行机制。

内存布局

每个Linux程序都有一个运行时的内存布局,主要结构如上图所示。在32位的Linux系统中,一个进程的虚拟地址空间最多为4GB,其中1GB(一般为地址空间的高位1G空间)是为内核保留的,剩下的3GB可为用户态所用。代码段总是从地址0x08048000初开始。数据段是在接下来的一个4KB对齐的地址处。运行时的堆在读/写段之后接下来第一个4KB对齐的地址处,并通过调用glibc库函数malloc往上增长。还有一段是为共享库保留的。用户栈总是从最大的合法用户地址开始,向下增长(向低地址方向增长)。从栈的上部开始的段是为内核的代码和数据保留的。

  • .init:在该段定义了一个小函数_init,程序的初始化代码会调用它。
  • .text:已编译程序的机器代码。
  • .rodata:只读数据。比如字符串字面值、switch的跳转表、C++中的虚函数表都存放此段。
  • .data:已初始化的全局变量。
  • .bss:未初始化的全局变量。
  • 运行时堆:一般通过调用glibc库函数malloc往上增长。
  • 共享库映射区域:一般是glibc或者其他动态链接库的映射区域,如printf等等。
  • 用户栈:维持程序运行时的栈帧结构。
  • 内核段:为内核态的代码和数据所保留的地址空间。
    阅读全文 »

对于return和exit,想必大家都不会陌生。

很多人以为在main函数中return和调用exit差不多,反正都是程序结束了。但其实并不是这样的,这里面还有许多门道。

fork与vfork

想一想下面两种情况会发生什么?

  • 分别在fork出的子进程中调用return和exit。
  • 分别在vfork出的子进程中调用return和exit。

在fork出的子进程的main函数中调用return和exit,都无异常情况发生,程序正常运行退出。在vfork出的子进程中调用exit,程序正常退出,而在vfork出的子进程的main函数中return,程序就直接挂掉了。

先说说fork与vfork的区别:

  • fork是创建一个子进程并得到与父进程相同的虚拟地址空间的一份独立的拷贝,也就是把父进程的内存数据都拷贝到子进程中了。(在实际中,一般采用的是写时复制COW)
  • vfork是创建一个子进程并与父进程共享虚拟地址空间(父进程的),也就是和父进程共享所有内存数据。

那么为什么会有vfork这么一个玩意呢?据说一开始unix下只有fork,但是很多程序员喜欢在fork后立马使用exec执行一个外部程序,于是fork需要复制父进程的内存数据这一动作就变得毫无意义了(注:开始的时候fork并没有使用COW机制)。于是就弄出了与父进程共享数据的vfork,方便高效的使用exec执行外部程序。

再回到上面的问题,为什么在vfork出的子进程的main函数里return程序就直接crash掉了?

阅读全文 »

Google Protocol Buffer(简称protbuf)是一种紧凑的可扩展的二进制消息格式,并且如上一篇文章《解析Google Protocol Buffer消息类型的自动反射原理》中介绍的那样,它还自带消息类型反射的功能。从Google公布的benchmark来看,protobuf的效率还是很高的,这主要依赖于它的高效编解码方式。

先来看一个简单的例子:

message Test1
{
required int32 id = 1;
}

在一个应用中,我们将一个Test1的Message中的id设为150,将这个Message进行编码序列化以后以16进制输出到stdout中,会看到这样的3个字节:
08 96 01

也就是说只用了3个字节就表示了数据类型和数据的值。

Base 128 Varints

Varints是protobuf的序列化整型数据的一种编码方式,它的优点是整型数据的值越小,编码后所用的字节数越小。

经过Varints编码后的数据,它的每一个字节8bit的高位代表一个标记位。如果标记位是1,则代表下一个字节仍然是当前整型数据的组成;如果标记位是0,则代表下一个字节就是下一个整型数据了。

先看最简单的数值1,一个字节足以表示它,所以它的标记位置0:

0000 0001

再看数值300,经过Varints编码后的序列:
1010 1100 0000 0010

从左到右,依次将每个字节的高位(标志位)去掉,若标志位为1,则下一个字节仍是当前数据的组成,若标志位为0,则下一个字节是下一个数据的组成。
1010 1100 0000 0010
→ 010 1100 000 0010

因为protobuf是以little endian来编码字节序的,所以将两个字节交换、拼接,即可得到原始数据的二进制表示:
000 0010  010 1100
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300

阅读全文 »