Blog

TASK_KILLABLE

kernel 为何要定义TASK_KILLABLE?其含义是什么? #

TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。这是一种深度睡眠状态,不可被信号唤醒,只能死等 I/O 操作完成。一旦 I/O 操作因为特殊原因不能完成,这个时候,谁也叫不醒这个进程了。你可能会说,我 kill 它呢?别忘了,kill 本身也是一个信号,既然这个状态不可被信号唤醒,kill 信号也被忽略了。除非重启电脑,没有其他办法。因此,这其实是一个比较危险的事情,除非程序员极其有把握,不然还是不要设置成 TASK_UNINTERRUPTIBLE。

于是,我们就有了一种新的进程睡眠状态,TASK_KILLABLE,可以终止的新睡眠状态。进程处于这种状态中,它的运行原理类似 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号。从定义可以看出,TASK_WAKEKILL 用于在接收到致命信号时唤醒进程,而 TASK_KILLABLE 相当于这两位都设置了。

#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)

Viewpoint #

From #

两种睡眠状态

同时是函数和构造器

Content #

JavaScript中用户用 function 关键字创建的函数必定同时是函数和构造器。不过,它们表现出来的行为效果却并不相同。

内置对象 Date 在作为构造器调用时产生新的对象,作为函数时,则产生字符串,见以下代码: console.log(new Date); console.log(Date())

Viewpoint #

From #

Refine线程

Refine线程 #

RSet 具体是何时被记录的呢?答案是写屏障。如下图所示: G1 在 RSet 中记录的也是 card。比如 Region1 中的对象 A 引用了 Region2 的对象 B,那么对象 A 所对应的 card 就会被记录在 Region2 的 RSet 中(注意!不是 Region1 的 RSet)。

在 G1 中,我们把这种 card 称为 dirty card。和 SATB 相似,业务线程也不是直接将 dirty card 放到 RSet 中的。而是在业务线程中引入一个叫做 dirty card queue(DCQ)的队列,在写屏障中,业务线程只需要将 dirty card 放入 DCQ 中,而不做非常细致的检查。

接下来,GC 线程中,有一类特殊的线程,它们会从 DCQ 中找到这种 dirty card,然后再去做更精细的检查,只有确实不属于 RSet的维护策略中所描述的三种情况的跨区引用,才真正放到专属 RSet 中去。这一类特殊的线程就是 G1 GC 中的 Refine 线程。

Viewpoint #

From #

22 | G1 GC:分区回收算法说的是什么?

...

RSet的维护策略

RSet的维护策略 #

RSet 的维护策略,也就是说哪些引用关系需要加入到 RSet:

  1. 如果是同一个 Region 的对象,它们之间相互引用是不必维护的,这个很好理解,因为不存在跨 Region 的问题;
  2. 由年轻代 Region 出发到其他 Region 的,无论目标是年轻代还是老年代,这一类引用也都不用维护。因为结合 young GC 和 mixed GC 的策略可以知道,无论是什么回收模式,年轻代的全部 Region 都会被清理,这就意味着一定会对年轻代的所有对象进行遍历;
  3. 从 CSet 集合的 Region 出发指向其他 Region 的,也不需要维护,理由和第 2 点是一样的。

总的来说,RSet 需要维护的引用关系只有两种:

  1. 非 CSet 老年代 Region 到年轻代 Region 的引用。
  2. 非 CSet 老年代 Region 到 CSet 老年代 Region 的引用。

Viewpoint #

From #

22 | G1 GC:分区回收算法说的是什么?

错误行为特征检视策略

错误行为特征检视策略 #

哨兵错误处理策略错误值类型检视策略在错误的构造方与错误处理方两者之间建立了耦合。我们是否还有手段可以降低错误处理方与错误值构造方的耦合呢?

在 Go 标准库中,我们发现了这样一种错误处理方式:将某个包中的错误类型归类,统一提取出一些公共的错误行为特征,并将这些错误行为特征放入一个公开的接口类型中。这种方式也被叫做错误行为特征检视策略。

以标准库中的net包为例,它将包内的所有错误类型的公共行为特征抽象并放入 net.Error这个接口中,如下面代码:

// $GOROOT/src/net/net.go
type Error interface {
    error
    Timeout() bool
    Temporary() bool
}

我们看到,net.Error 接口包含两个用于判断错误行为特征的方法:Timeout 用来判断是否是超时(Timeout)错误,Temporary 用于判断是否是临时(Temporary)错误。

而错误处理方只需要依赖这个公共接口,就可以检视具体错误值的错误行为特征信息,并根据这些信息做出后续错误处理分支选择的决策。

