本文分析了Golang的socket文件描述符和goroutine阻塞调度的原理。代码中大部分是Go代码,小部分是汇编代码。完整理解本文需要Go语言知识,并且用Golang写过网络程序。更重要的是,需要提前理解goroutine的调度原理。

1. TCP的连接对象:

连接对象:

在net.go中有一个名为Conn的接口,提供了对于连接的读写和其他操作:

  1. type Conn interface {
  2. Read(b []byte) (n int, err error)
  3. Write(b []byte) (n int, err error)
  4. Close() error
  5. LocalAddr() Addr
  6. RemoteAddr() Addr
  7. SetReadDeadline(t time.Time) error
  8. SetWriteDeadline(t time.Time) error
  9. }

这个接口就是对下面的结构体conn的抽象。conn结构体包含了对连接的读写和其他操作:

  1. type conn struct {
  2. fd *netFD
  3. }

从连接读取数据:

  1. // Read implements the Conn Read method.
  2. func (c *conn) Read(b []byte) (int, error) {
  3. if !c.ok() {
  4. return 0, syscall.EINVAL
  5. }
  6. return c.fd.Read(b)
  7. }

向连接写入数据:

  1. // Write implements the Conn Write method.
  2. func (c *conn) Write(b []byte) (int, error) {
  3. if !c.ok() {
  4. return 0, syscall.EINVAL
  5. }
  6. return c.fd.Write(b)
  7. }

关闭连接:

  1. // Close closes the connection.
  2. func (c *conn) Close() error {
  3. if !c.ok() {
  4. return syscall.EINVAL
  5. }
  6. return c.fd.Close()
  7. }

设置读写超时:

  1. // SetDeadline implements the Conn SetDeadline method.
  2. func (c *conn) SetDeadline(t time.Time) error {
  3. if !c.ok() {
  4. return syscall.EINVAL
  5. }
  6. return c.fd.setDeadline(t)
  7. }
  8. // SetReadDeadline implements the Conn SetReadDeadline method.
  9. func (c *conn) SetReadDeadline(t time.Time) error {
  10. if !c.ok() {
  11. return syscall.EINVAL
  12. }
  13. return c.fd.setReadDeadline(t)
  14. }
  15. // SetWriteDeadline implements the Conn SetWriteDeadline method.
  16. func (c *conn) SetWriteDeadline(t time.Time) error {
  17. if !c.ok() {
  18. return syscall.EINVAL
  19. }
  20. return c.fd.setWriteDeadline(t)
  21. }

可以看到,对连接的所有操作,都体现在对*netFD的操作上。我们继续跟踪c.fd.Read()函数.

2.文件描述符

net/fd_unix.go:

网络连接的文件描述符:

  1. // Network file descriptor.
  2. type netFD struct {
  3. // locking/lifetime of sysfd + serialize access to Read and Write methods
  4. fdmu fdMutex
  5. // immutable until Close
  6. sysfd int
  7. family int
  8. sotype int
  9. isConnected bool
  10. net string
  11. laddr Addr
  12. raddr Addr
  13. // wait server
  14. pd pollDesc
  15. }

文件描述符读取数据:

  1. func (fd *netFD) Read(p []byte) (n int, err error) {
  2. if err := fd.readLock(); err != nil {
  3. return 0, err
  4. }
  5. defer fd.readUnlock()
  6. if err := fd.pd.PrepareRead(); err != nil {
  7. return 0, &OpError{"read", fd.net, fd.raddr, err}
  8. }
  9. // 调用system call,循环从fd.sysfd读取数据
  10. for {
  11. // 系统调用Read读取数据
  12. n, err = syscall.Read(int(fd.sysfd), p)
  13. // 如果发生错误,则需要处理
  14. // 并且只处理EAGAIN类型的错误,其他错误一律返回给调用者
  15. if err != nil {
  16. n = 0
  17. // 对于非阻塞的网络连接的文件描述符,如果错误是EAGAIN
  18. // 说明Socket的缓冲区为空,未读取到任何数据
  19. // 则调用fd.pd.WaitRead,
  20. if err == syscall.EAGAIN {
  21. if err = fd.pd.WaitRead(); err == nil {
  22. continue
  23. }
  24. }
  25. }
  26. err = chkReadErr(n, err, fd)
  27. break
  28. }
  29. if err != nil && err != io.EOF {
  30. err = &OpError{"read", fd.net, fd.raddr, err}
  31. }
  32. return
  33. }

