2023年4月

前言

在上一篇文章中,我们介绍了
&
运算符的基础用法,本篇文章,我们将介绍
&
运算符的一些高级用法。

一、人物简介

  • 第一位闪亮登场,有请今后会一直教我们C语言的老师 —— 自在。

  • 第二位上场的是和我们一起学习的小白程序猿 —— 逍遥。

二、位掩码

  • 位掩码是一种用于按位操作的技术

  • 它通过使用一个二进制数(掩码)来屏蔽或保留目标数中的一些特定位

  • 例如,如果要将一个无符号整数的最高位清零,可以使用以下代码:

#include <stdio.h>

int main()
{  
	unsigned int x = 0xABCD1234; //0b10101066666610011010001001000110100
	x &= ~(1U << 31);
 
    printf("x = %u\n",x);
	return 0;
}
  • 1U << 31
    表示将一个无符号整数的最高位设置为
    1
  • ~(1U << 31)
    取反得到掩码,再和原数进行按位与运算,就可以将最高位清零

三、判断奇偶性

二进制末尾位
奇数 1
偶数 0
  • 一个数的二进制表示中,如果最后一位为 1,则它是奇数,否则它是偶数
  • 因此,可以使用 & 运算符来判断一个数的奇偶性,例如:
#include <stdio.h>
int main()
{
    int x = 7;
    if (x & 1) 
    {
        printf("%d 是奇数\n", x);
    } 
    else 
    {
        printf("%d 是偶数\n", x);
    }
    return 0;
}
  • x & 1 将返回 x 的最后一位与 1 的按位与
  • 如果结果为 1,说明 x 是奇数,否则 x 是偶数。

四、判断是否为 2 的幂次方

  • 如果一个数是 2 的幂次方,那么它的二进制表示中,只有最高位为 1,其他位都为 0
  • 把这个数减去1,那么它的二进制表示中,最高位为 0,其他位都为1
  • x & (x - 1)
    的结果一定为 0
  • 例如,判断16 是否是 2 的幂次方:
#include <stdio.h>
int main()
{
    unsigned int x = 16;
    if (!(x & (x - 1))) 
    {
        printf("%u 是 2 的幂次方\n", x);
    } 
    else 
    {
        printf("%u 不是 2 的幂次方\n", x);
    }
    return 0;
}

小结

通过这篇文章,我们学会了用位运算符&来做位掩码操作、判断奇偶性、判断一个数是否为2的幂次方。

在下一篇文章中,我们将介绍位运算符
|
的几个高级用法。

深入理解 python 虚拟机:字节码教程(2)——控制流是如何实现的?

在本篇文章当中主要给大家分析 python 当中与控制流有关的字节码,通过对这部分字节码的了解,我们可以更加深入了解 python 字节码的执行过程和控制流实现原理。

控制流实现

控制流这部分代码主要涉及下面几条字节码指令,下面的所有字节码指令都会有一个参数:

  • JUMP_FORWARD
    ,指令完整条指令会将当前执行字节码指令的位置加上这个参数,然后跳到对应的结果继续执行。
  • POP_JUMP_IF_TRUE
    ,如果栈顶元素等于 true,将字节码的执行位置改成参数的值。将栈顶元素弹出。
  • POP_JUMP_IF_FALSE
    ,这条指令和
    POP_JUMP_IF_TRUE
    一样,唯一差别就是判断栈顶元素是否等于 true。
  • JUMP_IF_TRUE_OR_POP
    ,如果栈顶元素等于等于 true 则将字节码执行位置设置成参数对应的值,并且不需要将栈顶元素弹出。但是如果栈顶元素是 false 的话那么就需要将栈顶元素弹出。
  • JUMP_IF_FALSE_OR_POP
    ,和
    JUMP_IF_TRUE_OR_POP
    一样只不过需要栈顶元素等于 false 。
  • JUMP_ABSOLUTE
    ,直接将字节码的执行位置设置成参数的值。

总的来说,这些跳转指令可以让 Python 的解释器在执行字节码时根据特定条件来改变执行流程,实现循环、条件语句等基本语言结构。

现在我们使用一个例子来深入理解上面的各种指令的执行过程。

import dis


def test_control01():
    a = 1

    if a > 1:
        print("a > 1")
    elif a < 1:
        print("a < 1")
    else:
        print("a == 1")