这里,我们再看一个 http 包使用错误行为特征检视策略进行错误处理的例子,加深下理解:

// $GOROOT/src/net/http/server.go
func (srv *Server) Serve(l net.Listener) error {
    ... ...
    for {
        rw, e := l.Accept()
        if e != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                // 注:这里对临时性(temporary)错误进行处理
                ... ...
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        ...
    }
    ... ...
}

在上面代码中,Accept 方法实际上返回的错误类型为*OpError,它是 net 包中的一个自定义错误类型,它实现了错误公共特征接口net.Error,如下代码所示:

// $GOROOT/src/net/net.go
type OpError struct {
    ... ...
    // Err is the error that occurred during the operation.
    Err error
}
type temporary interface {
    Temporary() bool
}
func (e *OpError) Temporary() bool {
  if ne, ok := e.Err.(*os.SyscallError); ok {
      t, ok := ne.Err.(temporary)
      return ok && t.Temporary()
  }
  t, ok := e.Err.(temporary)
  return ok && t.Temporary()
}

因此,OpError 实例可以被错误处理方通过net.Error接口的方法,判断它的行为是否满足 Temporary 或 Timeout 特征。

...

哨兵错误处理策略

哨兵错误处理策略 #

Go 标准库采用了定义导出的(Exported)“哨兵”错误值的方式,来辅助错误处理方检视(inspect)错误值并做出错误处理分支的决策,比如下面的 bufio 包中定义的“哨兵错误”:

// $GOROOT/src/bufio/bufio.go
var (
    ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
    ErrBufferFull        = errors.New("bufio: buffer full")
    ErrNegativeCount     = errors.New("bufio: negative count")
)

下面的代码片段利用了上面的哨兵错误,进行错误处理分支的决策:

data, err := b.Peek(1)
if err != nil {
    switch err {
    case bufio.ErrNegativeCount:
        // ... ...
        return
    case bufio.ErrBufferFull:
        // ... ...
        return
    case bufio.ErrInvalidUnreadByte:
        // ... ...
        return
    default:
        // ... ...
        return
    }
}

你可以看到,一般“哨兵”错误值变量以 ErrXXX 格式命名。和透明错误策略相比,“哨兵”策略让错误处理方在有检视错误值的需求时候,可以“有的放矢”。

不过,对于 API 的开发者而言,暴露“哨兵”错误值也意味着这些错误值和包的公共函数 / 方法一起成为了 API 的一部分。一旦发布出去,开发者就要对它进行很好的维护。而“哨兵”错误值也让使用这些值的错误处理方对它产生了依赖。

...

错误值类型检视策略

错误值类型检视策略 #

哨兵错误处理策略,除了让错误处理方可以“有的放矢”的进行值比较之外,并没有提供其他有效的错误上下文信息。那如果遇到错误处理方需要错误值提供更多的“错误上下文”的情况,上面这些错误处理策略和错误值构造方式都无法满足。

这种情况下,我们需要通过自定义错误类型的构造错误值的方式,来提供更多的“错误上下文”信息。并且,由于错误值都通过 error 接口变量统一呈现,要得到底层错误类型携带的错误上下文信息,错误处理方需要使用 Go 提供的类型断言机制(Type Assertion)或类型选择机制(Type Switch),这种错误处理方式,我称之为错误值类型检视策略。

我们来看一个标准库中的例子加深下理解,这个 json 包中自定义了一个 UnmarshalTypeError的错误类型:

// $GOROOT/src/encoding/json/decode.go
type UnmarshalTypeError struct {
    Value  string
    Type   reflect.Type
    Offset int64
    Struct string
    Field  string
}

错误处理方可以通过错误类型检视策略,获得更多错误值的错误上下文信息,下面就是利用这一策略的 json 包的一个方法的实现:

// $GOROOT/src/encoding/json/decode.go
func (d *decodeState) addErrorContext(err error) error {
    if d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0 {
        switch err := err.(type) {
        case *UnmarshalTypeError:
            err.Struct = d.errorContext.Struct.Name()
            err.Field = strings.Join(d.errorContext.FieldStack, ".")
            return err
        }
    }
    return err
}

我们看到,这段代码通过类型 switch 语句得到了 err 变量代表的动态类型和值,然后在匹配的 case 分支中利用错误上下文信息进行处理。

这里,一般自定义导出的错误类型以XXXError的形式命名。和“哨兵”错误处理策略一样,错误值类型检视策略,由于暴露了自定义的错误类型给错误处理方,因此这些错误类型也和包的公共函数 / 方法一起,成为了 API 的一部分。一旦发布出去,开发者就要对它们进行很好的维护。而它们也让使用这些类型进行检视的错误处理方对其产生了依赖。