网络轮询器

网络轮询器是Golang中针对每个socket文件描述符建立的轮询机制。 此处的轮询并不是一般意义上的轮询,而是Golang的runtime在调度goroutine或者GC完成之后或者指定时间之内,调用epoll_wait获取所有产生IO事件的socket文件描述符。当然在runtime轮询之前,需要将socket文件描述符和当前goroutine的相关信息加入epoll维护的数据结构中,并挂起当前goroutine,当IO就绪后,通过epoll返回的文件描述符和其中附带的goroutine的信息,重新恢复当前goroutine的执行。

  1. // Integrated network poller (platform-independent part).
  2. // 网络轮询器(平台独立部分)
  3. // A particular implementation (epoll/kqueue) must define the following functions:
  4. // 实际的实现(epoll/kqueue)必须定义以下函数:
  5. // func netpollinit() // to initialize the poller,初始化轮询器
  6. // func netpollopen(fd uintptr, pd *pollDesc) int32 // to arm edge-triggered notifications, 为fd和pd启动边缘触发通知
  7. // and associate fd with pd.
  8. // 一个实现必须调用下面的函数,用来指示pd已经准备好
  9. // An implementation must call the following function to denote that the pd is ready.
  10. // func netpollready(gpp **g, pd *pollDesc, mode int32)
  11. // pollDesc contains 2 binary semaphores, rg and wg, to park reader and writer
  12. // goroutines respectively. The semaphore can be in the following states:
  13. // pollDesc包含了2个二进制的信号,分别负责读写goroutine的暂停.
  14. // 信号可能处于下面的状态:
  15. // pdReady - IO就绪通知被挂起;
  16. // 一个goroutine将次状态置为nil来消费一个通知。
  17. // pdReady - io readiness notification is pending;
  18. // a goroutine consumes the notification by changing the state to nil.
  19. // pdWait - 一个goroutine准备暂停在信号上,但是还没有完成暂停。
  20. // 这个goroutine通过把这个状态改变为G指针去提交这个暂停动作。
  21. // 或者,替代性的,并行的其他通知将状态改变为READY.
  22. // 或者,替代性的,并行的超时/关闭会将次状态变为nil
  23. // pdWait - a goroutine prepares to park on the semaphore, but not yet parked;
  24. // the goroutine commits to park by changing the state to G pointer,
  25. // or, alternatively, concurrent io notification changes the state to READY,
  26. // or, alternatively, concurrent timeout/close changes the state to nil.
  27. // G指针 - 阻塞在信号上的goroutine
  28. // IO通知或者超时/关闭会分别将此状态置为READY或者nil.
  29. // G pointer - the goroutine is blocked on the semaphore;
  30. // io notification or timeout/close changes the state to READY or nil respectively
  31. // and unparks the goroutine.
  32. // nil - nothing of the above.
  33. const (
  34. pdReady uintptr = 1
  35. pdWait uintptr = 2
  36. )

网络轮询器的数据结构如下:

  1. // Network poller descriptor.
  2. // 网络轮询器描述符
  3. type pollDesc struct {
  4. link *pollDesc // in pollcache, protected by pollcache.lock
  5. // The lock protects pollOpen, pollSetDeadline, pollUnblock and deadlineimpl operations.
  6. // This fully covers seq, rt and wt variables. fd is constant throughout the PollDesc lifetime.
  7. // pollReset, pollWait, pollWaitCanceled and runtime·netpollready (IO readiness notification)
  8. // proceed w/o taking the lock. So closing, rg, rd, wg and wd are manipulated
  9. // in a lock-free way by all operations.
  10. // NOTE(dvyukov): the following code uses uintptr to store *g (rg/wg),
  11. // that will blow up when GC starts moving objects.
  12. //
  13. // lock锁对象保护了pollOpen, pollSetDeadline, pollUnblock和deadlineimpl操作。
  14. // 而这些操作又完全包含了对seq, rt, tw变量。
  15. // fd在PollDesc整个生命过程中都是一个常量。
  16. // 处理pollReset, pollWait, pollWaitCanceled和runtime.netpollready(IO就绪通知)不需要用到锁。
  17. // 所以closing, rg, rd, wg和wd的所有操作都是一个无锁的操作。
  18. lock mutex // protectes the following fields
  19. fd uintptr
  20. closing bool
  21. seq uintptr // protects from stale timers and ready notifications
  22. rg uintptr // pdReady, pdWait, G waiting for read or nil
  23. rt timer // read deadline timer (set if rt.f != nil)
  24. rd int64 // read deadline
  25. wg uintptr // pdReady, pdWait, G waiting for write or nil
  26. wt timer // write deadline timer
  27. wd int64 // write deadline
  28. user unsafe.Pointer // user settable cookie
  29. }