if __name__ == '__main__':
    dis.dis(test_control01)

上面的程序输出结果如下所示:

  6           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (a)

  8           4 LOAD_FAST                0 (a)
              6 LOAD_CONST               1 (1)
              8 COMPARE_OP               4 (>)
             10 POP_JUMP_IF_FALSE       22

  9          12 LOAD_GLOBAL              0 (print)
             14 LOAD_CONST               2 ('a > 1')
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 JUMP_FORWARD            26 (to 48)

 10     >>   22 LOAD_FAST                0 (a)
             24 LOAD_CONST               1 (1)
             26 COMPARE_OP               0 (<)
             28 POP_JUMP_IF_FALSE       40

 11          30 LOAD_GLOBAL              0 (print)
             32 LOAD_CONST               3 ('a < 1')
             34 CALL_FUNCTION            1
             36 POP_TOP
             38 JUMP_FORWARD             8 (to 48)

 13     >>   40 LOAD_GLOBAL              0 (print)
             42 LOAD_CONST               4 ('a == 1')
             44 CALL_FUNCTION            1
             46 POP_TOP
        >>   48 LOAD_CONST               0 (None)
             50 RETURN_VALUE

我们现在来模拟一下上面的字节码执行过程,我们使用 counter 表示当前字节码的执行位置:

在字节码还没开始执行之前,栈空间和 counter 的状态如下:

现在执行第一条字节码 LOAD_CONST,执行完之后 counter = 2,因为这条字节码占一个字节,参数栈一个字节,因此下次执行的字节码的位置在 bytecode 的低三个位置,对应的下标为 2,因此 counter = 2 。

现在执行第二条字节码 STORE_FAST,让 a 指向 1 ,同样的 STORE_FAST 操作码和操作数各占一个字节,因此执行完这条字节码之后栈空间没有数据,counter = 4 。

接下来 LOAD_FAST 将 a 指向的对象也就是 1 加载进入栈中,此时的 counter = 6,LOAD_CONST 将常量 1 加载进行入栈空间当中,此时 counter = 8,在执行完这两条指令之后,栈空间的变化如下图所示:

接下来的一条指令是 COMPARE_OP ,这个指令有一个参数表示比较的符号,这里是比较 a > 1,并且会将比较的结果压入栈中,比较的结果是 false ,因为 COMPARE_OP 首先会将栈空间的两个输入弹出,因此在执行完这条指令之后栈空间和 counter 的值如下:

下面一条指令为 POP_JUMP_IF_FALSE,根据前面的字节码含义,这个字节码会将栈顶的 false 弹出,并且会进行跳转,并且将 counter 的值直接编程参数的值,这里他的参数是 22 ,因此 counter = 22,在执行完这条指令之后,结果如下:

因为现在已经跳转到了 22 ,因此接下来执行的指令为 LOAD_FAST,将变量 a 加载进入栈空间,LOAD_CONST 将常量 1 加载进入栈空间,在执行完这两条执行之后,变化情况如下:

在次执行 POP_JUMP_IF_FALSE,这回的结果也是 false ,因此继续执行 POP_JUMP_IF_FALSE,这次的参数是 40,直接将 counter 的值设置成 40 。

接下来 LOAD_GLOBAL 加载一个全局变量 print 函数 counter 变成 42 ,LOAD_CONST 加载字符串 "a == 1" 进入栈空间,counter = 44,此时状态如下:

CALL_FUNCTION 这个字节码有一个参数,表示调用函数的参数的个数,这里是 1,因为 print 函数只有一个参数,然后输出字符串 "a== 1",但是这里需要注意的是 print 函数会返回一个 None,因此执行完 CALL_FUNCTION 之后状态如下:

至此差不多上面的函数差不多执行完了,后面几条字节码很简单,就不再进行叙述了。

总结

在 Python 中,控制流指令可以让解释器根据特定条件改变执行流程,实现循环、条件语句等基本语言结构。Python 中与控制流有关的字节码指令包括 JUMP_FORWARD、POP_JUMP_IF_TRUE、POP_JUMP_IF_FALSE、JUMP_IF_TRUE_OR_POP、JUMP_IF_FALSE_OR_POP 和 JUMP_ABSOLUTE 等。这些指令都有一个参数,主要是用来计算跳转的目标位置等。通过对这些指令的了解,我们可以更深入地理解 Python 字节码的执行过程和控制流实现原理。


