2019年2月

在比较 Python 框架的系列文章的第三部分中,我们来了解 Tornado,它是为处理异步进程而构建的。

在这个由四部分组成的系列文章的前两篇中,我们介绍了 PyramidFlask Web 框架。我们已经构建了两次相同的应用程序,看到了一个完整的 DIY 框架和包含了更多功能的框架之间的异同。

现在让我们来看看另一个稍微不同的选择:Tornado 框架。Tornado 在很大程度上与 Flask 一样简单,但有一个主要区别:Tornado 是专门为处理异步进程而构建的。在我们本系列所构建的应用程序中,这种特殊的酱料(LCTT 译注:这里意思是 Tornado 的异步功能)在我们构建的 app 中并不是非常有用,但我们将看到在哪里可以使用它,以及它在更一般的情况下是如何工作的。

让我们继续前两篇文章中模式,首先从处理设置和配置开始。

Tornado 启动和配置

如果你一直关注这个系列,那么第一步应该对你来说习以为常。

$ mkdir tornado_todo
$ cd tornado_todo
$ pipenv install --python 3.6
$ pipenv shell
(tornado-someHash) $ pipenv install tornado

创建一个 setup.py 文件来安装我们的应用程序相关的东西:

(tornado-someHash) $ touch setup.py
# setup.py
from setuptools import setup, find_packages

requires = [
    'tornado',
    'tornado-sqlalchemy',
    'psycopg2',
]

setup(
    name='tornado_todo',
    version='0.0',
    description='A To-Do List built with Tornado',
    author='<Your name>',
    author_email='<Your email>',
    keywords='web tornado',
    packages=find_packages(),
    install_requires=requires,
    entry_points={
        'console_scripts': [
            'serve_app = todo:main',
        ],
    },
)

因为 Tornado 不需要任何外部配置,所以我们可以直接编写 Python 代码来让程序运行。让我们创建 todo 目录,并用需要的前几个文件填充它。

todo/
    __init__.py
    models.py
    views.py

就像 Flask 和 Pyramid 一样,Tornado 也有一些基本配置,放在 __init__.py 中。从 tornado.web 中,我们将导入 Application 对象,它将处理路由和视图的连接,包括数据库(当我们谈到那里时再说)以及运行 Tornado 应用程序所需的其它额外设置。

# __init__.py
from tornado.web import Application

def main():
    """Construct and serve the tornado application."""
    app = Application()

像 Flask 一样,Tornado 主要是一个 DIY 框架。当构建我们的 app 时,我们必须设置该应用实例。因为 Tornado 用它自己的 HTTP 服务器来提供该应用,我们必须设置如何提供该应用。首先,在 tornado.options.define 中定义要监听的端口。然后我们实例化 Tornado 的 HTTPServer,将该 Application 对象的实例作为参数传递给它。

# __init__.py
from tornado.httpserver import HTTPServer
from tornado.options import define, options
from tornado.web import Application

define('port', default=8888, help='port to listen on')

def main():
    """Construct and serve the tornado application."""
    app = Application()
    http_server = HTTPServer(app)
    http_server.listen(options.port)

当我们使用 define 函数时,我们最终会在 options 对象上创建属性。第一个参数位置的任何内容都将是属性的名称,分配给 default 关键字参数的内容将是该属性的值。

例如,如果我们将属性命名为 potato 而不是 port,我们可以通过 options.potato 访问它的值。

HTTPServer 上调用 listen 并不会启动服务器。我们必须再做一步,找一个可以监听请求并返回响应的工作应用程序,我们需要一个输入输出循环。幸运的是,Tornado 以 tornado.ioloop.IOLoop 的形式提供了开箱即用的功能。

# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application

define('port', default=8888, help='port to listen on')

def main():
    """Construct and serve the tornado application."""
    app = Application()
    http_server = HTTPServer(app)
    http_server.listen(options.port)
    print('Listening on http://localhost:%i' % options.port)
    IOLoop.current().start()

我喜欢某种形式的 print 语句,来告诉我什么时候应用程序正在提供服务,这是我的习惯。如果你愿意,可以不使用 print

我们以 IOLoop.current().start() 开始我们的 I/O 循环。让我们进一步讨论输入,输出和异步性。

Python 中的异步和 I/O 循环的基础知识

请允许我提前说明,我绝对,肯定,一定并且放心地说不是异步编程方面的专家。就像我写的所有内容一样,接下来的内容源于我对这个概念的理解的局限性。因为我是人,可能有很深很深的缺陷。

异步程序的主要问题是:

* 数据如何进来?
* 数据如何出去?
* 什么时候可以在不占用我全部注意力情况下运行某个过程?

由于全局解释器锁(GIL),Python 被设计为一种单线程)语言。对于 Python 程序必须执行的每个任务,其线程执行的全部注意力都集中在该任务的持续时间内。我们的 HTTP 服务器是用 Python 编写的,因此,当接收到数据(如 HTTP 请求)时,服务器的唯一关心的是传入的数据。这意味着,在大多数情况下,无论是程序需要运行还是处理数据,程序都将完全消耗服务器的执行线程,阻止接收其它可能的数据,直到服务器完成它需要做的事情。

在许多情况下,这不是太成问题。典型的 Web 请求,响应周期只需要几分之一秒。除此之外,构建 HTTP 服务器的套接字可以维护待处理的传入请求的积压。因此,如果请求在该套接字处理其它内容时进入,则它很可能只是在处理之前稍微排队等待一会。对于低到中等流量的站点,几分之一秒的时间并不是什么大问题,你可以使用多个部署的实例以及 NGINX 等负载均衡器来为更大的请求负载分配流量。

但是,如果你的平均响应时间超过一秒钟,该怎么办?如果你使用来自传入请求的数据来启动一些长时间的过程(如机器学习算法或某些海量数据库查询),该怎么办?现在,你的单线程 Web 服务器开始累积一个无法寻址的积压请求,其中一些请求会因为超时而被丢弃。这不是一种选择,特别是如果你希望你的服务在一段时间内是可靠的。

异步 Python 程序登场。重要的是要记住因为它是用 Python 编写的,所以程序仍然是一个单线程进程。除非特别标记,否则在异步程序中仍然会阻塞执行。

但是,当异步程序结构正确时,只要你指定某个函数应该具有这样的能力,你的异步 Python 程序就可以“搁置”长时间运行的任务。然后,当搁置的任务完成并准备好恢复时,异步控制器会收到报告,只要在需要时管理它们的执行,而不会完全阻塞对新输入的处理。

这有点夸张,所以让我们用一个人类的例子来证明。

带回家吧

我经常发现自己在家里试图完成很多家务,但没有多少时间来做它们。在某一天,积压的家务可能看起来像:

* 做饭(20 分钟准备,40 分钟烹饪)
* 洗碗(60 分钟)
* 洗涤并擦干衣物(30 分钟洗涤,每次干燥 90 分钟)
* 真空清洗地板(30 分钟)

如果我是一个传统的同步程序,我会亲自完成每项任务。在我考虑处理任何其他事情之前,每项任务都需要我全神贯注地完成。因为如果没有我的全力关注,什么事情都完成不了。所以我的执行顺序可能如下:

1. 完全专注于准备和烹饪食物,包括等待食物烹饪(60 分钟)
2. 将脏盘子移到水槽中(65 分钟过去了)
3. 清洗所有盘子(125 分钟过去了)
4. 开始完全专注于洗衣服,包括等待洗衣机洗完,然后将衣物转移到烘干机,再等烘干机完成( 250 分钟过去了)
5. 对地板进行真空吸尘(280 分钟了)

从头到尾完成所有事情花费了 4 小时 40 分钟。

我应该像异步程序一样聪明地工作,而不是努力工作。我的家里到处都是可以为我工作的机器,而不用我一直努力工作。同时,现在我可以将注意力转移真正需要的东西上。

我的执行顺序可能看起来像:

1. 将衣物放入洗衣机并启动它(5 分钟)
2. 在洗衣机运行时,准备食物(25 分钟过去了)
3. 准备好食物后,开始烹饪食物(30 分钟过去了)
4. 在烹饪食物时,将衣物从洗衣机移到烘干机机中开始烘干(35 分钟过去了)
5. 当烘干机运行中,且食物仍在烹饪时,对地板进行真空吸尘(65 分钟过去了)
6. 吸尘后,将食物从炉子中取出并装盘子入洗碗机(70 分钟过去了)
7. 运行洗碗机(130 分钟完成)

现在花费的时间下降到 2 小时 10 分钟。即使我允许在作业之间切换花费更多时间(总共 10-20 分钟)。如果我等待着按顺序执行每项任务,我花费的时间仍然只有一半左右。这就是将程序构造为异步的强大功能。

那么 I/O 循环在哪里?

一个异步 Python 程序的工作方式是从某个外部源(输入)获取数据,如果某个进程需要,则将该数据转移到某个外部工作者(输出)进行处理。当外部进程完成时,Python 主程序会收到提醒,然后程序获取外部处理(输入)的结果,并继续这样其乐融融的方式。

当数据不在 Python 主程序手中时,主程序就会被释放来处理其它任何事情。包括等待全新的输入(如 HTTP 请求)和处理长时间运行的进程的结果(如机器学习算法的结果,长时间运行的数据库查询)。主程序虽仍然是单线程的,但成了事件驱动的,它对程序处理的特定事件会触发动作。监听这些事件并指示应如何处理它们的主要是 I/O 循环在工作。

我知道,我们走了很长的路才得到这个重要的解释,但我希望在这里传达的是,它不是魔术,也不是某种复杂的并行处理或多线程工作。全局解释器锁仍然存在,主程序中任何长时间运行的进程仍然会阻塞其它任何事情的进行,该程序仍然是单线程的。然而,通过将繁琐的工作外部化,我们可以将线程的注意力集中在它需要注意的地方。

这有点像我上面的异步任务。当我的注意力完全集中在准备食物上时,它就是我所能做的一切。然而,当我能让炉子帮我做饭,洗碗机帮我洗碗,洗衣机和烘干机帮我洗衣服时,我的注意力就会被释放出来,去做其它事情。当我被提醒,我的一个长时间运行的任务已经完成并准备再次处理时,如果我的注意力是空闲的,我可以获取该任务的结果,并对其做下一步需要做的任何事情。

Tornado 路由和视图

尽管经历了在 Python 中讨论异步的所有麻烦,我们还是决定暂不使用它。先来编写一个基本的 Tornado 视图。

与我们在 Flask 和 Pyramid 实现中看到的基于函数的视图不同,Tornado 的视图都是基于类的。这意味着我们将不在使用单独的、独立的函数来规定如何处理请求。相反,传入的 HTTP 请求将被捕获并将其分配为我们定义的类的一个属性。然后,它的方法将处理相应的请求类型。

让我们从一个基本的视图开始,即在屏幕上打印 “Hello, World”。我们为 Tornado 应用程序构造的每个基于类的视图都必须继承 tornado.web 中的 RequestHandler 对象。这将设置我们需要(但不想写)的所有底层逻辑来接收请求,同时构造正确格式的 HTTP 响应。

from tornado.web import RequestHandler

class HelloWorld(RequestHandler):
    """Print 'Hello, world!' as the response body."""

    def get(self):
        """Handle a GET request for saying Hello World!."""
        self.write("Hello, world!")