将当前goroutine设置为阻塞在fd上:

pd.WaitRead():

  1. func (pd *pollDesc) WaitRead() error {
  2. return pd.Wait('r')
  3. }
  4. func (pd *pollDesc) Wait(mode int) error {
  5. res := runtime_pollWait(pd.runtimeCtx, mode)
  6. return convertErr(res)
  7. }

res是runtime_pollWait函数返回的结果,由conevertErr函数包装后返回:

  1. func convertErr(res int) error {
  2. switch res {
  3. case 0:
  4. return nil
  5. case 1:
  6. return errClosing
  7. case 2:
  8. return errTimeout
  9. }
  10. println("unreachable: ", res)
  11. panic("unreachable")
  12. }
  • 函数返回0,表示IO已经准备好,返回nil。
  • 返回1,说明连接已关闭,应该放回errClosing。
  • 返回2,说明对IO进行的操作发生超时,应该返回errTimeout。

runtime_pollWait会调用runtime/thunk.s中的函数:

  1. TEXT net·runtime_pollWait(SB),NOSPLIT,$0-0
  2. JMP runtime·netpollWait(SB)

这是一个包装函数,没有参数,直接跳转到runtime/netpoll.go中的函数netpollWait:

  1. func netpollWait(pd *pollDesc, mode int) int {
  2. // 检查pd的状态是否异常
  3. err := netpollcheckerr(pd, int32(mode))
  4. if err != 0 {
  5. return err
  6. }
  7. // As for now only Solaris uses level-triggered IO.
  8. if GOOS == "solaris" {
  9. onM(func() {
  10. netpollarm(pd, mode)
  11. })
  12. }
  13. // 循环中检查pd的状态是不是已经被设置为pdReady
  14. // 即检查IO是不是已经就绪
  15. for !netpollblock(pd, int32(mode), false) {
  16. err = netpollcheckerr(pd, int32(mode))
  17. if err != 0 {
  18. return err
  19. }
  20. // Can happen if timeout has fired and unblocked us,
  21. // but before we had a chance to run, timeout has been reset.
  22. // Pretend it has not happened and retry.
  23. }
  24. return 0
  25. }

netpollcheckerr函数检查pd是否出现异常:


  1. // 检查pd的异常
  2. func netpollcheckerr(pd *pollDesc, mode int32) int {
  3. // 是否已经关闭
  4. if pd.closing {
  5. return 1 // errClosing
  6. }
  7. // 当读写状态下,deadline小于0,表示pd已经过了超时时间
  8. if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
  9. return 2 // errTimeout
  10. }
  11. // 正常情况返回0
  12. return 0
  13. }

netpollblock():

  1. // returns true if IO is ready, or false if timedout or closed
  2. // waitio - wait only for completed IO, ignore errors
  3. // 这个函数被netpollWait循环调用
  4. // 返回true说明IO已经准备好,返回false说明IO操作已经超时或者已经关闭
  5. func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
  6. // 获取pd的rg
  7. gpp := &pd.rg
  8. // 如果模式是w,则获取pd的wg
  9. if mode == 'w' {
  10. gpp = &pd.wg
  11. }
  12. // set the gpp semaphore to WAIT
  13. // 在循环中设置pd的gpp为pdWait
  14. // 因为casuintptr是自旋锁,所以需要在循环中调用
  15. for {
  16. // 如果在循环中发现IO已经准备好(pg的rg或者wg为pdReady状态)
  17. // 则设置rg/wg为0,返回true
  18. old := *gpp
  19. if old == pdReady {
  20. *gpp = 0
  21. return true
  22. }
  23. // 每次netpollblock执行完毕之后,gpp重置为0
  24. // 非0表示重复wait
  25. if old != 0 {
  26. gothrow("netpollblock: double wait")
  27. }
  28. // CAS操作改变gpp为pdWait
  29. if casuintptr(gpp, 0, pdWait) {
  30. break
  31. }
  32. }
  33. // need to recheck error states after setting gpp to WAIT
  34. // this is necessary because runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl
  35. // do the opposite: store to closing/rd/wd, membarrier, load of rg/wg
  36. //
  37. // 当设置gpp为pdWait状态后,重新检查gpp的状态
  38. // 这是必要的,因为runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl会做相反的操作
  39. // 如果状态正常则挂起当前的goroutine
  40. //
  41. // 当netpollcheckerr检查io出现超时或者错误,waitio为true可用于等待ioReady
  42. // 否则当waitio为false, 且io不出现错误或者超时才会挂起当前goroutine
  43. if waitio || netpollcheckerr(pd, mode) == 0 {
  44. // 解锁函数,设置gpp为pdWait,如果设置不成功
  45. // 说明已经是发生其他事件,可以让g继续运行,而不是挂起当前g
  46. f := netpollblockcommit
  47. // 尝试挂起当前g
  48. gopark(**(**unsafe.Pointer)(unsafe.Pointer(&f)), unsafe.Pointer(gpp), "IO wait")
  49. }
  50. // be careful to not lose concurrent READY notification
  51. old := xchguintptr(gpp, 0)
  52. if old > pdWait {
  53. gothrow("netpollblock: corrupted state")
  54. }
  55. return old == pdReady
  56. }