本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:
https://github.com/Chang-LeHung/dive-into-cpython

更多精彩内容合集可访问项目:
https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

一些知识

Textview

TextView中有下述几个属性:

  • id:
    为TextView设置一个组件id,根据id,我们可以在Java代码中通过findViewById()的方法获取到该对象,然后进行相关属性的设置,又或者使用RelativeLayout时,参考组件用的也是id!

  • layout_width:
    组件的宽度,一般写:
    wrap_content
    或者
    match_parent(fill_parent)
    ,前者是控件显示的内容多大,控件就多大,而后者会填满该控件所在的父容器;当然也可以设置成特定的大小,比如我这里为了显示效果,设置成了200dp。

  • layout_height:
    组件的高度,内容同上。

  • gravity:
    设置控件中内容的对齐方向,TextView中是文字,ImageView中是图片等等。

  • text:
    设置显示的文本内容,一般我们是把字符串写到string.xml文件中,然后通过@String/xxx取得对应的字符串内容的,这里为了方便我直接就写到""里,不建议这样写!!!

  • textColor:
    设置字体颜色,同上,通过colors.xml资源来引用,别直接这样写!

  • textStyle:
    设置字体风格,三个可选值:
    normal
    (无效果),
    bold
    (加粗),
    italic
    (斜体)

  • textSize:
    字体大小,单位一般是用sp!

  • background:
    控件的背景颜色,可以理解为填充整个控件的颜色,可以是图片哦!

startActivityForResult

大致可以理解为获取Activity的结果

Intent

intent相当于一个信道,putExtra相当于向里面传送一个名为参数一,内容为参数二的信息,接收方通过参数一相同的名称来接受参数二内容

  • startActivity
    (Intent)/
    startActivityForResult
    (Intent):来启动一个Activity

  • startService
    (Intent)/
    bindService
    (Intent):来启动一个Service

  • sendBroadcast
    :发送广播到指定BroadcastReceiver

.显式Intent使用示例:

这个用得很多,直接就上例子了:

例子1:
点击按钮返回Home界面:
运行效果图:

img

核心代码:

 Intent it = new Intent();
 it.setAction(Intent.ACTION_MAIN);
 it.addCategory(Intent.CATEGORY_HOME);
 startActivity(it);

例子2:
点击按钮打开百度页面:
运行效果图:

img

核心代码:

 Intent it = new Intent();
 it.setAction(Intent.ACTION_VIEW);
 it.setData(Uri.parse("http://www.baidu.com"));
 startActivity(it);

在记事本中应用


编辑页面
当点击返回按钮的时候跳转到
主页面

EditActivity:

 public boolean onKeyDown(int keyCode, KeyEvent event){
        if(keyCode == KeyEvent.KEYCODE_HOME){
            return true;
       }
        else if(keyCode == KeyEvent.KEYCODE_BACK){
            Intent intent = new Intent();//只用来传输信息,不用来跳转
            intent.putExtra("input",et.getText().toString());
            setResult(RESULT_OK,intent);
            finish();
            return true;
       }
        return super.onKeyDown(keyCode,event);
   }

MainActivity中:

 public void onClick(View view) {
                //测试btn
                //Log.d(TAG,"onClick:click");
                Intent intent = new Intent(MainActivity.this,EditActivity.class);//从参数一跳转到参数二
                //启动活动并获取结果
                startActivityForResult(intent,0);                                                         //启动参数这样的一个跳转
 
 
           }

开发过程

实现点击按钮界面的跳转

按钮:

img

跳转

先初始化按钮

 super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //floatingbutton初始化
        btn = ( FloatingActionButton)findViewById(R.id.fab);
        tv = findViewById(R.id.tv);
 

之后用一个setOnClickListener函数实现跳转

 btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //测试btn
                //Log.d(TAG,"onClick:click");
                Intent intent = new Intent(MainActivity.this,EditActivity.class);//从参数一跳转到参数二
                //启动活动并获取结果
                startActivityForResult(intent,0);                                                         //启动参数这样的一个跳转
           }
       });