因为我们要处理 GET 请求,所以我们声明(实际上是重写)了 get 方法。我们提供文本或 JSON 可序列化对象,用 self.write 写入响应体。之后,我们让 RequestHandler 来做在发送响应之前必须完成的其它工作。

就目前而言,此视图与 Tornado 应用程序本身并没有实际连接。我们必须回到 __init__.py,并稍微更新 main 函数。以下是新的内容:

# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application
from todo.views import HelloWorld

define('port', default=8888, help='port to listen on')

def main():
    """Construct and serve the tornado application."""
    app = Application([
        ('/', HelloWorld)
    ])
    http_server = HTTPServer(app)
    http_server.listen(options.port)
    print('Listening on http://localhost:%i' % options.port)
    IOLoop.current().start()

我们做了什么

我们将 views.py 文件中的 HelloWorld 视图导入到脚本 __init__.py 的顶部。然后我们添加了一个路由-视图对应的列表,作为 Application 实例化的第一个参数。每当我们想要在应用程序中声明一个路由时,它必须绑定到一个视图。如果需要,可以对多个路由使用相同的视图,但每个路由必须有一个视图。

我们可以通过在 setup.py 中启用的 serve_app 命令来运行应用程序,从而确保这一切都能正常工作。查看 http://localhost:8888/ 并看到它显示 “Hello, world!”。

当然,在这个领域中我们还能做更多,也将做更多,但现在让我们来讨论模型吧。

连接数据库

如果我们想要保留数据,就需要连接数据库。与 Flask 一样,我们将使用一个特定于框架的 SQLAchemy 变体,名为 tornado-sqlalchemy

为什么要使用它而不是 SQLAlchemy 呢?好吧,其实 tornado-sqlalchemy 具有简单 SQLAlchemy 的所有优点,因此我们仍然可以使用通用的 Base 声明模型,并使用我们习以为常的所有列数据类型和关系。除了我们已经惯常了解到的,tornado-sqlalchemy 还为其数据库查询功能提供了一种可访问的异步模式,专门用于与 Tornado 现有的 I/O 循环一起工作。

我们通过将 tornado-sqlalchemypsycopg2 添加到 setup.py 到所需包的列表并重新安装包来创建环境。在 models.py 中,我们声明了模型。这一步看起来与我们在 Flask 和 Pyramid 中已经看到的完全一样,所以我将跳过全部声明,只列出了 Task 模型的必要部分。

# 这不是完整的 models.py, 但是足够看到不同点
from tornado_sqlalchemy import declarative_base

Base = declarative_base

class Task(Base):
    # 等等,因为剩下的几乎所有的东西都一样 ...

我们仍然需要将 tornado-sqlalchemy 连接到实际应用程序。在 __init__.py 中,我们将定义数据库并将其集成到应用程序中。

# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application
from todo.views import HelloWorld

# add these
import os
from tornado_sqlalchemy import make_session_factory

define('port', default=8888, help='port to listen on')
factory = make_session_factory(os.environ.get('DATABASE_URL', ''))

def main():
    """Construct and serve the tornado application."""
    app = Application([
        ('/', HelloWorld)
    ],
        session_factory=factory
    )
    http_server = HTTPServer(app)
    http_server.listen(options.port)
    print('Listening on http://localhost:%i' % options.port)
    IOLoop.current().start()

就像我们在 Pyramid 中传递的会话工厂一样,我们可以使用 make_session_factory 来接收数据库 URL 并生成一个对象,这个对象的唯一目的是为视图提供到数据库的连接。然后我们将新创建的 factory 传递给 Application 对象,并使用 session_factory 关键字参数将它绑定到应用程序中。

最后,初始化和管理数据库与 Flask 和 Pyramid 相同(即,单独的 DB 管理脚本,与 Base 对象一起工作等)。它看起来很相似,所以在这里我就不介绍了。

回顾视图

Hello,World 总是适合学习基础知识,但我们需要一些真实的,特定应用程序的视图。

让我们从 info 视图开始。

# views.py
import json
from tornado.web import RequestHandler

class InfoView(RequestHandler):
    """只允许 GET 请求"""
    SUPPORTED_METHODS = ["GET"]

    def set_default_headers(self):
        """设置默认响应头为 json 格式的"""
        self.set_header("Content-Type", 'application/json; charset="utf-8"')

    def get(self):
        """列出这个 API 的路由"""
        routes = {
            'info': 'GET /api/v1',
            'register': 'POST /api/v1/accounts',
            'single profile detail': 'GET /api/v1/accounts/<username>',
            'edit profile': 'PUT /api/v1/accounts/<username>',
            'delete profile': 'DELETE /api/v1/accounts/<username>',
            'login': 'POST /api/v1/accounts/login',
            'logout': 'GET /api/v1/accounts/logout',
            "user's tasks": 'GET /api/v1/accounts/<username>/tasks',
            "create task": 'POST /api/v1/accounts/<username>/tasks',
            "task detail": 'GET /api/v1/accounts/<username>/tasks/<id>',
            "task update": 'PUT /api/v1/accounts/<username>/tasks/<id>',
            "delete task": 'DELETE /api/v1/accounts/<username>/tasks/<id>'
        }
        self.write(json.dumps(routes))

有什么改变吗?让我们从上往下看。

我们添加了 SUPPORTED_METHODS 类属性,它是一个可迭代对象,代表这个视图所接受的请求方法,其他任何方法都将返回一个 405 状态码。当我们创建 HelloWorld 视图时,我们没有指定它,主要是当时有点懒。如果没有这个类属性,此视图将响应任何试图绑定到该视图的路由的请求。

我们声明了 set_default_headers 方法,它设置 HTTP 响应的默认头。我们在这里声明它,以确保我们返回的任何响应都有一个 "Content-Type""application/json" 类型。

我们将 json.dumps(some_object) 添加到 self.write 的参数中,因为它可以很容易地构建响应主体的内容。

现在已经完成了,我们可以继续将它连接到 __init__.py 中的主路由。

# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application
from todo.views import InfoView

# 添加这些
import os
from tornado_sqlalchemy import make_session_factory

define('port', default=8888, help='port to listen on')
factory = make_session_factory(os.environ.get('DATABASE_URL', ''))

def main():
    """Construct and serve the tornado application."""
    app = Application([
        ('/', InfoView)
    ],
        session_factory=factory
    )
    http_server = HTTPServer(app)
    http_server.listen(options.port)
    print('Listening on http://localhost:%i' % options.port)
    IOLoop.current().start()

我们知道,还需要编写更多的视图和路由。每个都会根据需要放入 Application 路由列表中,每个视图还需要一个 set_default_headers 方法。在此基础上,我们还将创建 send_response 方法,它的作用是将响应与我们想要给响应设置的任何自定义状态码打包在一起。由于每个视图都需要这两个方法,因此我们可以创建一个包含它们的基类,这样每个视图都可以继承基类。这样,我们只需要编写一次。

# views.py
import json
from tornado.web import RequestHandler

class BaseView(RequestHandler):
    """Base view for this application."""

    def set_default_headers(self):
        """Set the default response header to be JSON."""
        self.set_header("Content-Type", 'application/json; charset="utf-8"')

    def send_response(self, data, status=200):
        """Construct and send a JSON response with appropriate status code."""
        self.set_status(status)
        self.write(json.dumps(data))

对于我们即将编写的 TaskListView 这样的视图,我们还需要一个到数据库的连接。我们需要 tornado_sqlalchemy 中的 SessionMixin 在每个视图类中添加一个数据库会话。我们可以将它放在 BaseView 中,这样,默认情况下,从它继承的每个视图都可以访问数据库会话。

# views.py
import json
from tornado_sqlalchemy import SessionMixin
from tornado.web import RequestHandler

class BaseView(RequestHandler, SessionMixin):
    """Base view for this application."""

    def set_default_headers(self):
        """Set the default response header to be JSON."""
        self.set_header("Content-Type", 'application/json; charset="utf-8"')

    def send_response(self, data, status=200):
        """Construct and send a JSON response with appropriate status code."""
        self.set_status(status)
        self.write(json.dumps(data))

只要我们修改 BaseView 对象,在将数据发布到这个 API 时,我们就应该定位到这里。

当 Tornado(从 v.4.5 开始)使用来自客户端的数据并将其组织起来到应用程序中使用时,它会将所有传入数据视为字节串。但是,这里的所有代码都假设使用 Python 3,因此我们希望使用的唯一字符串是 Unicode 字符串。我们可以为这个 BaseView 类添加另一个方法,它的工作是将输入数据转换为 Unicode,然后再在视图的其他地方使用。

如果我们想要在正确的视图方法中使用它之前转换这些数据,我们可以重写视图类的原生 prepare 方法。它的工作是在视图方法运行前运行。如果我们重写 prepare 方法,我们可以设置一些逻辑来运行,每当收到请求时,这些逻辑就会执行字节串到 Unicode 的转换。

# views.py
import json
from tornado_sqlalchemy import SessionMixin
from tornado.web import RequestHandler

class BaseView(RequestHandler, SessionMixin):
    """Base view for this application."""

    def prepare(self):
        self.form_data = {
            key: [val.decode('utf8') for val in val_list]
            for key, val_list in self.request.arguments.items()
        }

    def set_default_headers(self):
        """Set the default response header to be JSON."""
        self.set_header("Content-Type", 'application/json; charset="utf-8"')

    def send_response(self, data, status=200):
        """Construct and send a JSON response with appropriate status code."""
        self.set_status(status)
        self.write(json.dumps(data))

如果有任何数据进入,它将在 self.request.arguments 字典中找到。我们可以通过键访问该数据库,并将其内容(始终是列表)转换为 Unicode。因为这是基于类的视图而不是基于函数的,所以我们可以将修改后的数据存储为一个实例属性,以便以后使用。我在这里称它为 form_data,但它也可以被称为 potato。关键是我们可以存储提交给应用程序的数据。

异步视图方法

现在我们已经构建了 BaseaView,我们可以构建 TaskListView 了,它会继承 BaseaView

正如你可以从章节标题中看到的那样,以下是所有关于异步性的讨论。TaskListView 将处理返回任务列表的 GET 请求和用户给定一些表单数据来创建新任务的 POST 请求。让我们首先来看看处理 GET 请求的代码。

# all the previous imports
import datetime
from tornado.gen import coroutine
from tornado_sqlalchemy import as_future
from todo.models import Profile, Task

# the BaseView is above here
class TaskListView(BaseView):
    """View for reading and adding new tasks."""
    SUPPORTED_METHODS = ("GET", "POST",)

    @coroutine
    def get(self, username):
        """Get all tasks for an existing user."""
        with self.make_session() as session:
            profile = yield as_future(session.query(Profile).filter(Profile.username == username).first)
            if profile:
                tasks = [task.to_dict() for task in profile.tasks]
                self.send_response({
                    'username': profile.username,
                    'tasks': tasks
                })

这里的第一个主要部分是 @coroutine 装饰器,它从 tornado.gen 导入。任何具有与调用堆栈的正常流程不同步的 Python 可调用部分实际上是“协程”,即一个可以与其它协程一起运行的协程。在我的家务劳动的例子中,几乎所有的家务活都是一个共同的例行协程。有些阻止了例行协程(例如,给地板吸尘),但这种例行协程只会阻碍我开始或关心其它任何事情的能力。它没有阻止已经启动的任何其他协程继续进行。