runtime/proc.go: gopark():

  1. // Puts the current goroutine into a waiting state and calls unlockf.
  2. // If unlockf returns false, the goroutine is resumed.
  3. // 将当前goroutine置为waiting状态,然后调用unlockf
  4. func gopark(unlockf unsafe.Pointer, lock unsafe.Pointer, reason string) {
  5. // 获取当前M
  6. mp := acquirem()
  7. // 获取当前G
  8. gp := mp.curg
  9. // 获取G的状态
  10. status := readgstatus(gp)
  11. // 如果不是_Grunning或者_Gscanrunning,则报错
  12. if status != _Grunning && status != _Gscanrunning {
  13. gothrow("gopark: bad g status")
  14. }
  15. // 设置lock和unlockf
  16. mp.waitlock = lock
  17. mp.waitunlockf = unlockf
  18. gp.waitreason = reason
  19. releasem(mp)
  20. // can't do anything that might move the G between Ms here.
  21. // 在m->g0这个栈上调用park_m,而不是当前g的栈
  22. mcall(park_m)
  23. }

mcall函数是一段汇编,在m->g0的栈上调用park_m,而不是在当前goroutine的栈上。mcall的功能分两部分,第一部分保存当前G的PC/SP到G的gobuf的pc/sp字段,第二部分调用park_m函数:

  1. // func mcall(fn func(*g))
  2. // Switch to m->g0's stack, call fn(g).
  3. // Fn must never return. It should gogo(&g->sched)
  4. // to keep running g.
  5. TEXT runtime·mcall(SB), NOSPLIT, $0-8
  6. // 将需要执行的函数保存在DI
  7. MOVQ fn+0(FP), DI
  8. // 将M的TLS存放在CX
  9. get_tls(CX)
  10. // 将G对象存放在AX
  11. MOVQ g(CX), AX // save state in g->sched
  12. // 将调用者的PC存放在BX
  13. MOVQ 0(SP), BX // caller's PC
  14. // 将调用者的PC保存到g->sched.pc
  15. MOVQ BX, (g_sched+gobuf_pc)(AX)
  16. // 第一个参数的地址,即栈顶的地址,保存到BX
  17. LEAQ fn+0(FP), BX // caller's SP
  18. // 保存SP的地址到g->sched.sp
  19. MOVQ BX, (g_sched+gobuf_sp)(AX)
  20. // 将g对象保存到g->sched->g
  21. MOVQ AX, (g_sched+gobuf_g)(AX)
  22. // switch to m->g0 & its stack, call fn
  23. // 将g对象指针保存到BX
  24. MOVQ g(CX), BX
  25. // 将g->m保存到BX
  26. MOVQ g_m(BX), BX
  27. // 将m->g0保存到SI
  28. MOVQ m_g0(BX), SI
  29. CMPQ SI, AX // if g == m->g0 call badmcall
  30. JNE 3(PC)
  31. MOVQ $runtime·badmcall(SB), AX
  32. JMP AX
  33. // 将m->g0保存到g
  34. MOVQ SI, g(CX) // g = m->g0
  35. // 将g->sched.sp恢复到SP寄存器
  36. // 即使用g0的栈
  37. MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp
  38. // AX进栈
  39. PUSHQ AX
  40. MOVQ DI, DX
  41. // 将fn的地址复制到DI
  42. MOVQ 0(DI), DI
  43. // 调用函数
  44. CALL DI
  45. // AX出栈
  46. POPQ AX
  47. MOVQ $runtime·badmcall2(SB), AX
  48. JMP AX
  49. RET