接受Edit输入的结果

MainActivity中:

 //下面接受startActivityForResult结果
    @Override
    //通过上面的Intent跳转到Edit,并通过下面的函数来对Edit的内容进行接收
    protected void onActivityResult(int requestCode,int resultCode,Intent data){
        super.onActivityResult(requestCode,resultCode,data);
        String edit = data.getStringExtra("input");
        //Log.d(TAG,edit);
        tv.setText(edit);       //输出输入的内容到Main屏幕中
   }

EditActivity中:

 //可以背一下下面格式
    //功能:传回一个“input”的值来对应前面的getStringExtra
    public boolean onKeyDown(int keyCode, KeyEvent event){
        if(keyCode == KeyEvent.KEYCODE_HOME){
            return true;
       }
        else if(keyCode == KeyEvent.KEYCODE_BACK){
            Intent intent = new Intent();//只用来传输信息,不用来跳转
            intent.putExtra("input",et.getText().toString());
            setResult(RESULT_OK,intent);
            finish();
            return true;
       }
        return super.onKeyDown(keyCode,event);
   }

此时的完整代码:

MainActivity:

package com.example.note;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ListView;
import android.widget.TextView;

import com.google.android.material.floatingactionbutton.FloatingActionButton;

public class MainActivity extends AppCompatActivity {

final String TAG = "tag";
FloatingActionButton btn; //后面再初始化
TextView tv;
//笔记本的核心:ListView
ListView lv;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//floatingbutton初始化
btn = ( FloatingActionButton)findViewById(R.id.fab);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//测试btn
//Log.d(TAG,"onClick:click");
Intent intent = new Intent(MainActivity.this,EditActivity.class);//从参数一跳转到参数二
//启动活动并获取结果
startActivityForResult(intent,0); //启动参数这样的一个跳转


}
});

}

//下面接受startActivityForResult结果
@Override
//通过上面的Intent跳转到Edit,并通过下面的函数来对Edit的内容进行接收
protected void onActivityResult(int requestCode,int resultCode,Intent data){
super.onActivityResult(requestCode,resultCode,data);
String edit = data.getStringExtra("input");
//Log.d(TAG,edit);
tv.setText(edit); //输出输入的内容到屏幕中
}

}

EditActivity:

package com.example.note;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.widget.EditText;
import android.content.Intent;
import java.security.Key;

public class EditActivity extends AppCompatActivity{

//实例化一个et
EditText et;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.edit_layout);
//定位输入好的内容
et=findViewById(R.id.et);
}
//可以背一下下面格式
//功能:传回一个“input”的值来对应前面的getStringExtra
public boolean onKeyDown(int keyCode, KeyEvent event){
if(keyCode == KeyEvent.KEYCODE_HOME){
return true;
}
else if(keyCode == KeyEvent.KEYCODE_BACK){
Intent intent = new Intent();//只用来传输信息,不用来跳转
intent.putExtra("input",et.getText().toString());
setResult(RESULT_OK,intent);
finish();
return true;
}
return super.onKeyDown(keyCode,event);
}

}

效果

点击按钮会跳转到edit界面,输入内容后按返回键会将内容显示在MainActivity界面中

img

学习操作系统原理最好的方法是自己写一个简单的操作系统。


之前讲解开发环境时并没有介绍具体的安装过程,有网友反应CentOS的安装配置有问题,尤其是共享文件夹。本讲我们就来补充介绍一下在VirtualBox中安装配置CentOS的具体过程,彻底解决GrapeOS开发环境问题。

一、新建虚拟机

1.在VirtulBox中点击新建按钮。

2.名称和文件夹都可以自己定,我这里名称填写为CentOS7,类型选Linux,版本选Red Had (64-bit)。点击“下一步”按钮。

3.内存大小就填写默认的1024MB,够用了。如果不够用后面也可以改。点击“下一步”按钮。

4.虚拟硬盘选默认的“现在创建虚拟硬盘”。点击“创建”按钮。

5.虚拟硬盘文件类型选默认的VDI。点击“下一步”按钮。

6.存储在物理硬盘上选默认的“动态分配”。点击“下一步”按钮。

7.文件位置就用默认的。文件大小也用默认的8GB,对本教程来说够用了。点击“创建”按钮。

