systemd负责管理整个操作系统,功能非常强大。我们在实际工作中,常常使用systemd来管理我们的服务。
(1)想要开机之后自动启动,从而不需要每次开机之后都手动启动进程,可以通过systemd来实现。
(2)如果我们要求进程异常崩溃之后要自动拉起来,可以通过systemd来实现。
(3)我们有3个服务:a、b、c,其中服务b和c的启动依赖于服务a,只有服务a启动之后,服务b和c才可以启动,这样的依赖关系管理,我们也可以通过systemd来实现。
(4)想要限制服务所使用的资源,比如将服务的cpu利用率限制在50%,内存使用限制在1G,我们可以通过systemd来实现。
(5)如果我们想要设置进程的调度策略,那么可以通过systemd来实现。
1最简example
如下是我们自己的代码,在main函数中有一个死循环,在循环中打印一条日志。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
int i = 0;
while(1) {
printf("systemd daemon example\n");
}
return 0;
}
service文件,该文件是服务的配置文件,也是systemd可以解析的文件。
[Unit]
Description=hello systemd
[Service]
ExecStart=/home/wangyanlong/systemdtest/daemon
[Install]
WantedBy=multi-user.target
将源码编译成可执行文件:gcc daemon.c -o daemon
将service文件拷贝到systemd的工作目录:cp helloworld.service /lib/systemd/system/
systemd配置文件重新加载:systemctl daemon-reload
启动服务:systemctl start helloworld.service
查看服务状态:ststemctl status helloworld.service
查看日志:joutnalctl -u helloworld.service
2服务类型
systemd中的服务类型,常用的有simple、forking和notify:
2.1simple
最简单的类型,默认的服务类型,上边的例子就是simple类型,当我们的服务本身就会一直运行,那么可以配置simple类型。
2.2forking
forking类型的服务,systemd启动的服务内部会创建子进程,子进程运行之后父进程就退出了。
如下代码,在主进程中会通过fork来执行一个子进程,fork之后,父进程退出。这种情况下,需要配置为forking类型。当父进程退出后,service的状态会切为active状态,父进程退出前,使用systemctl status xxx看到的服务状态为activating。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("fork main\n");
// sleep(20);
printf("before fork\n");
pid_t pid = fork();
if (pid == 0) {
execve("/home/wangyanlong/systemdtest/daemon", NULL, NULL);
}
sleep(20);
printf("fork end\n");
return 0;
}
[Unit]
Description=hello systemd
[Service]
Type=forking
ExecStart=/home/wangyanlong/systemdtest/fork
[Install]
WantedBy=multi-user.target
2.3notify
对于simple和forking类型来说,服务从activating切换为active状态的时机,完全由systemd内部来决定。simple类型,当把服务拉起来之后,便会切换为active状态;forking类型的服务,当主进程退出后,便会切换到active状态。这样的方式,用户不可控,当我们对多个服务有严格的启动顺序要求的时候,这种方式难以满足这样的使用场景,在这种情况下,就需要使用notify。
notify类型的服务,需要用户在代码中调用sd_notify函数来向systemd报状态,systemd收到信息之后则会将服务的状态切换为active状态。
如下代码,在main函数中先睡20s,然后调用sd_notify来通知systemd。通过systemctl status helloworld.service可以看到,启动20s之后,服务的状态切换为active。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <systemd/sd-daemon.h>
int main() {
sleep(20);
sd_notify(0, "READY=1");
int i = 0;
while(1) {
printf("systemd daemon example\n");
// sleep(5);
}
return 0;
}
3开机自动重启
使用systemctl enable xxx和systemctl disable xxx可以分别使能开机自启动和禁止开机自启动。
4异常退出自动重启
当服务异常退出时,systemd能够自动将服务重新拉起,这是我们实际工作中经常需要的。同时,当进程正常退出的时候,systemd不会将服务拉起。
如下是进程退出后重启的最简单的例子。Restart配置为always,说明只要进程退出,都会重启,不管进程是正常退出还是异常退出。RestartSec是进程退出之后,间隔多长时间才会重启。
[Unit]
Description=hello systemd
[Service]
Type=simple
ExecStart=/home/wangyanlong/systemdtest/daemon
Restart=always
RestartSec=1
[Install]
WantedBy=multi-user.target
Restart有多个配置:no、always、on-success、on-failure、on-abnormal、on-abort、on-watchdog。其中no是默认值,表示永远不会重启;always是永远会重启,不管进程是正常退出还是异常退出;on-success表示只有进程正常退出的时候才会重启,所谓正常退出,指的是退出码是0,或者被信号SIGHUP、SIGINT、SIGTERM、SIGPIPE或者被中的一个杀死;on-failure表示失败,所谓失败,是当退出码不为0,或者服务是被上述几个信号之外的信号杀死的。
在实际使用中,具体什么退出码是正常的,什么退出码是异常的,我们自己也可以修改,比如我们设置信号SIGKILL为正常的,那么使用SIGKILL杀死进程的时候,也属于正常退出,不会重启。
配置如下:
[Unit]
Description=hello systemd[Service]
Type=simple
ExecStart=/home/wangyanlong/systemdtest/daemon
Restart=on-failure
RestartSec=1
SuccessExitStatus=SIGKILL[Install]
WantedBy=multi-user.target
5调度策略
systemd可以设置服务的调度策略和优先级,如下配置,其中CPUSchedulingPolicy=rr和
CPUSchedulingPriority=50分别设置调度策略为RR,优先级是50。
[Unit]
Description=hello systemd
[Service]
Type=simple
ExecStart=/home/wangyanlong/systemdtest/daemon
Restart=on-failure
RestartSec=1
SuccessExitStatus=SIGKILL
CPUSchedulingPolicy=rr
CPUSchedulingPriority=50
[Install]
WantedBy=multi-user.target
6资源限制
使用systemd管理的服务,可以限制服务所使用的资源。systemd通过cgroup来对服务做资源限制,可以限制的资源包括cpu、内存、io等。
如下代码,main函数中是一个while(1)死循环,默认情况下cpu使用率达到100%。但是通过CPUQuota=50%可以将cpu使用限制到50%。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
while(1) {
}
sleep(10);
return 0;
}
如下配置文件,如果限制了cpu,那么在cgroup下便会创建一个对应的cpu controller,如下图所示,可以看到,在service文件中配置的CPUQuota=50%,在cgroup中对应的参数是cpu.cfs_quota_us,值为50000。
[Unit]
Description=hello systemd[Service]
Type=simple
ExecStart=/home/wangyanlong/systemdtest/daemon
CPUQuota=50%[Install]
WantedBy=multi-user.target
7服务之间的依赖
与进程依赖相关的配置有多个After和Before,Requires,Wants。
After、Before的依赖关系最弱,Wants次之,Requires最强。
After和Before可以决定服务启动的顺序,只是约束启动顺序而已,如果在service2.service中配置After=service1.servce,那么即使service1没有启动,service2也是可以启动的。
Wants表示服务之间的依赖关系,假如在service2.service中配置了Wants=service1.servce,如果sevice1没有启动的情况下启动service2,那么会尝试启动service1,不管service1是否能启动,servie2最终都会启动。
Requires是最强的依赖,如果在service2.service中配置了Requires=service1.servce,如果service1启动失败,那么service2也会启动失败,也就是说必须要在service1正常运行的时候,service2才可以正常启动,正常运行。如果service1和service2都在正常运行,这个时候将service1关闭了,那么service2也会被关闭。同样的情况,如果是service2 Wants service1,两个服务都在运行,如果间service1关闭,那么service2还会运行,不会被关闭。
service1.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <systemd/sd-daemon.h>
int main() {
sleep(20);
sd_notify(0, "READY=1");
int i = 0;
while(1) {
printf("systemd s1\n");
// sleep(5);
}
return 0;
}
service2.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <systemd/sd-daemon.h>
int main() {
sleep(20);
sd_notify(0, "READY=1");
int i = 0;
while(1) {
printf("systemd s2\n");
// sleep(5);
}
return 0;
}
service1.service
[Unit]
Description=service1[Service]
Type=notify
ExecStart=/home/wangyanlong/systemdtest/service1[Install]
WantedBy=multi-user.target
service2.service
[Unit]
Description=service2
Wants=service1.service[Service]
Type=notify
ExecStart=/home/wangyanlong/systemdtest/service2[Install]
WantedBy=multi-user.target