park_m函数的功能分为三部分,第一部分让当前G和当前M脱离关系,第二部分是调用解锁函数,这里是调用netpoll.go源文件中的netpollblockcommit函数:

  1. // runtime·park continuation on g0.
  2. void
  3. runtime·park_m(G *gp)
  4. {
  5. bool ok;
  6. // 设置当前g为Gwaiting状态
  7. runtime·casgstatus(gp, Grunning, Gwaiting);
  8. // 让当前g和m脱离关系
  9. dropg();
  10. if(g->m->waitunlockf) {
  11. ok = g->m->waitunlockf(gp, g->m->waitlock);
  12. g->m->waitunlockf = nil;
  13. g->m->waitlock = nil;
  14. // 返回0为false,非0为true
  15. // 0说明g->m->waitlock发生了变化,即不是在gopark是设置的(pdWait)
  16. // 说明了脱离了WAIT状态,应该设置为Grunnable,并执行g
  17. if(!ok) {
  18. runtime·casgstatus(gp, Gwaiting, Grunnable);
  19. execute(gp); // Schedule it back, never returns.
  20. }
  21. }
  22. // 这里是调度当前m继续执行其他g
  23. // 而不是上面执行execute
  24. schedule();
  25. }

netpollblockcommit函数,设置gpp为pdWait,设置成功返回1,否则返回0。1为true,0为false:

  1. func netpollblockcommit(gp *g, gpp unsafe.Pointer) bool {
  2. return casuintptr((*uintptr)(gpp), pdWait, uintptr(unsafe.Pointer(gp)))
  3. }

到这里当前goroutine对socket文件描述符的等待IO继续的行为已经完成。过程中首先尽早尝试判断IO是否已经就绪,如果未就绪则挂起当前goroutine,挂起之后再次判断IO是否就绪,如果还未就绪则调度当前M运行其他G。如果是在调度goroutine之前IO已经就绪,则不会使当前goroutine进入调度队列,会直接运行刚才挂起的G。否则当前goroutine会进入调度队列。

接下来是等待runtime将其唤醒。runtime在执行findrunnablequeuestarttheworldsysmon函数时,都会调用netpoll_epoll.go中的netpoll函数,寻找到IO就绪的socket文件描述符,并找到这些socket文件描述符对应的轮询器中附带的信息,根据这些信息将之前等待这些socket文件描述符就绪的goroutine状态修改为Grunnable。在以上函数中,执行完netpoll之后,会找到一个就绪的goroutine列表,接下来将就绪的goroutine加入到调度队列中,等待调度运行。

在netpoll_epoll.go中的netpoll函数中,epoll_wait函数返回N个发生事件的文件描述符对应的epollevent,接着对于每个event使用其data属性,将event.data转换为*pollDesc类型,再调用netpoll.go中的netpollready函数,将*pollDesc类型中的G数据类型去除,并附加到netpoll函数的调用者传递的G链表中:

  1. // 将ev.data转换为*pollDesc类型
  2. pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
  3. // 调用netpollready将取出pd中保存的G,并添加到链表中
  4. netpollready((**g)(noescape(unsafe.Pointer(&gp))), pd, mode)

所以runtime在执行findrunnablequeuestarttheworldsysmon函数中会执行netpoll函数,并返回N个goroutine。这些goroutine期待的网络事件已经发生,runtime会将这些goroutine放入到当前P的可运行队列中,接下来调度它们并运行。

http://ju.outofmemory.cn/entry/168649