8.一台虚拟裸机就配好了,具体配置如下截图:

二、安装CentOS

裸机配好了,下面我们就该装操作系统了。鼠标点击虚拟光驱,选择虚拟盘。选择我们之前已经下载好的CentOS安装文件CentOS-7-x86_64-Minimal-2009.iso,iso文件相当于虚拟光盘。

点击绿色的启动按钮,启动虚拟机。

启动后会显示一个安装选项,可以用键盘上下键选择,选第一项或第二项都可以,同时停止倒计时。如果不做选择的话,倒计时结束会自动安装。

倒计时时间比较长,我这里选默认的第二项,然后回车。需要注意的是如果你第一次使用会发现鼠标进去虚拟机里出不来,此时按一下键盘上的右Ctrl键鼠标就出来了。

过一会儿会出现图形安装界面,语言选中文:

点击“安装位置”:

单击本地标准磁盘下的磁盘图标2次,然后点击“完成”按钮:

点击“开始安装”:

安装过程大概5分钟:

安装完需要设置一下root密码,点击“ROOT密码”:

设置好密码点击“完成按钮”:

继续点击“完成配置”按钮:

稍等片刻:

安装过程结束,点击“重启”按钮即可启动安装好的CentOS,同时虚拟机会自动弹出虚拟安装光盘,防止重新从光盘启动。

登录:

三、网络与IP配置

1.与外网通信

虽然网络服务已经开机自动启动,但此时并不能上网:

原因是网卡没有启动,下面我们来启动网卡。修改文件/etc/sysconfig/netwrok-scripts/ifcfg-enp0s3

只要把最后一行“ONBOOT=no”改成“ONBOOT=yes”即可。

重启网络服务就可以ping通百度了:

2.与宿主机通信

此时CentOS虚拟机已经可以访问外网了,但是和宿主机Windows之间还不能相互通信,需要再加一个网卡才行,下面我们就来配置。

poweroff
命令关闭CentOS虚拟机:

在VirtualBox管理器中点击“网络”:

弹出网络设置窗口:

点击选项卡“网卡2”:

勾选“启用网络连接”,连接方式选“仅主机(Host-Only)网络”,界面名称选默认的“VirtualBox Host-Only Ethernet Adapter”。

点击“OK”按钮,在VirtualBox管理器中可以看到多个一个网卡2:

启动并登录CentOS虚拟机,通过
ip a
命令可以看到此时多了一个enp0s8的网卡。此时该网卡上的IP地址是动态分配的,每次启动都有可能不同。

为了方便使用,我们需要设置静态IP,下面就来设置。
新建并编辑文件/etc/sysconfig/netwrok-scripts/ifcfg-enp0s8

在ifcfg-enp0s8中输入如下内容:

TYPE=Ethernet
BOOTPROTO=static
ONBOOT=yes
IPADDR=192.168.56.2
NETMASK=255.255.255.0
GATEWAY=192.168.56.1
DNS1=114.114.114.114

关于网关地址和网络掩码可以通过如下方式查看到。
在VirtualBox管理器菜单中点击“管理”->“主机网络管理器”:

可以看到默认有一个网络适配器“VirtualBox Host-Only Ethernet Adapter”,如果没有可以点击“创建”按钮创建一个。

点击“属性”按钮就可以看到网关地址和网络掩码了。

保存好ifcfg-enp0s8文件,然后通过
reboot
命令重启CentOS虚拟机。

可以看到我们设置的静态IP生效了:

此后我们就可以方便的通过SSH从PowerShell登录到CentOS虚拟机了。
Windows键+R键打开运行小窗口,输入“powershell”:

通过SSH登录:

四、安装增强功能

设置共享文件夹前必须先安装增强功能,下面我们来安装。
首先需要安装几个前置软件:

yum install -y kernel-devel-$(uname -r) bzip2 gcc make perl

前置软件安装完毕:

在CentOS虚拟机窗口的菜单栏中点击“设备”->“安装增强功能”

点击之后的作用就是VirtualBox将自带的增强功能iso光盘文件放入到CentOS虚拟机的虚拟光驱中。

挂载后就可以看到光盘中的内容,我们要运行的是“VBoxLinuxAdditions.run”这个文件。

大约一分钟增强功能安装完毕:

五、设置共享文件夹

首先在CentOS虚拟机中创建共享文件夹/media/VMShare

在VirtualBox管理器中点击“共享文件夹”:

点击“添加共享文件夹”

然后填写相应的内容并点击“OK”按钮:

再点击“OK”按钮:

可以看到共享文件夹数量为1:

此时在PowerShell中再查看文件夹/media/VMShare,已经能看到共享的内容了:

如果看不到共享的内容,重启一下CentOS虚拟机就能了。
共享文件夹设置完毕。


视频版地址:
https://www.bilibili.com/video/BV1nX4y1r7U4/
配套的代码和资料:
https://gitee.com/jackchengyujia/grapeos-course
GrapeOS操作系统QQ群:643474045

哈喽大家好,我是咸鱼。今天跟大家分享一个关于 Linux 服务(service)相关的案例

案例现象

我在 3 月 31日的时候发表了一篇《
shell 脚本之一键部署安装 Nginx
》,介绍了如何通过 shell 脚本一键安装 Nginx

我脚本中执行了 Nginx 开机自启动的命令,当我使用
systemctl status nginx
命令复核的时候,我发现
Nginx 服务设置开机自启动并没有生效

使用下面的命令设置一下

通常来说,设置开机自启动其实就是将 nginx.service 这个文件创建一个软连接然后挂在
/etc/systemd/system/multi-user.target.wants/
目录下面

举个例子,我要将 atd.service 设置开机自启动

可以看到设置了开机自启动的服务都在这个目录下面有软连接,但是没有 Nginx 服务

我们使用下面的命令来看下 nginx 服务有没有设置开机自启动

奇怪,怎么
systemctl enable nginx.service

没有生效?

手动创建一下软链接试试

发现设置开机自启动成功

问题:使用 systemctl 命令不能设置 nginx 服务开机自启动,需要手动去挂载软连接

定位问题

在排查问题之前,我先给大家简单介绍一下
daemon 与 服务(service)

daemon 与 服务(service)

我们知道,在 Linux 中,服务(service)其实就是一个个程序,它们能够实现某一功能、提供某一服务

但通常我们在查阅类 Unix 系统相关的技术文档时,又经常会看到“请启动某某 daemon 来提供某某功能”

那么这个 daemon 到底是啥意思?它跟 service 有什么区别?

简单点来说,系统为了实现某些功能必须要提供一些服务(比如想要实现负载均衡的功能需要提供 Nginx 服务)

但是提供的 service 需要程序的运作(例如你需要启动 Nginx 进程),所以我们


认为使系统能够提供某些 service 的程序称作 daemon
(例如使系统能够提供负载均衡服务的程序 nginx 为 daemon)

看到这里小伙伴们可能都晕了,说实话我第一次看到的时候也是这样的

其实你不必去区分什么是 daemon 和 service,因为提供某一 service 是需要一个 daemon 在运作,没有这个运作的 daemon 就不会有这个 service

无论是命令行模式(runlevel 3),还是图像界面模式(runlevel 5),我们在开机进入 Linux 主机之后,系统已经开始提供很多 service 了(例如 sshd )

那么这些 service 是如何启动的,系统又是怎么管理它们的呢?

在早期 Linux 是使用 SystemV 来管理服务的,启动系统服务的管理方式被称为 SysV 的 init 脚本处理方式——系统内核第一个程序是 init,然后 init 去唤起所有系统需要的服务

SystemV 管理服务的开机自启动有两种方式:

  1. 通过挂软连接的方式


/etc/rc.d/rc[0-6]/SXX
服务名字挂载到
/etc/init.d/
下(其中 SXX 中的 S 表示启动该服务,XX 是数字,为启动的顺序)

  1. 通过 chkconfig 命令

创建软连接的方式比较麻烦,一般来说都是用命令来管理

但是 CentOS 7 之后就放弃了使用多年的 SystemV ,改用 systemd 来管理服务

systemd 管理服务

systemd 将过去所谓的 daemon 程序称作一个个服务单位(unit),而每个 unit 根据功能来区分成不同的类型(type):

  • 系统服务(service)

  • 负责网络数据监听与交换的服务(socket)

  • 快照服务(sanpshot)