Tornado 提供了许多方法来构建一个利用协程的应用程序,包括允许我们设置函数调用锁,同步异步协程的条件,以及手动修改控制 I/O 循环的事件系统。

这里使用 @coroutine 装饰器的唯一条件是允许 get 方法将 SQL 查询作为后台进程,并在查询完成后恢复,同时不阻止 Tornado I/O 循环去处理其他传入的数据源。这就是关于此实现的所有“异步”:带外数据库查询。显然,如果我们想要展示异步 Web 应用程序的魔力和神奇,那么一个任务列表就不是好的展示方式。

但是,这就是我们正在构建的,所以让我们来看看方法如何利用 @coroutine 装饰器。SessionMixin 混合到 BaseView 声明中,为我们的视图类添加了两个方便的,支持数据库的属性:sessionmake_session。它们的名字相似,实现的目标也相当相似。

self.session 属性是一个关注数据库的会话。在请求-响应周期结束时,在视图将响应发送回客户端之前,任何对数据库的更改都被提交,并关闭会话。

self.make_session 是一个上下文管理器和生成器,可以动态构建和返回一个全新的会话对象。第一个 self.session 对象仍然存在。无论如何,反正 make_session 会创建一个新的。make_session 生成器还为其自身提供了一个功能,用于在其上下文(即缩进级别)结束时提交和关闭它创建的会话。

如果你查看源代码,则赋值给 self.session 的对象类型与 self.make_session 生成的对象类型之间没有区别,不同之处在于它们是如何被管理的。

使用 make_session 上下文管理器,生成的会话仅属于上下文,在该上下文中开始和结束。你可以使用 make_session 上下文管理器在同一个视图中打开,修改,提交以及关闭多个数据库会话。

self.session 要简单得多,当你进入视图方法时会话已经打开,在响应被发送回客户端之前会话就已提交。

虽然读取文档片段PyPI 示例都说明了上下文管理器的使用,但是没有说明 self.session 对象或由 self.make_session 生成的 session 本质上是不是异步的。当我们启动查询时,我们开始考虑内置于 tornado-sqlalchemy 中的异步行为。

tornado-sqlalchemy 包为我们提供了 as_future 函数。它的工作是装饰 tornado-sqlalchemy 会话构造的查询并 yield 其返回值。如果视图方法用 @coroutine 装饰,那么使用 yield as_future(query) 模式将使封装的查询成为一个异步后台进程。I/O 循环会接管等待查询的返回值和 as_future 创建的 future 对象的解析。

要访问 as_future(query) 的结果,你必须从它 yield。否则,你只能获得一个未解析的生成器对象,并且无法对查询执行任何操作。

这个视图方法中的其他所有内容都与之前课堂上的类似,与我们在 Flask 和 Pyramid 中看到的内容类似。

post 方法看起来非常相似。为了保持一致性,让我们看一下 post 方法以及它如何处理用 BaseView 构造的 self.form_data

@coroutine
def post(self, username):
    """Create a new task."""
    with self.make_session() as session:
        profile = yield as_future(session.query(Profile).filter(Profile.username == username).first)
        if profile:
            due_date = self.form_data['due_date'][0]
            task = Task(
                name=self.form_data['name'][0],
                note=self.form_data['note'][0],
                creation_date=datetime.now(),
                due_date=datetime.strptime(due_date, '%d/%m/%Y %H:%M:%S') if due_date else None,
                completed=self.form_data['completed'][0],
                profile_id=profile.id,
                profile=profile
            )
            session.add(task)
            self.send_response({'msg': 'posted'}, status=201)

正如我所说,这是我们所期望的:

* 与我们在 get 方法中看到的查询模式相同 * 构造一个新的 Task 对象的实例,用 form_data 的数据填充 * 添加新的 Task 对象(但不提交,因为它由上下文管理器处理!)到数据库会话 * 将响应发送给客户端

这样我们就有了 Tornado web 应用程序的基础。其他内容(例如,数据库管理和更多完整应用程序的视图)实际上与我们在 Flask 和 Pyramid 应用程序中看到的相同。

关于使用合适的工具完成合适的工作的一点想法

在我们继续浏览这些 Web 框架时,我们开始看到它们都可以有效地处理相同的问题。对于像这样的待办事项列表,任何框架都可以完成这项任务。但是,有些 Web 框架比其它框架更适合某些工作,这具体取决于对你来说什么“更合适”和你的需求。

虽然 Tornado 显然和 Pyramid 或 Flask 一样可以处理相同工作,但将它用于这样的应用程序实际上是一种浪费,这就像开车从家走一个街区(LCTT 译注:这里意思应该是从家开始走一个街区只需步行即可)。是的,它可以完成“旅行”的工作,但短途旅行不是你选择汽车而不是自行车或者使用双脚的原因。

根据文档,Tornado 被称为 “Python Web 框架和异步网络库”。在 Python Web 框架生态系统中很少有人喜欢它。如果你尝试完成的工作需要(或将从中获益)以任何方式、形状或形式的异步性,使用 Tornado。如果你的应用程序需要处理多个长期连接,同时又不想牺牲太多性能,选择 Tornado。如果你的应用程序是多个应用程序,并且需要线程感知以准确处理数据,使用 Tornado。这是它最有效的地方。

用你的汽车做“汽车的事情”,使用其他交通工具做其他事情。

向前看,进行一些深度检查

谈到使用合适的工具来完成合适的工作,在选择框架时,请记住应用程序的范围和规模,包括现在和未来。到目前为止,我们只研究了适用于中小型 Web 应用程序的框架。本系列的下一篇也是最后一篇将介绍最受欢迎的 Python 框架之一 Django,它适用于可能会变得更大的大型应用程序。同样,尽管它在技术上能够并且将会处理待办事项列表问题,但请记住,这不是它的真正用途。我们仍然会通过它来展示如何使用它来构建应用程序,但我们必须牢记框架的意图以及它是如何反映在架构中的:

  • Flask: 适用于小型,简单的项目。它可以使我们轻松地构建视图并将它们快速连接到路由,它可以简单地封装在一个文件中。
  • Pyramid: 适用于可能增长的项目。它包含一些配置来启动和运行。应用程序组件的独立领域可以很容易地划分并构建到任意深度,而不会忽略中央应用程序。
  • Tornado: 适用于受益于精确和有意识的 I/O 控制的项目。它允许协程,并轻松公开可以控制如何接收请求或发送响应以及何时发生这些操作的方法。
  • Django:(我们将会看到)意味着可能会变得更大的东西。它有着非常庞大的生态系统,包括大量插件和模块。它非常有主见的配置和管理,以保持所有不同部分在同一条线上。

无论你是从本系列的第一篇文章开始阅读,还是稍后才加入的,都要感谢阅读!请随意留下问题或意见。下次再见时,我手里会拿着 Django。

感谢 Python BDFL

我必须把功劳归于它应得的地方,非常感谢 Guido van Rossum,不仅仅是因为他创造了我最喜欢的编程语言。

PyCascades 2018 期间,我很幸运的不仅做了基于这个文章系列的演讲,而且还被邀请参加了演讲者的晚宴。整个晚上我都坐在 Guido 旁边,不停地问他问题。其中一个问题是,在 Python 中异步到底是如何工作的,但他没有一点大惊小怪,而是花时间向我解释,让我开始理解这个概念。他后来推特给我发了一条消息:是用于学习异步 Python 的广阔资源。我随后在三个月内阅读了三次,然后写了这篇文章。你真是一个非常棒的人,Guido!


via: https://opensource.com/article/18/6/tornado-framework

作者:Nicholas Hunt-Walker 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

(以下内容是我的硕士论文的摘录,几乎是整个 2.1 章节,向具有 CS 背景的人快速介绍 Go)

Go 是一门用于并发编程的命令式编程语言,它主要由创造者 Google 进行开发,最初主要由 Robert Griesemer、Rob Pike 和 Ken Thompson 开发。这门语言的设计起始于 2007 年,并在 2009 年推出最初版本;而第一个稳定版本是 2012 年发布的 1.0 版本。 1

Go 有 C 风格的语法(没有预处理器)、垃圾回收机制,而且类似它在贝尔实验室里被开发出来的前辈们:Newsqueak(Rob Pike)、Alef(Phil Winterbottom)和 Inferno(Pike、Ritchie 等人),使用所谓的 Go 协程 goroutines 信道 channels (一种基于 Hoare 的“通信顺序进程”理论的协程)提供内建的并发支持。 2

Go 程序以包的形式组织。包本质是一个包含 Go 文件的文件夹。包内的所有文件共享相同的命名空间,而包内的符号有两种可见性:以大写字母开头的符号对于其他包是可见,而其他符号则是该包私有的:

func PublicFunction() {
    fmt.Println("Hello world")
}

func privateFunction() {
    fmt.Println("Hello package")
}

类型

Go 有一个相当简单的类型系统:没有子类型(但有类型转换),没有泛型,没有多态函数,只有一些基本的类型:

  1. 基本类型:intint64int8uintfloat32float64
  2. struct
  3. interface:一组方法的集合
  4. map[K, V]:一个从键类型到值类型的映射
  5. [number]Type:一些 Type 类型的元素组成的数组
  6. []Type:某种类型的切片(具有长度和功能的数组的指针)
  7. chan Type:一个线程安全的队列
  8. 指针 *T 指向其他类型
  9. 函数
  10. 具名类型:可能具有关联方法的其他类型的别名(LCTT 译注:这里的别名并非指 Go 1.9 中的新特性“类型别名”):
  type T struct { foo int }
  type T *T
  type T OtherNamedType

具名类型完全不同于它们的底层类型,所以你不能让它们互相赋值,但一些操作符,例如 +,能够处理同一底层数值类型的具名类型对象们(所以你可以在上面的示例中把两个 T 加起来)。

映射、切片和信道是类似于引用的类型——它们实际上是包含指针的结构。包括数组(具有固定长度并可被拷贝)在内的其他类型则是值传递(拷贝)。

类型转换

类型转换类似于 C 或其他语言中的类型转换。它们写成这样子:

TypeName(value)

常量

Go 有“无类型”字面量和常量。

1 // 无类型整数字面量
const foo = 1 // 无类型整数常量
const foo int = 1 // int 类型常量

无类型值可以分为以下几类:UntypedBoolUntypedIntUntypedRuneUntypedFloatUntypedComplexUntypedString 以及 UntypedNil(Go 称它们为基础类型,其他基础种类可用于具体类型,如 uint8)。一个无类型值可以赋值给一个从基础类型中派生的具名类型;例如:

type someType int

const untyped = 2 // UntypedInt
const bar someType = untyped // OK: untyped 可以被赋值给 someType
const typed int = 2 // int
const bar2 someType = typed // error: int 不能被赋值给 someType

接口和对象

正如上面所说的,接口是一组方法的集合。Go 本身不是一种面向对象的语言,但它支持将方法关联到具名类型上:当声明一个函数时,可以提供一个接收者。接收者是函数的一个额外参数,可以在函数之前传递并参与函数查找,就像这样:

type SomeType struct { ... }
type SomeType struct { ... }

func (s *SomeType) MyMethod() {
}

func main() {
    var s SomeType
    s.MyMethod()
}

如果对象实现了所有方法,那么它就实现了接口;例如,*SomeType(注意指针)实现了下面的接口 MyMethoder,因此 *SomeType 类型的值就能作为 MyMethoder 类型的值使用。最基本的接口类型是 interface{},它是一个带空方法集的接口 —— 任何对象都满足该接口。

type MyMethoder interface {
    MyMethod()
}

合法的接收者类型是有些限制的;例如,具名类型可以是指针类型(例如,type MyIntPointer *int),但这种类型不是合法的接收者类型。

控制流

Go 提供了三个主要的控制了语句:ifswitchfor。这些语句同其他 C 风格语言内的语句非常类似,但有一些不同:

  • 条件语句没有括号,所以条件语句是 if a == b {} 而不是 if (a == b) {}。大括号是必须的。
  • 所有的语句都可以有初始化,比如这个 if result, err := someFunction(); err == nil { // use result }
  • switch 语句在分支里可以使用任何表达式
  • switch 语句可以处理空的表达式(等于 true
  • 默认情况下,Go 不会从一个分支进入下一个分支(不需要 break 语句),在程序块的末尾使用 fallthrough 则会进入下一个分支。
  • 循环语句 for 不仅能循环值域:for key, val := range map { do something }

Go 协程

关键词 go 会产生一个新的 Go 协程 goroutine ,这是一个可以并行执行的函数。它可以用于任何函数调用,甚至一个匿名函数:

func main() {
    ...
    go func() {
        ...
    }()

    go some_function(some_argument)
}

信道

Go 协程通常和信道channels结合,用来提供一种通信顺序进程的扩展。信道是一个并发安全的队列,而且可以选择是否缓冲数据:

var unbuffered = make(chan int) // 直到数据被读取时完成数据块发送
var buffered = make(chan int, 5) // 最多有 5 个未读取的数据块

运算符 <- 用于和单个信道进行通信。

valueReadFromChannel := <- channel
otherChannel <- valueToSend

语句 select 允许多个信道进行通信:

select {
    case incoming := <- inboundChannel:
    // 一条新消息
    case outgoingChannel <- outgoing:
    // 可以发送消息
}

defer 声明

Go 提供语句 defer 允许函数退出时调用执行预定的函数。它可以用于进行资源释放操作,例如:

func myFunc(someFile io.ReadCloser) {
    defer someFile.close()
    /* 文件相关操作 */
}

当然,它允许使用匿名函数作为被调函数,而且编写被调函数时可以像平常一样使用任何变量。

错误处理

Go 没有提供异常类或者结构化的错误处理。然而,它通过第二个及后续的返回值来返回错误从而处理错误:

func Read(p []byte) (n int, err error)

// 内建类型:
type error interface {
    Error() string
}

必须在代码中检查错误或者赋值给 _

n0, _ := Read(Buffer) // 忽略错误
n, err := Read(buffer)
if err != nil {
    return err
}

有两个函数可以快速跳出和恢复调用栈:panic()recover()。当 panic() 被调用时,调用栈开始弹出,同时每个 defer 函数都会正常运行。当一个 defer 函数调用 recover()时,调用栈停止弹出,同时返回函数 panic() 给出的值。如果我们让调用栈正常弹出而不是由于调用 panic() 函数,recover() 将只返回 nil。在下面的例子中,defer 函数将捕获 panic() 抛出的任何 error 类型的值并储存在错误返回值中。第三方库中有时会使用这个方法增强递归代码的可读性,如解析器,同时保持公有函数仍使用普通错误返回值。

func Function() (err error) {
    defer func() {
        s := recover()
        switch s := s.(type) {  // type switch
            case error:
                err = s         // s has type error now
            default:
                panic(s)
        }
    }
}

数组和切片

正如前边说的,数组是值类型,而切片是指向数组的指针。切片可以由现有的数组切片产生,也可以使用 make() 创建切片,这会创建一个匿名数组以保存元素。

slice1 := make([]int, 2, 5) // 分配 5 个元素,其中 2 个初始化为0
slice2 := array[:] // 整个数组的切片
slice3 := array[1:] // 除了首元素的切片

除了上述例子,还有更多可行的切片运算组合,但需要明了直观。

使用 append() 函数,切片可以作为一个变长数组使用。

slice = append(slice, value1, value2)
slice = append(slice, arrayOrSlice...)

切片也可以用于函数的变长参数。

映射

映射 maps 是简单的键值对储存容器,并支持索引和分配。但它们不是线程安全的。

someValue := someMap[someKey]
someValue, ok := someMap[someKey] // 如果键值不在 someMap 中,变量 ok 会赋值为 `false`
someMap[someKey] = someValue

via: https://blog.jak-linux.org/2018/12/24/introduction-to-go/

作者:Julian Andres Klode 选题:lujun9972 译者:LazyWolfLin 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出


  1. Frequently Asked Questions (FAQ) - The Go Programming Language https://golang.org/doc/faq#history [return]
  2. HOARE, Charles Antony Richard. Communicating sequential processes. Communications of the ACM, 1978, 21. Jg., Nr. 8, S. 666-677. [return]

这个众所周知的话题我们早已经写过了足够多的文章。即使这样,我们今天也要去讨论相同的话题。

其他的工具都是在本地运行的,但是 Asciinema 可以以相同的方式在本地和 Web 端运行。我的意思是我们可以在 Web 上分享这个录像。

默认情况下,每个人都更愿意使用 history 命令来回看、调用之前在终端内输入的命令。不过,不行的是,这个命令只展示了我们运行的命令却没有展示这些命令上次运行时的输出。

在 Linux 下有很多的组件来记录终端会话活动。在过去,我们也写了一些组件,不过今天我们依然要讨论这同一类心的工具。

如果你想要使用其他工具来记录你的 Linux 终端会话活动,你可以试试 Script 命令Terminalizer 工具Asciinema 工具

不过如果你想要找一个 GIF 录制工具,可以试试 GifineKgifPeek

什么是 Asciinema

asciinema 是一个自由开源的用于录制终端会话并将它们分享到网络上的解决方案。

当你在你的终端内运行 asciinema rec 来启动录像时,你输入命令的时候,终端内的所有输出都会被抓取。

当抓取停止时(通过按下 Ctrl-D 或输出 exit),抓取的输出将会被上传到 asciinema.org 的网站,并为后续的回放做准备。

Asciinema 项目由多个不同的完整的部分组成,比如 asciinema 命令行工具、asciinema.org API 和 JavaScript 播放器。

Asciinema 的灵感来自于 scriptscriptreplay 命令。

如何在 Linux 上安装 Asciinema

Asciinema 由 Python 写就,在 Linux 上,推荐使用 pip 安装的方法来安装。

确保你已经在你的系统里安装了 python-pip 包。如果没有,使用下述命令来安装它。

对于 Debian/Ubuntu 用户,使用 Apt 命令Apt-Get 命令 来安装 pip 包。

$ sudo apt install python-pip

对于 Archlinux 用户,使用 Pacman 命令 来安装 pip 包。

$ sudo pacman -S python-pip

对于 Fedora 用户,使用 DNF 命令 来安装 pip 包。

$ sudo dnf install python-pip

对于 CentOS/RHEL 用户,使用 YUM 命令 来安装 pip 包。

$ sudo yum install python-pip

对于 openSUSE 用户,使用 Zypper 命令 来安装 pip 包。

$ sudo zypper install python-pip

最后,运行如下的 pip 命令 来在 Linux 上安装 Asciinema 工具。

$ sudo pip3 install asciinema

如何使用 Asciinema 工具来记录你的终端会话

一旦你成功的安装了 Asciinema,只需要运行如下命令来开始录制:

$ asciinema rec 2g-test
asciinema: recording asciicast to 2g-test
asciinema: press "ctrl-d" or type "exit" when you're done

出于测试的目的,运行一些简单的命令,并看一看它是否运行良好。

$ free
              total        used        free      shared  buff/cache   available
Mem:          15867        2783       10537        1264        2546       11510
Swap:         17454           0       17454

$ hostnamectl
   Static hostname: daygeek-Y700
         Icon name: computer-laptop
           Chassis: laptop
        Machine ID: 31bdeb7b833547368d230a2025d475bc
           Boot ID: c84f7e6f39394d1f8fdc4bcaa251aee2
  Operating System: Manjaro Linux
            Kernel: Linux 4.19.8-2-MANJARO
      Architecture: x86-64

$ uname -a
Linux daygeek-Y700 4.19.8-2-MANJARO #1 SMP PREEMPT Sat Dec 8 14:45:36 UTC 2018 x86_64 GNU/Linux

$ lscpu
Architecture:        x86_64
CPU op-mode(s):      32-bit, 64-bit
Byte Order:          Little Endian
Address sizes:       39 bits physical, 48 bits virtual
CPU(s):              8
On-line CPU(s) list: 0-7
Thread(s) per core:  2
Core(s) per socket:  4
Socket(s):           1
NUMA node(s):        1
Vendor ID:           GenuineIntel
CPU family:          6
Model:               94
Model name:          Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
Stepping:            3
CPU MHz:             800.047
CPU max MHz:         3500.0000
CPU min MHz:         800.0000
BogoMIPS:            5186.00
Virtualization:      VT-x
L1d cache:           32K
L1i cache:           32K
L2 cache:            256K
L3 cache:            6144K
NUMA node0 CPU(s):   0-7
Flags:               fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid ept_add fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d

当你完成后,简单的按下 CTRL+D 或输入 exit 来退出录制。这个结果将会被保存在同一个目录。

$ exit
exit
asciinema: recording finished
asciinema: asciicast saved to 2g-test

如果你想要保存输出到不同的目录中,就需要提醒 Asciinema 你想要保存文件的目录。

$ asciinema rec /opt/session-record/2g-test1

我们可以使用如下命令来回放录制的会话。

$ asciinema play 2g-test

我们能够以两倍速来回放录制的会话。

$ asciinema play -s 2 2g-test

或者,我们可以以正常速度播放录制的会话,限制空闲时间为 2 秒。

$ asciinema play -i 2 2g-test

如何在网络上分享已经录制的会话

如果你想要分享录制的会话给你的朋友,只要运行下述命令上传你的会话到 asciinema.org,就可以获得一个唯一链接。

它将会在被上传 7 天后被归档。

$ asciinema upload 2g-test
View the recording at:

    https://asciinema.org/a/jdJrxhDLboeyrhzZRHsve0x8i

This installation of asciinema recorder hasn't been linked to any asciinema.org
account. All unclaimed recordings (from unknown installations like this one)
are automatically archived 7 days after upload.

If you want to preserve all recordings made on this machine, connect this
installation with asciinema.org account by opening the following link:

    https://asciinema.org/connect/10cd4f24-45b6-4f64-b737-ae0e5d12baf8

如果你想要分享录制的会话在社交媒体上,只需要点击页面底部的 “Share” 按钮。

如果任何人想要去下载这个录制,只需要点击页面底部的 “Download” 按钮,就可以将其保存在你系统里。

如何管理 asciinema.org 中的录制片段

如果你想要留存所有在这个机器上录制的片段,点击上述显示的链接并使用你在 asciinema.org 的账户登录,然后跟随这个说明继续操作,来将你的机器和该网站连接起来。

https://asciinema.org/connect/10cd4f24-45b6-4f64-b737-ae0e5d12baf8

如果你早已录制了一份,但是你没有在你的 asciinema.org 账户界面看到它,只需要运行 asciinema auth 命令来移动它们。

$ asciinema auth

Open the following URL in a web browser to link your install ID with your asciinema.org user account:

https://asciinema.org/connect/10cd4f24-45b6-4f64-b737-ae0e5d12baf8

This will associate all recordings uploaded from this machine (past and future ones) to your account, and allow you to manage them (change title/theme, delete) at asciinema.org.

如果你想直接上传文件而不是将其保存在本地,直接运行如下命令:

$ asciinema rec
asciinema: recording asciicast to /tmp/tmp6kuh4247-ascii.cast
asciinema: press "ctrl-d" or type "exit" when you're done

出于测试目的,运行下述命令,并看一看它是否运行的很好。

$ free
              total        used        free      shared  buff/cache   available
Mem:          15867        2783       10537        1264        2546       11510
Swap:         17454           0       17454

$ hostnamectl
   Static hostname: daygeek-Y700
         Icon name: computer-laptop
           Chassis: laptop
        Machine ID: 31bdeb7b833547368d230a2025d475bc
           Boot ID: c84f7e6f39394d1f8fdc4bcaa251aee2
  Operating System: Manjaro Linux
            Kernel: Linux 4.19.8-2-MANJARO
      Architecture: x86-64

$ uname -a
Linux daygeek-Y700 4.19.8-2-MANJARO #1 SMP PREEMPT Sat Dec 8 14:45:36 UTC 2018 x86_64 GNU/Linux

如果你完成了,简单的按下 CTRL+D 或输入 exit 来停止录制,然后按下回车来上传文件到 asciinema.org 网站。

这将会花费一些时间来为你的录制生成唯一链接。一旦它完成,你会看到和下面一样的样式:

$ exit
exit
asciinema: recording finished
asciinema: press "enter" to upload to asciinema.org, "ctrl-c" to save locally

View the recording at:

 https://asciinema.org/a/b7bu5OhuCy2vUH7M8RRPjsSxg

via: https://www.2daygeek.com/linux-asciinema-record-your-terminal-sessions-share-them-on-web/

作者:Magesh Maruthamuthu 选题:lujun9972 译者:Bestony 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

OK03 课程基于 OK02 课程来构建,它教你在汇编中如何使用函数让代码可复用和可读性更好。假设你已经有了 课程 2:OK02 的操作系统,我们将以它为基础。

1、可复用的代码

到目前为止,我们所写的代码都是以我们希望发生的事为顺序来输入的。对于非常小的程序来说,这种做法很好,但是如果我们以这种方式去写一个完整的系统,所写的代码可读性将非常差。我们应该去使用函数。

一个函数是一段可复用的代码片断,可以用于去计算某些答案,或执行某些动作。你也可以称它们为 过程 procedure 例程 routine 子例程 subroutine 。虽然它们都是不同的,但人们几乎都没有正确地使用这个术语。

你应该在数学上遇到了函数的概念。例如,余弦函数应用于一个给定的数时,会得到介于 -1 到 1 之间的另一个数,这个数就是角的余弦。一般我们写成 cos(x) 来表示应用到一个值 x 上的余弦函数。

在代码中,函数可以有多个输入(也可以没有输入),然后函数给出多个输出(也可以没有输出),并可能导致副作用。例如一个函数可以在一个文件系统上创建一个文件,第一个输入是它的名字,第二个输入是文件的长度。

Function as black boxes

函数可以认为是一个“黑匣子”。我们给它输入,然后它给我们输出,而我们不需要知道它是如何工作的。

在像 C 或 C++ 这样的高级代码中,函数是语言的组成部分。在汇编代码中,函数只是我们的创意。

理想情况下,我们希望能够在我们的寄存器中设置一些输入值,然后分支切换到某个地址,然后预期在某个时刻分支返回到我们代码,并通过代码来设置输出值到寄存器。这就是我们所设想的汇编代码中的函数。困难之处在于我们用什么样的方式去设置寄存器。如果我们只是使用平时所接触到的某种方法去设置寄存器,每个程序员可能使用不同的方法,这样你将会发现你很难理解其他程序员所写的代码。另外,编译器也不能像使用汇编代码那样轻松地工作,因为它们压根不知道如何去使用函数。为避免这种困惑,为每个汇编语言设计了一个称为 应用程序二进制接口 Application Binary Interface (ABI)的标准,由它来规范函数如何去运行。如果每个人都使用相同的方法去写函数,这样每个人都可以去使用其他人写的函数。在这里,我将教你们这个标准,而从现在开始,我所写的函数将全部遵循这个标准。

该标准规定,寄存器 r0r1r2r3 将被依次用于函数的输入。如果函数没有输入,那么它不会在意值是什么。如果只需要一个输入,那么它应该总是在寄存器 r0 中,如果它需要两个输入,那么第一个输入在寄存器 r0 中,而第二个输入在寄存器 r1 中,依此类推。输出值也总是在寄存器 r0 中。如果函数没有输出,那么 r0 中是什么值就不重要了。

另外,该标准要求当一个函数运行之后,寄存器 r4r12 的值必须与函数启动时的值相同。这意味着当你调用一个函数时,你可以确保寄存器 r4r12 中的值没有发生变化,但是不能确保寄存器 r0r3 中的值也没有发生变化。

当一个函数运行完成后,它将返回到启动它的代码分支处。这意味着它必须知道启动它的代码的地址。为此,需要一个称为 lr(链接寄存器)的专用寄存器,它总是在保存调用这个函数的指令后面指令的地址。

表 1.1 ARM ABI 寄存器用法

寄存器简介保留规则
r0参数和结果r0r1 用于给函数传递前两个参数,以及函数返回的结果。如果函数返回值不使用它,那么在函数运行之后,它们可以携带任何值。
r1参数和结果
r2参数r2r3 用去给函数传递后两个参数。在函数运行之后,它们可以携带任何值。
r3参数
r4通用寄存器r4r12 用于保存函数运行过程中的值,它们的值在函数调用之后必须与调用之前相同。
r5通用寄存器
r6通用寄存器
r7通用寄存器
r8通用寄存器
r9通用寄存器
r10通用寄存器
r11通用寄存器
r12通用寄存器
lr返回地址当函数运行完成后,lr 中保存了分支的返回地址,但在函数运行完成之后,它将保存相同的地址。
sp栈指针sp 是栈指针,在下面有详细描述。它的值在函数运行完成后,必须是相同的。

通常,函数需要使用很多的寄存器,而不仅是 r0r3。但是,由于 r4r12 必须在函数完成之后值必须保持相同,因此它们需要被保存到某个地方。我们将它们保存到称为栈的地方。

Stack diagram

一个 stack 就是我们在计算中用来保存值的一个很形象的方法。就像是摞起来的一堆盘子,你可以从上到下来移除它们,而添加它们时,你只能从下到上来添加。

在函数运行时,使用栈来保存寄存器值是个非常好的创意。例如,如果我有一个函数需要去使用寄存器 r4r5,它将在一个栈上存放这些寄存器的值。最后用这种方式,它可以再次将它拿回来。更高明的是,如果为了运行完我的函数,需要去运行另一个函数,并且那个函数需要保存一些寄存器,在那个函数运行时,它将把寄存器保存在栈顶上,然后在结束后再将它们拿走。而这并不会影响我保存在寄存器 r4r5 中的值,因为它们是在栈顶上添加的,拿走时也是从栈顶上取出的。

用来表示使用特定的方法将值放到栈上的专用术语,我们称之为那个方法的“ 栈帧 stack frame ”。不是每种方法都使用一个栈帧,有些是不需要存储值的。

因为栈非常有用,它被直接实现在 ARMv6 的指令集中。一个名为 sp(栈指针)的专用寄存器用来保存栈的地址。当需要有值添加到栈上时,sp 寄存器被更新,这样就总是保证它保存的是栈上第一个值的地址。push {r4,r5} 将推送 r4r5 中的值到栈顶上,而 pop {r4,r5} 将(以正确的次序)取回它们。

2、我们的第一个函数

现在,关于函数的原理我们已经有了一些概念,我们尝试来写一个函数。由于是我们的第一个很基础的例子,我们写一个没有输入的函数,它将输出 GPIO 的地址。在上一节课程中,我们就是写到这个值上,但将它写成函数更好,因为我们在真实的操作系统中经常需要用到它,而我们不可能总是能够记住这个地址。

复制下列代码到一个名为 gpio.s 的新文件中。就像在 source 目录中使用的 main.s 一样。我们将把与 GPIO 控制器相关的所有函数放到一个文件中,这样更好查找。

.globl GetGpioAddress
GetGpioAddress:
ldr r0,=0x20200000
mov pc,lr

.globl lbl 使标签 lbl 从其它文件中可访问。

mov reg1,reg2 复制 reg2 中的值到 reg1 中。

这就是一个很简单的完整的函数。.globl GetGpioAddress 命令是通知汇编器,让标签 GetGpioAddress 在所有文件中全局可访问。这意味着在我们的 main.s 文件中,我们可以使用分支指令到标签 GetGpioAddress 上,即便这个标签在那个文件中没有定义也没有问题。

你应该认得 ldr r0,=0x20200000 命令,它将 GPIO 控制器地址保存到 r0 中。由于这是一个函数,我们必须要让它输出到寄存器 r0 中,我们不能再像以前那样随意使用任意一个寄存器了。

mov pc,lr 将寄存器 lr 中的值复制到 pc 中。正如前面所提到的,寄存器 lr 总是保存着方法完成后我们要返回的代码的地址。pc 是一个专用寄存器,它总是包含下一个要运行的指令的地址。一个普通的分支命令只需要改变这个寄存器的值即可。通过将 lr 中的值复制到 pc 中,我们就可以将要运行的下一行命令改变成我们将要返回的那一行。

理所当然这里有一个问题,那就是我们如何去运行这个代码?我们将需要一个特殊的分支类型 bl 指令。它像一个普通的分支一样切换到一个标签,但它在切换之前先更新 lr 的值去包含一个在该分支之后的行的地址。这意味着当函数执行完成后,将返回到 bl 指令之后的那一行上。这就确保了函数能够像任何其它命令那样运行,它简单地运行,做任何需要做的事情,然后推进到下一行。这是理解函数最有用的方法。当我们使用它时,就将它们按“黑匣子”处理即可,不需要了解它是如何运行的,我们只了解它需要什么输入,以及它给我们什么输出即可。

到现在为止,我们已经明白了函数如何使用,下一节我们将使用它。

3、一个大的函数

现在,我们继续去实现一个更大的函数。我们的第一项任务是启用 GPIO 第 16 号针脚的输出。如果它是一个函数那就太好了。我们能够简单地指定一个针脚号和一个函数作为输入,然后函数将设置那个针脚的值。那样,我们就可以使用这个代码去控制任意的 GPIO 针脚,而不只是 LED 了。

将下列的命令复制到 gpio.s 文件中的 GetGpioAddress 函数中。

.globl SetGpioFunction
SetGpioFunction:
cmp r0,#53
cmpls r1,#7
movhi pc,lr

带后缀 ls 的命令只有在上一个比较命令的结果是第一个数字小于或与第二个数字相同的情况下才会被运行。它是无符号的。

带后缀 hi 的命令只有上一个比较命令的结果是第一个数字大于第二个数字的情况下才会被运行。它是无符号的。

在写一个函数时,我们首先要考虑的事情就是输入,如果输入错了我们怎么办?在这个函数中,我们有一个输入是 GPIO 针脚号,而它必须是介于 0 到 53 之间的数字,因为只有 54 个针脚。每个针脚有 8 个函数,被编号为 0 到 7,因此函数编号也必须是 0 到 7 之间的数字。我们可以假设输入应该是正确的,但是当在硬件上使用时,这种做法是非常危险的,因为不正确的值将导致非常糟糕的副作用。所以,在这个案例中,我们希望确保输入值在正确的范围。

为了确保输入值在正确的范围,我们需要做一个检查,即 r0 <= 53 并且 r1 <= 7。首先我们使用前面看到的比较命令去将 r0 的值与 53 做比较。下一个指令 cmpls 仅在前一个比较指令结果是小于或与 53 相同时才会去运行。如果是这种情况,它将寄存器 r1 的值与 7 进行比较,其它的部分都和前面的是一样的。如果最后的比较结果是寄存器值大于那个数字,最后我们将返回到运行函数的代码处。

这正是我们所希望的效果。如果 r0 中的值大于 53,那么 cmpls 命令将不会去运行,但是 movhi 会运行。如果 r0 中的值 <= 53,那么 cmpls 命令会运行,它会将 r1 中的值与 7 进行比较,如果 r1 > 7,movhi 会运行,函数结束,否则 movhi 不会运行,这样我们就确定 r0 <= 53 并且 r1 <= 7。

ls(低于或相同)与 le(小于或等于)有一些细微的差别,以及后缀 hi(高于)和 gt(大于)也一样有一些细微差别,我们在后面将会讲到。

将这些命令复制到上面的代码的下面位置。

push {lr}
mov r2,r0
bl GetGpioAddress

push {reg1,reg2,...} 复制列出的寄存器 reg1reg2、… 到栈顶。该命令仅能用于通用寄存器和 lr 寄存器。

bl lbl 设置 lr 为下一个指令的地址并切换到标签 lbl

这三个命令用于调用我们第一个方法。push {lr} 命令复制 lr 中的值到栈顶,这样我们在后面可以获取到它。当我们调用 GetGpioAddress 时必须要这样做,我们将需要使用 lr 去保存我们函数要返回的地址。

如果我们对 GetGpioAddress 函数一无所知,我们必须假设它改变了 r0r1r2r3 的值 ,并移动我们的值到 r4r5 中,以在函数完成之后保持它们的值一样。幸运的是,我们知道 GetGpioAddress 做了什么,并且我们也知道它仅改变了 r0 为 GPIO 地址,它并没有影响 r1r2r3 的值。因此,我们仅去将 GPIO 针脚号从 r0 中移出,这样它就不会被覆盖掉,但我们知道,可以将它安全地移到 r2 中,因为 GetGpioAddress 并不去改变 r2

最后我们使用 bl 指令去运行 GetGpioAddress。通常,运行一个函数,我们使用一个术语叫“调用”,从现在开始我们将一直使用这个术语。正如我们前面讨论过的,bl 调用一个函数是通过更新 lr 为下一个指令的地址并切换到该函数完成的。

当一个函数结束时,我们称为“返回”。当一个 GetGpioAddress 调用返回时,我们已经知道了 r0 中包含了 GPIO 的地址,r1 中包含了函数编号,而 r2 中包含了 GPIO 针脚号。

我前面说过,GPIO 函数每 10 个保存在一个块中,因此首先我们需要去判断我们的针脚在哪个块中。这似乎听起来像是要使用一个除法,但是除法做起来非常慢,因此对于这些比较小的数来说,不停地做减法要比除法更好。

将下面的代码复制到上面的代码中最下面的位置。

functionLoop$:

cmp r2,#9
subhi r2,#10
addhi r0,#4
bhi functionLoop$
add reg,#val 将数字 val 加到寄存器 reg 的内容上。

这个简单的循环代码将针脚号(r2)与 9 进行比较。如果它大于 9,它将从针脚号上减去 10,并且将 GPIO 控制器地址加上 4,然后再次运行检查。

这样做的效果就是,现在,r2 中将包含一个 0 到 9 之间的数字,它是针脚号除以 10 的余数。r0 将包含这个针脚的函数所设置的 GPIO 控制器的地址。它就如同是 “GPIO 控制器地址 + 4 × (GPIO 针脚号 ÷ 10)”。

最后,将下面的代码复制到上面的代码中最下面的位置。

add r2, r2,lsl #1
lsl r1,r2
str r1,[r0]
pop {pc}

移位参数 reg,lsl #val 表示将寄存器 reg 中二进制表示的数逻辑左移 val 位之后的结果作为与前面运算的操作数。

lsl reg,amt 将寄存器 reg 中的二进制数逻辑左移 amt 中的位数。

str reg,[dst]str reg,[dst,#0] 相同。

pop {reg1,reg2,...} 从栈顶复制值到寄存器列表 reg1reg2、… 仅有通用寄存器与 pc 可以这样弹出值。

这个代码完成了这个方法。第一行其实是乘以 3 的变体。乘法在汇编中是一个大而慢的指令,因为电路需要很长时间才能给出答案。有时使用一些能够很快给出答案的指令会让它变得更快。在本案例中,我们知道 r2 × 3 与 r2 × 2 + r2 是相同的。一个寄存器乘以 2 是非常容易的,因为它可以通过将二进制表示的数左移一位来很方便地实现。

ARMv6 汇编语言其中一个非常有用的特性就是,在使用它之前可以先移动参数所表示的位数。在本案例中,我将 r2 加上 r2 中二进制表示的数左移一位的结果。在汇编代码中,你可以经常使用这个技巧去更快更容易地计算出答案,但如果你觉得这个技巧使用起来不方便,你也可以写成类似 mov r3,r2add r2,r3add r2,r3 这样的代码。

现在,我们可以将一个函数的值左移 r2 中所表示的位数。大多数对数量的指令(比如 addsub)都有一个可以使用寄存器而不是数字的变体。我们执行这个移位是因为我们想去设置表示针脚号的位,并且每个针脚有三个位。

然后,我们将函数计算后的值保存到 GPIO 控制器的地址上。我们在循环中已经算出了那个地址,因此我们不需要像 OK01 和 OK02 中那样在一个偏移量上保存它。

最后,我们从这个方法调用中返回。由于我们将 lr 推送到了栈上,因此我们 pop pc,它将复制 lr 中的值并将它推送到 pc 中。这个操作类似于 mov pc,lr,因此函数调用将返回到运行它的那一行上。

敏锐的人可能会注意到,这个函数其实并不能正确工作。虽然它将 GPIO 针脚函数设置为所要求的值,但它会导致在同一个块中的所有的 10 个针脚的函数都归 0!在一个大量使用 GPIO 针脚的系统中,这将是一个很恼人的问题。我将这个问题留给有兴趣去修复这个函数的人,以确保只设置相关的 3 个位而不去覆写其它位,其它的所有位都保持不变。关于这个问题的解决方案可以在本课程的下载页面上找到。你可能会发现非常有用的几个函数是 and,它是计算两个寄存器的布尔与函数,mvns 是计算布尔非函数,而 orr 是计算布尔或函数。

4、另一个函数

现在,我们已经有了能够管理 GPIO 针脚函数的函数。我们还需要写一个能够打开或关闭 GPIO 针脚的函数。我们不需要写一个打开的函数和一个关闭的函数,只需要一个函数就可以做这两件事情。

我们将写一个名为 SetGpio 的函数,它将 GPIO 针脚号作为第一个输入放入 r0 中,而将值作为第二个输入放入 r1 中。如果该值为 0,我们将关闭针脚,而如果为非零则打开针脚。

将下列的代码复制粘贴到 gpio.s 文件的结尾部分。

.globl SetGpio
SetGpio:
pinNum .req r0
pinVal .req r1
alias .req reg 设置寄存器 reg 的别名为 alias

我们再次需要 .globl 命令,标记它为其它文件可访问的全局函数。这次我们将使用寄存器别名。寄存器别名允许我们为寄存器使用名字而不仅是 r0r1。到目前为止,寄存器别名还不是很重要,但随着我们后面写的方法越来越大,它将被证明非常有用,现在开始我们将尝试使用别名。当在指令中使用到 pinNum .req r0 时,它的意思是 pinNum 表示 r0

将下面的代码复制粘贴到上述的代码下面位置。

cmp pinNum,#53
movhi pc,lr
push {lr}
mov r2,pinNum
.unreq pinNum
pinNum .req r2
bl GetGpioAddress
gpioAddr .req r0
.unreq alias 删除别名 alias

就像在函数 SetGpio 中所做的第一件事情是检查给定的针脚号是否有效一样。我们需要同样的方式去将 pinNumr0)与 53 进行比较,如果它大于 53 将立即返回。一旦我们想要再次调用 GetGpioAddress,我们就需要将 lr 推送到栈上来保护它,将 pinNum 移动到 r2 中。然后我们使用 .unreq 语句来删除我们给 r0 定义的别名。因为针脚号现在保存在寄存器 r2 中,我们希望别名能够反映这个变化,因此我们从 r0 移走别名,重新定义到 r2。你应该每次在别名使用结束后,立即删除它,这样当它不再存在时,你就不会在后面的代码中因它而产生错误。

然后,我们调用了 GetGpioAddress,并且我们创建了一个指向 r0的别名以反映此变化。

将下面的代码复制粘贴到上述代码的后面位置。

pinBank .req r3
lsr pinBank,pinNum,#5a
lsl pinBank,#2
add gpioAddr,pinBank
.unreq pinBank
lsr dst,src,#valsrc 中二进制表示的数右移 val 位,并将结果保存到 dst

对于打开和关闭 GPIO 针脚,每个针脚在 GPIO 控制器上有两个 4 字节组。第一个 4 字节组每个位控制前 32 个针脚,而第二个 4 字节组控制剩下的 22 个针脚。为了判断我们要设置的针脚在哪个 4 字节组中,我们需要将针脚号除以 32。幸运的是,这很容易,因为它等价于将二进制表示的针脚号右移 5 位。因此,在本案例中,我们将 r3 命名为 pinBank,然后计算 pinNum ÷ 32。因为它是一个 4 字节组,我们需要将它与 4 相乘的结果。它与二进制表示的数左移 2 位相同,这就是下一行的命令。你可能想知道我们能否只将它右移 3 位呢,这样我们就不用先右移再左移。但是这样做是不行的,因为当我们做 ÷ 32 时答案有些位可能被舍弃,而如果我们做 ÷ 8 时却不会这样。

现在,gpioAddr 的结果有可能是 20200000 16(如果针脚号介于 0 到 31 之间),也有可能是 20200004 16(如果针脚号介于 32 到 53 之间)。这意味着如果加上 28 10,我们将得到打开针脚的地址,而如果加上 40 10 ,我们将得到关闭针脚的地址。由于我们用完了 pinBank ,所以在它之后立即使用 .unreq 去删除它。

将下面的代码复制粘贴到上述代码的下面位置。

and pinNum,#31
setBit .req r3
mov setBit,#1
lsl setBit,pinNum
.unreq pinNum
and reg,#val 计算寄存器 reg 中的数与 val 的布尔与。

该函数的下一个部分是产生一个正确的位集合的数。至于 GPIO 控制器去打开或关闭针脚,我们在针脚号除以 32 的余数里设置了位的数。例如,设置 16 号针脚,我们需要第 16 位设置数字为 1 。设置 45 号针脚,我们需要设置第 13 位数字为 1,因为 45 ÷ 32 = 1 余数 13。

这个 and 命令计算我们需要的余数。它是这样计算的,在两个输入中所有的二进制位都是 1 时,这个 and 运算的结果就是 1,否则就是 0。这是一个很基础的二进制操作,and 操作非常快。我们给定的输入是 “pinNum and 31 10 = 11111 2”。这意味着答案的后 5 位中只有 1,因此它肯定是在 0 到 31 之间。尤其是在 pinNum 的后 5 位的位置是 1 的地方它只有 1。这就如同被 32 整除的余数部分。就像 31 = 32 - 1 并不是巧合。

binary division example

代码的其余部分使用这个值去左移 1 位。这就有了创建我们所需要的二进制数的效果。

将下面的代码复制粘贴到上述代码的下面位置。

teq pinVal,#0
.unreq pinVal
streq setBit,[gpioAddr,#40]
strne setBit,[gpioAddr,#28]
.unreq setBit
.unreq gpioAddr
pop {pc}
teq reg,#val 检查寄存器 reg 中的数字与 val 是否相等。

这个代码结束了该方法。如前面所说,当 pinVal 为 0 时,我们关闭它,否则就打开它。teq(等于测试)是另一个比较操作,它仅能够测试是否相等。它类似于 cmp ,但它并不能算出哪个数大。如果你只是希望测试数字是否相同,你可以使用 teq

如果 pinVal 是 0,我们将 setBit 保存在 GPIO 地址偏移 40 的位置,我们已经知道,这样会关闭那个针脚。否则将它保存在 GPIO 地址偏移 28 的位置,它将打开那个针脚。最后,我们通过弹出 pc 返回,这将设置它为我们推送链接寄存器时保存的值。

5、一个新的开始

在完成上述工作后,我们终于有了我们的 GPIO 函数。现在,我们需要去修改 main.s 去使用它们。因为 main.s 现在已经有点大了,也更复杂了。将它分成两节将是一个很好的设计。到目前为止,我们一直使用的 .init 应该尽可能的让它保持小。我们可以更改代码来很容易地反映出这一点。

将下列的代码插入到 main.s 文件中 _start: 的后面:

b main

.section .text
main:
mov sp,#0x8000

在这里重要的改变是引入了 .text 节。我设计了 makefile 和链接器脚本,它将 .text 节(它是默认节)中的代码放在地址为 8000 16.init 节之后。这是默认加载地址,并且它给我们提供了一些空间去保存栈。由于栈存在于内存中,它也有一个地址。栈向下增长内存,因此每个新值都低于前一个地址,所以,这使得栈顶是最低的一个地址。

Layout diagram of operating system

图中的 “ATAGs” 节的位置保存了有关树莓派的信息,比如它有多少内存,默认屏幕分辨率是多少。

用下面的代码替换掉所有设置 GPIO 函数针脚的代码:

pinNum .req r0
pinFunc .req r1
mov pinNum,#16
mov pinFunc,#1
bl SetGpioFunction
.unreq pinNum
.unreq pinFunc

这个代码将使用针脚号 16 和函数编号 1 去调用 SetGpioFunction。它的效果就是启用了 OK LED 灯的输出。

用下面的代码去替换打开 OK LED 灯的代码:

pinNum .req r0
pinVal .req r1
mov pinNum,#16
mov pinVal,#0
bl SetGpio
.unreq pinNum
.unreq pinVal

这个代码使用 SetGpio 去关闭 GPIO 第 16 号针脚,因此将打开 OK LED。如果我们(将第 4 行)替换成 mov pinVal,#1 它将关闭 LED 灯。用以上的代码去替换掉你关闭 LED 灯的旧代码。

6、继续向目标前进

但愿你能够顺利地在你的树莓派上测试我们所做的这一切。到目前为止,我们已经写了一大段代码,因此不可避免会出现错误。如果有错误,可以去查看我们的排错页面。

如果你的代码已经正常工作,恭喜你。虽然我们的操作系统除了做 课程 2:OK02 中的事情,还做不了别的任何事情,但我们已经学会了函数和格式有关的知识,并且我们现在可以更好更快地编写新特性了。现在,我们在操作系统上修改 GPIO 寄存器将变得非常简单,而它就是用于控制硬件的!

课程 4:OK04 中,我们将处理我们的 wait 函数,目前,它的时间控制还不精确,这样我们就可以更好地控制我们的 LED 灯了,进而最终控制所有的 GPIO 针脚。


via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok03.html

作者:Robert Mullins 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

远程工作似乎是最近的一个热门话题。CNBC 报道称,70% 的专业人士至少每周在家工作一次。同样地,CoSo Cloud 调查发现, 77% 的人在远程工作时效率更高 ,而 aftercollege 的一份调查显示,8% 的千禧一代会更多地考虑提供远程工作的公司。 这看起来很合理:技术、网络以及文化似乎越来越推动了远程工作的发展。哦,自制咖啡也比以前任何时候更好喝了。

目前,我准备写另一篇关于公司如何优化远程工作的文章(所以请确保你加入我们的会员以持续关注——这是免费的)。

但今天,我想 分享一些个人如何做好远程工作的建议。不管你是全职远程工作者,或者是可以选择一周某几天在家工作的人,希望这篇文章对你有用。

眼下,你需要明白,远程工作不是万能药。当然,穿着睡衣满屋子乱逛,听听反社会音乐,喝一大杯咖啡看起来似乎挺完美的,但这不适合每个人。

有的人需要办公室的空间。有的人需要办公室的社会元素。有的人需要从家里走出来。有的人在家里缺乏保持专注的自律。有的人因为好几年未缴退税而怕政府工作人员来住处敲门。

远程工作就好像一块肌肉:如果你锻炼并且保持它,那么它能带来极大的力量和能力。如果不这么做,结果就不一样了。

在我职业生涯的大多数时间里,我在家工作。我喜欢这么做。当我在家工作的时候,我更有效率,更开心,更有能力。我并非不喜欢在办公室工作,我享受办公室的社会元素,但我更喜欢在家工作时我的“空间”。我喜欢听重金属音乐,但当整个办公室的人不想听到 After The Burial 的时候,这会引起一些问题。

“Squirrel.” 图片来源

我已经学会了如何正确平衡工作、旅行以及其他元素来管理我的远程工作,以下是我的一些建议。请务必在评论中分享一些你的建议

1、你需要纪律和习惯(以及了解你的“波动”)

远程工作确实是需要训练的一块肌肉。就像练出真正的肌肉一样,它需要一个明确的习惯混以健康的纪律。

永远保持穿戴整齐(不要穿睡衣)。设置你一天工作的开始和结束时间(大多时候我从早上 9 点工作到下午 6 点)。选好你的午餐休息时间(我的是中午 12 点)。选好你的早晨仪式(我的是电子邮件,紧接着是全面审查客户需求)。决定你的主工作场所在哪(我的主工作场所是我家庭办公室)。决定好每天你什么时候运动(大多数时候我在下午 5 点运动)。

设计一个实际的习惯并坚持 66 天。建立一个习惯需要很长时间,尽量不要偏离你的习惯。你越坚持这个习惯,做下去所花费的功夫越少。在这 66 天的末尾,你想都不会想,自然而然地就按习惯去做了。

话虽这么说,我们又不住在真空里 (更干净,或者别的什么)。我们都有自己的“波动”。

“波动”是你为了改变做事的方法时,对日常做出的一些改变。举个例子,夏天的时候我通常需要更多的阳光。那时我经常会在室外的花园工作。临近假期的时候我更容易分心,所以我在上班时间会更需要呆在室内。有时候我只想要多点人际接触,因此我会在咖啡馆里工作几周。有时候我就是喜欢在厨房或者长椅上工作。你需要认识你的“波动”并倾听你的身体。 首先养成习惯,然后在你认识到自己的“波动”的时候再对它进行适当的调整

2、与你的上司及同事一起设立预期目标

不是每个人都知道怎么远程工作,如果你的公司对远程工作没那么熟悉,你尤其需要和同事一起设立预期目标。

这件事十分简单:当你要设计自己的日常工作的时候,清楚地跟你的上司和团队进行交流。让他们知道如何找到你,紧急情况下如何联系你,以及你在家的时候如何保持合作。

在这里通信方式至关重要。有些远程工作者很怕离开他们的电脑,因为害怕当他们不在的时候有人给他们发消息(他们担心别人会觉得他们在边吃奇多边看 Netflix)。

你需要离开一会的时间。你需要在吃午餐的时候眼睛不用一直盯着电脑屏幕。你又不是 911 接线员。设定预期:有时候你可能不能立刻回复,但你会尽快回复

同样地,设定你的通常可响应的时间范围的预期。举个例子,我对客户设立的预期是我一般每天早上 9 点到下午 6 点工作。当然,如果某个客户急需某样东西,我很乐意在这段时间外回应他,但作为一个一般性规则,我通常只在这段时间内工作。这对于生活的平衡是必要的。

3、分心是你的敌人,它们需要管理

我们都会分心,这是人类的本能。让你分心的事情可能是你的孩子回家了,想玩救援机器人;可能是看看Facebook、Instagram,或者 Twitter 以确保你不会错过任何不受欢迎的政治观点,或者某人的午餐图片;可能是你生活中即将到来的某件事带走了你的注意力(例如,即将举办的婚礼、活动,或者一次大旅行)。

你需要明白什么让你分心以及如何管理它。举个例子,我知道我的电子邮件和 Twitter 会让我分心。我经常查看它们,并且每次查看都会让我脱离我正在工作的空间。拿水或者咖啡的时候我总会分心去吃零食,看 Youtube 的视频。

我的分心克星

由数字信息造成的分心有一个简单对策:锁起来。关闭选项卡,直到你完成了你手头的事情。有一大堆工作的时候我总这么干:我把让我分心的东西锁起来,直到做完手头的工作。这需要控制能力,但所有的一切都需要。

因为别人影响而分心的元素更难解决。如果你是有家庭的人,你需要明确表示,在你工作的时候常需要独处。这也是为什么家庭办公室这么重要:你需要设一些“爸爸/妈妈正在工作”的界限。如果有急事才能进来,否则让孩子自个儿玩去。

把让你分心的事锁起来有许多方法:把你的电话静音;把自己的 Facebook 状态设成“离开”;换到一个没有让你分心的事的房间(或建筑物)。再重申一次,了解是什么让你分心并控制好它。如果不这么做,你会永远被分心的事摆布。

4、(良好的)关系需要面对面的关注

有些角色比其他角色更适合远程工作。例如,我见过工程、质量保证、支持、安全以及其他团队(通常更专注于数字信息协作)的出色工作。其他团队,如设计或营销,往往在远程环境下更难熬(因为它们更注重触觉性)。

但是,对于任何团队而言,建立牢固的关系至关重要,而现场讨论、协作和社交很有必要。我们的许多感官(例如肢体语言)在数字环境中被剔除,而这些在我们建立信任和关系的方式中发挥着关键作用。

火箭也很有帮助

这尤为重要,如果(a)你初来这家公司,需要建立关系;(b)对某种角色不熟悉,需要和你的团队建立关系;或者(c)你处于领导地位,构建团队融入和参与是你工作的关键部分。

解决方法是?合理搭配远程工作与面对面的时间。 如果你的公司就在附近,可以用一部分的时间在家工作,一部分时间在公司工作。如果你的公司比较远,安排定期前往办公室(并对你的上司设定你需要这么做的预期)。例如,当我在 XPRIZE 工作的时候,我每几周就会飞往洛杉矶几天。当我在 Canonical 工作时(总部在伦敦),我们每三个月来一次冲刺。

5、保持专注,不要松懈

本文所有内容的关键在于构建一种(远程工作的)能力,并培养远程工作的肌肉。这就像建立你的日常惯例,坚持它,并认识你的“波动”和让你分心的事情以及如何管理它们一样简单。

我以一种相当具体的方式来看待这个世界:我们所做的一切都有机会得到改进和完善。举个例子,我已经公开演讲超过 15 年,但我总是能发现新的改进方法,以及修复新的错误(说到这些,请参阅我的 提升你公众演讲的10个方法)。

发现新的改善方法,以及把每个绊脚石和错误视为一个开启新的不同的“啊哈!”时刻让人兴奋。远程工作和这没什么不同:寻找有助于解锁方式的模式,让你的远程工作时间更高效,更舒适,更有趣。

看看这些书。它们非常适合个人发展。参阅我的 150 美元个人发展工具包 文章

……但别为此狂热。有的人花尽他们每一分钟来寻求如何变得更好,他们经常以“做得还不够好”、“完成度不够高”等为由打击自己,无法达到他们内心关于完美的不切实际的观点。

我们都是人,我们是有生命的,不是机器人。始终致力于改进,但要明白不是所有东西都是完美的。你应该有一些休息日或休息周。你也会因为压力和倦怠而挣扎。你也会遇到一些在办公室比远程工作更容易的情况。从这些时刻中学习,但不要沉迷于此。生命太短暂了。

你有什么提示,技巧和建议吗?你如何管理远程工作?我的建议中还缺少什么吗?在评论区中与我分享!


via: https://www.jonobacon.com/2019/01/14/remote-working-survival/

作者:Jono Bacon 选题:lujun9972 译者:beamrolling 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Google 云端硬盘 是全球比较受欢迎的云存储平台. 直到 2017 年, 全球有超过 8 亿的活跃用户在使用它。即使用户数在持续增长,但直到现在 Google 还是没有发布一款可以在 Linux 平台使用的客户端。但这难不倒 Linux 社区。不时就有一些开发者给 Linux 操作系统带来一些客户端。下面我将会介绍三个用于 Linux 系统非官方开发的 Google 云端硬盘客户端。使用这些客户端,你能把 Google 云端硬盘像虚拟磁盘一样挂载到 Linux 系统。请继续阅读。

1、Google-drive-ocamlfuse

google-drive-ocamlfuse 把 Google 云端硬盘当做是一个 FUSE 类型的文件系统,它是用 OCam 语言写的。FUSE 意即 用户态文件系统 Filesystem in Userspace ,此项目允许非管理员用户在用户空间创建虚拟文件系统。google-drive-ocamlfuse 可以让你把 Google 云端硬盘当做磁盘一样挂载到 Linux 系统。支持对普通文件和目录的读写操作,支持对 Google dock、表单和演示稿的只读操作,支持多个 Googe 云端硬盘用户,重复文件处理,支持访问回收站等等。

安装 google-drive-ocamlfuse

google-drive-ocamlfuse 能在 Arch 系统的 AUR 上直接找到,所以你可以使用 AUR 助手程序,如 Yay 来安装。

$ yay -S google-drive-ocamlfuse

在 Ubuntu 系统:

$ sudo add-apt-repository ppa:alessandro-strada/ppa
$ sudo apt-get update
$ sudo apt-get install google-drive-ocamlfuse

安装最新的测试版本:

$ sudo add-apt-repository ppa:alessandro-strada/google-drive-ocamlfuse-beta
$ sudo apt-get update
$ sudo apt-get install google-drive-ocamlfuse

使用方法

安装完成后,直接在终端里面输入如下命令,就可以启动 google-drive-ocamlfuse 程序了:

$ google-drive-ocamlfuse

当你第一次运行该命令,程序会直接打开你的浏览器并要求你确认是否对 Google 云端硬盘的文件的操作进行授权。当你确认授权后,挂载 Google 云端硬盘所需要的配置文件和目录都会自动进行创建。

当成功授权后,你会在终端里面看到如下的信息。

Access token retrieved correctly.

好了,我们可以进行下一步操作了。关闭浏览器并为我们的 Google 云端硬盘创建一个挂载点吧。

$ mkdir ~/mygoogledrive

最后操作,使用如下命令挂载 Google 云端硬盘:

$ google-drive-ocamlfuse ~/mygoogledrive

恭喜你了!你可以使用终端或文件管理器来访问 Google 云端硬盘里面的文件了。

使用终端:

$ ls ~/mygoogledrive

使用文件管理器:

如何你有不止一个账户,可以使用 label 命令对其进行区分不同的账户,就像下面一样:

$ google-drive-ocamlfuse -label label [mountpoint]

当操作完成后,你可以使用如下的命令卸载 Google 云端硬盘:

$ fusermount -u ~/mygoogledrive

获取更多信息,你可以参考 man 手册。

$ google-drive-ocamlfuse --help

当然你也可以看看官方文档和该项目的 GitHub 项目以获取更多内容。

2. GCSF

GCSF 是基于 Google 云端硬盘的 FUSE 文件系统,使用 Rust 语言编写。GCSF 得名于罗马尼亚语中的“ G oogle C onduce S istem de F ișiere”,翻译成英文就是“Google Drive Filesystem”(即 Google 云端硬盘文件系统)。使用 GCSF,你可以把 Google 云端硬盘当做虚拟磁盘一样挂载到 Linux 系统,可以通过终端和文件管理器对其进行操作。你肯定会很好奇,这到底与其它的 Google 云端硬盘 FUSE 项目有什么不同,比如 google-drive-ocamlfuse。GCSF 的开发者回应 Reddit 上的类似评论:“GCSF 意在某些方面更快(递归列举文件、从 Google 云端硬盘中读取大文件)。当文件被缓存后,在消耗更多的内存后,其缓存策略也能让读取速度更快(相对于 google-drive-ocamlfuse 4-7 倍的提升)”。

安装 GCSF

GCSF 能在 AUR 上面找到,对于 Arch 用户来说直接使用 AUR 助手来安装就行了,例如Yay

$ yay -S gcsf-git

对于其它的发行版,需要进行如下的操作来进行安装。

首先,你得确认系统中是否安装了Rust语言。

确保 pkg-configfuse 软件包是否安装了。它们在绝大多数的 Linux 发行版的默认仓库中都能找到。例如,在 Ubuntu 及其衍生版本中,你可以使用如下的命令进行安装:

$ sudo apt-get install -y libfuse-dev pkg-config

当所有的依赖软件安装完成后,你可以使用如下的命令来安装 GCSF:

$ cargo install gcsf

使用方法

首先,我们需要对 Google 云端硬盘的操作进行授权,简单输入如下命令:

$ gcsf login ostechnix

你必须指定一个会话名称。请使用自己的会话名称来代 ostechnix。你会看到像下图的提示信息和Google 云端硬盘账户的授权验证连接。

直接复制并用浏览器打开上述 URL,并点击 “allow” 来授权访问你的 Google 云端硬盘账户。当完成授权后,你的终端会显示如下的信息。

Successfully logged in. Credentials saved to "/home/sk/.config/gcsf/ostechnix".

GCSF 会把配置保存文件在 $XDG_CONFIG_HOME/gcsf/gcsf.toml,通常位于 $HOME/.config/gcsf/gcsf.toml。授权凭证也会保存在此目录当中。

下一步,创建一个用来挂载 Google 云端硬盘的目录。

$ mkdir ~/mygoogledrive

之后,修改 /etc/fuse.conf 文件:

$ sudo vi /etc/fuse.conf

注释掉以下的行,以允许非管理员用 allow_otherallow_root 挂载选项来挂载。

user_allow_other

保存并关闭文件。

最后一步,使用如下命令挂载 Google 云端硬盘:

$ gcsf mount ~/mygoogledrive -s ostechnix

示例输出:

INFO gcsf > Creating and populating file system...
INFO gcsf > File sytem created.
INFO gcsf > Mounting to /home/sk/mygoogledrive
INFO gcsf > Mounted to /home/sk/mygoogledrive
INFO gcsf::gcsf::file_manager > Checking for changes and possibly applying them.
INFO gcsf::gcsf::file_manager > Checking for changes and possibly applying them.

重复一次,使用自己的会话名来更换 ostechnix。你可以使用如下的命令来查看已经存在的会话:

$ gcsf list
Sessions:
- ostechnix

你现在可以使用终端和文件管理器对 Google 云端硬盘进行操作了。

使用终端:

$ ls ~/mygoogledrive

使用文件管理器:

如果你不知道自己把 Google 云端硬盘挂载到哪个目录了,可以使用 df 或者 mount 命令,就像下面一样。

$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 968M 0 968M 0% /dev
tmpfs 200M 1.6M 198M 1% /run
/dev/sda1 20G 7.5G 12G 41% /
tmpfs 997M 0 997M 0% /dev/shm
tmpfs 5.0M 4.0K 5.0M 1% /run/lock
tmpfs 997M 0 997M 0% /sys/fs/cgroup
tmpfs 200M 40K 200M 1% /run/user/1000
GCSF 15G 857M 15G 6% /home/sk/mygoogledrive

$ mount | grep GCSF
GCSF on /home/sk/mygoogledrive type fuse (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000,allow_other)

当操作完成后,你可以使用如下命令来卸载 Google 云端硬盘:

$ fusermount -u ~/mygoogledrive

浏览GCSF GitHub 项目以获取更多内容。

3、Tuxdrive

Tuxdrive 也是一个非官方 Linux Google 云端硬盘客户端。我们之前有写过一篇关于 Tuxdrive 比较详细的使用方法。可以查看如下链接:

当然,之前还有过其它的非官方 Google 云端硬盘客户端,例如 Grive2、Syncdrive。但它们好像都已经停止开发了。当有更受欢迎的 Google 云端硬盘客户端出现,我会对这个列表进行持续的跟进。

谢谢你的阅读。


via: https://www.ostechnix.com/how-to-mount-google-drive-locally-as-virtual-file-system-in-linux/

作者:SK 选题:lujun9972 译者:sndnvaps 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出