Golang网络库中socket阻塞调度源码剖析的更多相关文章

  1. 自己实现多线程的socket,socketserver源码剖析

    1,IO多路复用 三种多路复用的机制:select.poll.epoll 用的多的两个:select和epoll 简单的说就是:1,select和poll所有平台都支持,epoll只有linux支持2 ...

  2. Netty学习笔记(三)——netty源码剖析

    1.Netty启动源码剖析 启动类: public class NettyNioServer { public static void main(String[] args) throws Excep ...

  3. Animate.css动画库,简单的使用,以及源码剖析

    animate.css是什么?能做些什么? animate.css是一个css动画库,使用它可以很方便的快捷的实现,我们想要的动画效果,而省去了操作js的麻烦.同时呢,它也是一个开源的库,在GitHu ...

  4. STL源码剖析之空间配置器

    本文大致对STL中的空间配置器进行一个简单的讲解,由于只是一篇博客类型的文章,无法将源码表现到面面俱到,所以真正感兴趣的码农们可以从源码中或者<STL源码剖析>仔细了解一下. 1,为什么S ...

  5. [开源] gnet: 一个轻量级且高性能的 Golang 网络库

    Github 主页 https://github.com/panjf2000/gnet 欢迎大家围观~~,目前还在持续更新,感兴趣的话可以 star 一下暗中观察哦. 简介 gnet 是一个基于 Ev ...

  6. (转)python标准库中socket模块详解

    python标准库中socket模块详解 socket模块简介 原文:http://www.lybbn.cn/data/datas.php?yw=71 网络上的两个程序通过一个双向的通信连接实现数据的 ...

  7. Golang 源码剖析:log 标准库

    Golang 源码剖析:log 标准库 原文地址:Golang 源码剖析:log 标准库 日志 输出 2018/09/28 20:03:08 EDDYCJY Blog... 构成 [日期]<空格 ...

  8. 【安卓网络请求开源框架Volley源码解析系列】定制自己的Request请求及Volley框架源码剖析

    通过前面的学习我们已经掌握了Volley的基本用法,没看过的建议大家先去阅读我的博文[安卓网络请求开源框架Volley源码解析系列]初识Volley及其基本用法.如StringRequest用来请求一 ...

  9. 细说并发5:Java 阻塞队列源码分析(下)

    上一篇 细说并发4:Java 阻塞队列源码分析(上) 我们了解了 ArrayBlockingQueue, LinkedBlockingQueue 和 PriorityBlockingQueue,这篇文 ...

随机推荐

  1. eclipse 使用maven 创建纯spring mvc项目

    接着eclipse 使用maven 创建web3.1项目 创建完成后, 讲spring mvc加入到项目中 先修改pom.xml文件 注意红色字部分 <project xmlns="h ...

  2. 利用js闭包获取索引号

    以tab选项卡效果为例: 网页中的选项卡效果

  3. IE9下Ajax缓存问题

    使用jQuery的getJSON从后台定时获取数据并刷新界面,使用以下方法时,在Chrome,Firefox下没问题,但在IE9下却无法刷新数据 $.getJSON(webApp + "/G ...

  4. a 标签的四个伪类

    link        有链接属性时visited    链接地址已被访问过active     被用户激活(在鼠标点击与释放之间发生的事件)hover      其鼠标悬停 <!DOCTYPE ...

  5. Python命令行参数sys.argv[]

    学习C语言的时候就没弄明白命令行参数的用法,在学习Pyton 的时候又遇到了命令行参数,在这里稍微学习了一下,稍微明白了一些在这里做个记录方便后面回顾复习. Sys.argv[]是用来获取命令行参数的 ...

  6. .NET日志工具介绍

    最近项目需要一个日志工具来跟踪程序便于调试和测试,为此研究了一下.NET日志工具,本文介绍了一些主流的日志框架并进行了对比.发表出来与大家分享. 综述 所谓日志(这里指程序日志)就是用于记录程序执行过 ...

  7. iOS UITableViewCell透明度 和 cell文字居中

    1.创建UITableViewCell时,的模式用UITableViewCellStyleValue1时,透明度直接将UITableView的透明度设置以下就搞定拉,但是文字居中难以实现. 2.创建U ...

  8. Boost正则表达式库regex常用search和match示例 - 编程语言 - 开发者第2241727个问答

    Boost正则表达式库regex常用search和match示例 - 编程语言 - 开发者第2241727个问答 Boost正则表达式库regex常用search和match示例 发表回复   Boo ...

  9. mininet 中图形化界面的安装

    just run a GUI in VM console window First, log in to the VM in its console window (i.e. type directl ...

  10. 可用的CSS文字两端对齐

    最近在工作项目中接触到Web界面设计的问题,要实现文字两端对齐的效果.在网上搜索了一下,用的都是类似的技巧: text-align:justify;text-justify:inter-ideogra ...