而且
systemd 将许多的 unit 集合成一个所谓的 target 项目
,你执行某个 target 其实就是执行 target 下的多个 unit

可能有小伙伴觉得,这么多 unit 分成不同的 type,然后又被合集到不同的 target ,管理起来不会很麻烦吗

其实也还好,因为相关的文件都存放在下面的目录当中了

总结,系统开机会不会执行某些服务是看
/etc/systemd/system/

目录下有没有该服务的启动脚本,而服务的启动脚本是放在
/usr/lib/systemd/system/

下的

systemctl 命令

systemd 来管理服务的方式是通过 systemctl 命令,相较于 SysV 通过 service / chkconfig / setup / init 一堆命令,systemd 管理服务的方式简单多了

PS:关闭服务除了 systemctl 命令,也能用 kill 命令的方式,但是这两个命令不要混用!

服务的状态

  • 服务的当前状态:

    • active (running):表示服务正在运行

    • active (exited):表示该服务执行一次就正常结束,目前没有执行

    • active (waiting):表示该服务正在运行,不要需要等待其他事件执行之后才能继续处理

    • inactive:表示服务目前关闭,没有运行

  • 服务预设状态:

    • enable:开机的时候将自启动

    • disable:开机的时候不会自启动

    • static:这个服务不会开机自启动,但是有可能会被其他开机自启动的服务来唤醒(依赖性)

    • mask:无论如何都不会被启动,因为已经被强制注销

服务的启动文件

前面我们说过,服务的启动脚本文件放在
/usr/lib/systemd/system/
下的,如果需要对服务的启动脚本文件修改,需要进入到该目录下(官方不建议直接修改该目录下的文件,但是会比较麻烦且繁琐)

我们就拿 sshd.service 举例,来了解下服务的启动脚本里面的配置字段

分析上面文件中的内容,我们可以看到分成了三个部分(block):

  • [Unit]

    • unit(即服务)本身的说明,以及与其他服务的依赖性设定(After、Wants 字段)

  • [Service]

    • 还有 [Socket], [Timer], [Mount], [Path] 等等,不同的 type 就用不同的字段

    • 我们拿的是 sshd.service,所以就是 [Service]

    • 这个部分中主要规定了服务的启动脚本、环境文件名、重启方式等等

  • [Install]

    • 表示这个服务安装到哪个 target 下面去

    • 这部分与
      systemctl enable

      systemctl disable
      命令相结合,用于 enable 或 disable 一个服务

下面我将分别列出三个部分的一些常见配置字段

解决问题

现在我们已经大致对 Linux 的服务有了一个初步了解

我们回到刚开始的问题:nginx 服务无法通过 systemctl 命令设置开机自启动,手动挂载软连接之后自启动状态不是 enable ,而是 static

既然是跟 systemctl 相关的,我们去看下 nginx 的服务启动脚本

可以看到,这台机器上 nginx 的服务启动脚本只有两个部分([Unit]、[Service]),并没有 [Install]

而  [Install] 部分往往是跟服务的开机自启动相关

我们加上 [Install]

其中
multi-user.target
表示命令行模式(即等效于系统运行级别为 3 )


WantedBy
表示该服务放在哪个 target 下,一般来讲
WantedBy
对应的 target 为指定系统的运行级别

然后重启一下 nginx 启动脚本文件

设置开机自启动,发现创建软连接成功了

看下状态

总结:

  • 一般来讲,服务无法设置开机自启动首先考虑是不是服务启动脚本配置有问题(
    /usr/lib/systemd/system/
    目录下),这种情况常见于编译安装的时候需要自己编写服务启动文件

  • 服务能够开机自启动其实就是将
    /usr/lib/systemd/system/
    目录下的服务启动脚本挂载到了
    /etc/systemd/system/
    下,一般是挂载到
    /etc/systemd/system/multi-user.target.wants/

    • multi-user.target.wants:表示启动了 multi-user.target 之后(即系统启动且运行级别为 3,为系统的默认启动 target)这个目录下的文件都会跟着启动

  • systemctl status
    命令显示的内容里面有一个
    vendor preset: disabled
    字段,这个表示该服务首次安装之后不会自启动,需要手动启动(
    systemctl enable


感谢阅读,
喜欢作者就动动小手
[
一键三连]
,这是我写作最大的动力