从 Go 1.13 版本开始,标准库 errors 包提供了As函数给错误处理方检视错误值。As函数类似于通过类型断言判断一个 error 类型变量是否为特定的自定义错误类型,如下面代码所示:

...

判断包装错误WrappedError

判断包装错误WrappedError #

从 Go 1.13 版本开始,标准库 errors 包提供了 Is 函数用于错误处理方对错误值的检视。Is 函数类似于把一个 error 类型变量与“哨兵”错误值进行比较,比如下面代码:

// 类似 if err == ErrOutOfBounds{ … }
if errors.Is(err, ErrOutOfBounds) {
    // 越界的错误处理
}

不同的是,如果 error 类型变量的底层错误值是一个包装错误(Wrapped Error),errors.Is 方法会沿着该包装错误所在错误链(Error Chain),与链上所有被包装的错误(Wrapped Error)进行比较,直至找到一个匹配的错误为止。下面是 Is 函数应用的一个例子:

var ErrSentinel = errors.New("the underlying sentinel error")
func main() {
  err1 := fmt.Errorf("wrap sentinel: %w", ErrSentinel)
  err2 := fmt.Errorf("wrap err1: %w", err1)
    println(err2 == ErrSentinel) //false
  if errors.Is(err2, ErrSentinel) {
    println("err2 is ErrSentinel")
    return
  }
  println("err2 is not ErrSentinel")
}

在这个例子中,我们通过 fmt.Errorf 函数,并且使用 %w 创建包装错误变量 err1 和 err2,其中 err1 实现了对 ErrSentinel 这个“哨兵错误值”的包装,而 err2 又对 err1 进行了包装,这样就形成了一条错误链。位于错误链最上层的是 err2,位于最底层的是 ErrSentinel。之后,我们再分别通过值比较和 errors.Is 这两种方法,判断 err2 与 ErrSentinel 的关系。运行上述代码,我们会看到如下结果:

...

两种构造错误值的方法

两种构造错误值的方法 #

Go 语言的设计者显然也想到了这一点,他们在标准库中提供了两种方便 Go 开发者构造错误值的方法: errors.New和fmt.Errorf。使用这两种方法,我们可以轻松构造出一个满足 error 接口的错误值,就像下面代码这样:

err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("index %d is out of bounds", i)

这两种方法实际上返回的是同一个实现了 error 接口的类型的实例,这个未导出的类型就是errors.errorString,它的定义是这样的:

// $GOROOT/src/errors/errors.go
type errorString struct {
    s string
}
func (e *errorString) Error() string {
    return e.s
}

大多数情况下,使用这两种方法构建的错误值就可以满足我们的需求了。但我们也要看到,虽然这两种构建错误值的方法很方便,但它们给错误处理者提供的错误上下文(Error Context)只限于以字符串形式呈现的信息,也就是 Error 方法返回的信息。

Viewpoint #

From #

22|函数:怎么结合多返回值进行错误处理?

显式转换为HandlerFunc类型

显式转换为HandlerFunc类型 #

函数也可以被显式转型。并且,这样的转型在特定的领域具有奇妙的作用,一个最为典型的示例就是标准库 http 包中的 HandlerFunc 这个类型。我们来看一个使用了这个类型的例子:

func greeting(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome, Gopher!\n")
}
func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greeting))
}

我们先来看一下 http 包的函数 ListenAndServe 的源码:

// $GOROOT/src/net/http/server.go
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

函数 ListenAndServe 会把来自客户端的 http 请求,交给它的第二个参数 handler 处理,而这里 handler 参数的类型 http.Handler,是一个自定义的接口类型,它的源码是这样的:

// $GOROOT/src/net/http/server.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

这个接口只有一个方法 ServeHTTP,他的函数类型是 func(http.ResponseWriter, *http.Request)。这和我们自己定义的 http 请求处理函数 greeting 的类型是一致的,但是我们没法直接将 greeting 作为参数值传入,否则编译器会报错:

func(http.ResponseWriter, *http.Request) does not implement http.Handler (missing ServeHTTP method)

这里,编译器提示我们,函数 greeting 还没有实现接口 Handler 的方法,无法将它赋值给 Handler 类型的参数。现在我们再回过头来看下代码,代码中我们也没有直接将 greeting 传给 ListenAndServe 函数,而是将 http.HandlerFunc(greeting)作为参数传给了 ListenAndServe。那这个 http.HandlerFunc 究竟是什么呢?我们直接来看一下它的源码:

...