안드로이드 프레임워크 : init 프로세스
init 프로세스 main()
1. 시그널 핸들러 등록
리눅스 프로세스들은 서로 정보를 교환하기 위해 시그널을 주고 받는다. 각 프로세스는 다른 프로세스에서 발생하는 시그널을 처리하기 위해 시그널 핸들러를 등록한다.
init 프로세스는 sigaction() 함수를 통해 자신이 생성한 자식 프로세스가 종료될때 발생하는 SIGCHLD 시그널을 처리할 핸들러를 등록하는데, 이때 등록되는 sigchld_handler는 signal_fd 소켓에 SIGCHLD 시그널이 발생했음을 알리는 핸들러이며 시그널에 대한 실제 처리는 init의 마지막단계인 이벤트처리 루프에서 이뤄진다.
static int signal_fd = -1;
static void sigchld_handler(int s){
write(signal_fd, &s, 1); // signal_fd가 가리키는 소켓에 인자로 전달받은 s 데이터를 저장
}
int main(int argc, char **argv){
struct sigaction act;
act.sa_handler = sigchld_handler;
act.sa_falgs = SA_NOCLDSTOP; // 프로세스가 종료되는 상황에만 SIGCHLD시그널을 받겠다는 뜻
act.sa_mask = 0;
act.sa_restorer = NULL;
sigaction(SIGCHLD, &act, 0); // SIGCHLD 핸들러 등록
...
2. 부팅에 필요한 디렉터리 생성 및 마운트
시그널 핸들러가 등록되고 나면 부팅에 필요한 디렉터리를 생성하고 마운트하는데 /dev, /proc, /sys와 같은 디렉터리는안드로이드 시스템 운용중에 init 프로세스가 생성하고 시스템이 종료될 때 사라진다.
// init의 main() 계속
mkdir("/dev", 0755);
mkdir("/proc", 0755);
mkdir("/sys", 0755);
// tmpfs는 램 파일시스템의 일종으로 접근속도가 빠르다는 특징이 있다. /dev에 빠르게 접근하기 위함
mount("tmpfs", "/dev", "tmpfs", 0, "mode=0755");
mkdir("/dev/pts", 0755);
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0, "NULL"); // 가상터미널을 위한 파일시스템
// 커널메모리에 존재하는 가상파일시스템. 애플리케이션은 /proc를 통해 커널공간의 데이터에 접근한다.
mount("proc", "/proc", "proc", 0, "NULL");
mount("sysfs", "/sys", "sysfs", 0, "NULL");
...
3. 출력 디바이스 생성 및 커널 로그 기록
/dev 디렉터리가 생성된 이후 open_devnull_stdio() 함수 호출을 통해 /dev/__null__ 디바이스 노드 파일을 생성하고 표준 입출력, 표준에러를 모두 __null__로 리다이렉션한다.
// init의 main() 계속
open_devnull_stdio();
log_init();
INFO("reading config file\n");
...
그렇기 때문에 표준출력으로 init의 로그를 확인할 수 없게 되는데, log_init() 함수를 호출하여 /dev/__kmsg__ 라는 새로운 장치(로그버퍼)를 만들고 커널의 메시지 출력 함수인 printk() 함수를 통해 __kmsg__로 에러를 출력한다.
dmesg은 __kmsg__의 내용을 파싱해서 보여주는 명령이다.
4. init.rc 파싱 및 명령어 실행
리눅스에서는 /etc/rc.d/ 하위의 스크립트를 통해 부팅 시 실행할 파일을 정의하고, /etc 에 환경변수를 세팅하는 파일을 두는데, 안드로이드는 init.rc 파일을 통해 init 프로세스가 공통적으로 수행할 기능 이나 환경설정을 정의한다.
init.{hardware}.rc 파일에서는 플랫폼에 따라 변경되는 특화된 프로세스나 환경설정을 정의한다.
- nox 에뮬레이터의 init.rc 파일
//init의 main()
parse_config_file("/init.rc");
snprintf(tmp, sizeof(tmp), "/init.%s.rc", hardware);
parse_config_file(tmp);
qemu_init(); // 4-1. qemu 초기화 (AVD)
...
parse_config_file()함수는 인자로 전달된 init.rc 파일을 읽어 파싱한 후 init프로세스에서 전역 구조체로 선언된 service_list와 action_list에 연결리스트 형태로 등록된다.
이후 init.rc 파일의 액션리스트인 early-init, init, early-boot, boot 등의 섹션에 포함된 명령어들을 순서대로 실행한다.
boot섹션에서는 서비스 리스트에 존재하는 프로세스를 하나씩 실행하는 class_start 명령을 가지고 있다.
// init의 main()
// 첫번째 인자에 해당하는 섹션의 명령어들을 실행큐인 action_add_queue_tail에 저장한다
action_for_each_trigger("early-init", action_add_queue_tail);
drain_action_queue(); // 큐에 저장된 명령어를 한줄씩 실행한다
...
// 6. 프로퍼티 초기화, 부팅로고 출력 이후
action_for_each_trigger("init", action_add_queue_tail);
drain_action_queue();
...
// 8. UDS 소켓 생성 이후
action_for_each_trigger("early-boot", action_add_queue_tail);
action_for_each_trigger("boot", action_add_queue_tail);
drain_action_queue();
...
- init.rc의 내용 중 on으로 시작하는 액션리스트
부팅시 필요한 디렉터리 생성이나 특정 파일에 대한 퍼미션 지정, 시스템 환경변수 설정등의 역할을 수행한다.
- init.rc의 내용 중 service로 시작하는 서비스 리스트
부팅 시 실행하는 프로세스 리스트
- 파싱과정 상세
system/core/init/keywords.h
#ifndef KEYWORD
int do_mkdir(int nargs, char **args);
int do_chown(int nargs, char **args);
#define __MAKE_KEYWORD_ENUM__
#define KEYWORD(symbol, flags, nargs, func) K_##symbol,
enum {
K_UNKNOWN,
#endif
KEYWORD(capability, OPTION, 0, 0)
KEYWORD(mkdir, COMMAND, 1, do_mkdir)
KEYWORD(on, SECTION, 0, 0)
KEYWORD(service, SECTION, 0, 0)
KEYWORD(chown, COMMAND, 2, do_chown)
...
#ifdef __MAKE_KEYWORD_ENUM__
KEYWORD_COUNT,
};
#undef __MAKE_KEYWORD_ENUM__
#undef KEYWORD
#endif
키워드는 keywords.h의 매크로를 통해 keyword_list 구조체 배열이 만들어지는데, 키워드에 해당하는 그룹, 인자 개수, 함수가 지정되어 있다.
인자로 전달된 파일(/init.rc, /init.{hardware}.rc)의 내용을 읽은 후 문자열을 라인단위로 자른뒤 맨 앞글자를 가져와서 해당 라인이 무슨 역할을 수행하는지 파악한다.
이후 kw_is 함수를 통해 SECTION에 해당하는 키워드(on, service) 만 parse_new_section 함수를 실행하면서 on키워드는 액션리스트에, service 키워드는 서비스리스트에 등록한다.
int parse_config_file(const char *fn){
char *data;
data = read_file(fn, 0); // 인자로 전달된 파일의 내용을 읽어서 data에 저장
parse_config(fn, data);
}
static void parse_config(const char *fn, char *s){
for(;;){
switch (next_token(&state)){ // 문자열을 라인단위로 나눈다.
case T_NEWLINE:
if(nargs) {
int kw = lookup_keyword(args[0]);
if(kw_is(kw, SECTION)) // 키워드 중 섹션 그룹에 해당하는 키워드만 parse_new_section 실행
parse_new_section(*state, kw, nargs, args);
}
}
}
}
// 한줄한줄의 맨앞 글자를 통해 가져온 라인이 무슨역할을 수행하는지(키워드)를 파악하여 코드를 반환한다.
int lookup_keyword(const char *s){
switch (*s++){
...
case 'm': // 맨앞글자가 m인경우
if(!strcmp(s, "mkdir")) return K_mkdir; // mkdir인경우 int형 K_mkdir; 반환
if(!strcmp(s, "mount")) return K_mount; // mount인경우 int형 K_mount; 반환
break;
case 'o':
if(!strcmp(s, "on")) return K_on;
if(!strcmp(s, "oneshot")) return K_oneshot;
if(!strcmp(s, "onrestart")) return K_onrestart;
break;
case 's':
if(!strcmp(s, "service")) return K_service;
...
}
- 실행과정 상세
액션리스트 큐에서 맨앞 액션을 가져온 뒤 command 구조체로 변경해서 액션 하위의 command 그룹의 각 명령어에 매핑된 함수(mkdir -> do_mkdir()) 를 실행하여 각 줄에 맞는 명령을 수행한다.
static void drain_action_queue(void){
struct listnode *node;
struct command *cmd;
struct action *act;
int ret;
while((act = action_remove_queue_head())){ // 액션리스트의 head를 가져온다. (맨앞 액션)
INFO("processing action %p (%s)\n", act, act->name); // 처리중인 액션, 이름을 로그에 기록
list_for_each(node, &act->commands){
cmd = node_to_item(node, struct command, clist); // 액션을 command 구조체로 변환한다.
ret = cmd->func(cmd->nargs, cmd->args); // command구조체의 func는 명령어에 매핑된 함수이다.
}
}
}
서비스리스트의 실행은 on boot섹션의 마지막 명령어인 class_start 명령어에 매핑된 do_class_start() 를 통해 서비스리스트의 명령을 실행하게된다.
do_class_start 함수는 service_for_each_class 함수를 통해 service_start_if_not_disabled 함수를 실행시키고 service_start 함수는 execve() 시스템 콜을 통해 전달받은 서비스를 실행시킨다.
static void service_start_if_not_disabled(struct service *svc){
if (!(svc->flags & SVC_DISABLED))
service_start(svc);
}
int do_class_start(int nargs, char **args){
service_for_each_class(args[1], service_start_if_not_disabled);
return 0;
}
4-1. QEMU 장치 초기화 (AVD 에뮬레이터만)
구글에서는 개발자를 위해 SDK 배포 시 에뮬레이터를 포함시켜 안드로이드가 탑재된 실제 장치가 없어도 QEMU 에뮬레이터를 통해 애플리케이션을 실행할 수 있다. QEMU는 ARM Core로 이뤄져 ARM 명령어를 사용하며, 하드웨어 장치는 Goldfish라는 가상의 하드웨어 플랫폼에 존재한다.
- AVD (android 9.0)
- 실제 기기 (갤럭시 a8 2018 android 9.0)
5. 정적 디바이스 노드 생성
//init 의 main()
device_fd = device_init();
...
안드로이드에서는 udev 데몬의 역할을 init 프로세스가 수행한다. (9-1 참고)
/system/core/init/devices.c
// 콜드플러그 처리될 드라이버 노드파일 정보
static struct perms_ devperms[] = {
// 파일이름, 접근권한, 사용자ID, 그룹ID
{ "/dev/null", 0666, AID_ROOT, AID_ROOT, 0 },
{ "/dev/zero", 0666, AID_ROOT, AID_ROOT, 0 },
{ "/dev/full", 0666, AID_ROOT, AID_ROOT, 0 },
{ "/dev/ptmx", 0666, AID_ROOT, AID_ROOT, 0 },
{ "/dev/tty", 0666, AID_ROOT, AID_ROOT, 0 },
{ "/dev/random", 0666, AID_ROOT, AID_ROOT, 0 },
{ "/dev/urandom", 0666, AID_ROOT, AID_ROOT, 0 },
...
device_init 함수는 uevent를 수신하기 위한 소켓을 생성한 뒤 coldboot()로 do_coldboot() 함수를 호출하여 커널 부팅 시 sys 디렉토리에 등록한 드라이버에 대해 콜드플러그 처리를 한다.
do_coldboot 함수는 전달받은 경로에서 uevent 파일을 찾은 후 해당 파일에 add 메시지를 써넣어 강제로 uevent를 발생시킨다. 그 후 handle_device_fd 함수에서 uevent를 수신하고 parse_event 함수를 호출하여 uevent 메시지를 uevent 구조체에 저장한다.
int device_init(void){
fd = open_uevent_socket();
t0 = get_usecs();
coldboot(fd, "/sys/class");
coldboot(fd, "/sys/block");
coldboot(fd, "/sys/devices");
t1 = get_usecs();
log_event_print("coldboot %ld uS\n", ((long) (t1 - t0)));
}
static void do_coldboot(int event_fd, DIR *d){
fd = openat(dfd, "uevent", O_WRONLY);
if(fd >= 0){
write(fd, "add\n", 4);
close(fd);
handle_device_fd(event_fd);
}
}
void handle_device_fd(int fd){
while((n = recv(fd, msg, UEVENT_MSG_LEN, 0)) > 0){
struct uevent uevent;
parse_event(msg, &uevent);
handle_device_event(&uevent);
}
}
struct uevent {
const char *action; // "add"
const char *path; // "device/virtual/misc/binder"
const char *subsystem; // "misc"
const char *firmware;
int major; // 10
int minor; // 62
}
handle_device_event() 함수는 uevent 구조체의 subsystem을 파악하여 /sys/{서브시스템에 따라 다름}/{uevent->path} 디렉터리 경로를 생성하고 make_device() 함수에서는 미리정의된 콜드플러그 노드파일리스트인 devperms에서 경로, uid, gid를 찾은 후 mknod 함수를 호출하여 실제 디바이스 노드파일을 생성한다.
static void handle_device_event(struct uevent *uevent){
...
if(!strncmp(uevent->subsystem, "block", 5)){
block = 1;
base = "/dev/block/";
mkdir(base, 0755);
}
...
else
base = "/dev/";
if(!strcmp(uevent->action, "add")) {
make_device(devpath, block, uevent->major, uevent->minor);
}
}
static void make_device(const char *path, int block, int major, int minor){
mode = get_device_perm(path, &uid, &gid) | (block ? S_IFBLK : S_IFCHR);
dev = (major << 8) | minor;
mknod(path, mode, dev);
chown(path, uid, gid);
}
6. 프로퍼티 서비스 초기화 및 시작
모든 프로세스에서 시스템 설정값을 공유하기 위해 프로퍼티라는 이름의 저장소를 사용하는데, property_init() 함수가 호출되면서 공유메모리 영역(ASHMEM)에 생성 및 초기화 되어 모든 프로세스에서는 제공된 API인 property_get()을 이용해 프로퍼티 영역에 설정된 값을 조회할 수 있다.
값 변경은 init 프로세스에서만 가능하기 때문에 init에 property_set()으로 요청하면 공유메모리의 값을 변경해준다.
start_property_service() 함수로 몇개의 프로퍼티 설정파일을 읽어 초기화하며 다른 프로세스로부터 값 변경 요청을 받기 위한 /dev/socket/property_service 소켓을 생성하여 프로퍼티 서비스를 시작한다.
//init 의 main()
property_init();
if (!strcmp(bootmode, "factory"))
property_set("ro.factorytest", "1");
else if (!strcmp(bootmode, "factory"))
property_set("ro.factorytest", "2");
else
property_set("ro.factorytest", "0");
property_set("ro.serialno", serialno[0] ? serialno : "");
property_set("ro.bootmode", bootmode[0] ? bootmode : "unknown");
property_set("ro.baseband", baseband[0] ? baseband : "unknown");
property_set("ro.carrier", carrier[0] ? carrier : "unknown");
property_set("ro.bootloader", bootloader[0] ? bootloader : "unknown");
property_set("ro.hadware", hardware);
snprintf(tmp, PROP_VALUE_MAX, "%d", revision);
property_set("ro.revision", tmp);
property_set_fd = start_property_service();
...
property_init 함수에서 init_property_area()를 호출할 때 공유메모리 영역이 생성되고, load_properties_from_file()을 통해 default.prop파일에서 초기값을 읽어 프로퍼티 값을 설정한다.
void property_init(void){
init_property_area();
load_propertied_from_file(PROP_PATH_RAMDISK_DEFAULT);
}
- 프로퍼티 영역 구조
프로퍼티 영역은 헤더와 프로퍼티 info의 연결리스트 형태로 이뤄져있고, 각 블록마다 name = value 쌍이 저장된다.
- default.prop의 내용
7. LCD에 부팅로고 출력
#define INIT_IMAGE_FILE "/initlogo.rle"
//init 의 main()
load_565rle_image(INIT_IMAGE_FILE);
...
8. SIGCHLD 시그널 처리용 UDS 소켓 생성
socketpair() 함수는 연결된 소켓 쌍 s[2] 을 생성하고 성공한다면 signal_fd, signal_recv_fd 를 세팅한 후 이 소켓을 통해 부모와 자식 프로세스간 통신을 수행하여 SIGCHLD 시그널 수신시 자식프로세스의 종료처리 핸들러를 호출한다.
//init 의 main()
if (socketpair(AF_UNIX, SOCK_STREAM, 0, s) == 0) {
signal_fd = s[0];
signal_recv_fd = s[1];
fcntl(s[0], F_SETFD, FD_CLOEXEC);
fcntl(s[0], F_SETFL, O_NONBLOCK);
fcntl(s[1], F_SETFD, FD_CLOEXEC);
fcntl(s[1], F_SETFL, O_NONBLOCK);
}
...
9. 이벤트 처리 루프에서 감시할 이벤트 설정
init 프로세스가 감시할 파일 디스크립터와 이벤트를 지정해서 poll에 등록한다.
poll 함수가 감시하던 이벤트가 발생하여 종료된 경우에는 커널이 revents값에 events와 동일한 값을 지정할 수 있고, 비정상 종료된 경우 POLLERR(fd에 오류발생), POLLHUP(fd 연결끊김), POLLNVAL(fd 유효하지 않음) 값이 지정될 수 있다.
#define POLLIN 0x0001 // 읽을 데이터가 있는 경우
struct pollfd {
int fd; // 감시할 fd
short events; // 감시하는 이벤트. poll함수가 fd에서 발생하는지 검사하도록 할 이벤트 비트마스크
short revents; // 돌려받은 이벤트. poll함수가 반환될때 커널이 세팅해주는 이벤트 비트마스크
}
감시할 이벤트에 POLLIN을 설정하고 블록상태로(최대 타임아웃까지) 검사하다가 읽을 데이터(POLLIN 이벤트)가 발생하면 poll함수를 빠져나와 fd에 이벤트가 발생하면 각각의 처리방식으로 처리하게 된다.
//init 의 main()
// device_fd = device_init();
// property_set_fd = start_property_service();
// signal_recv_fd = s[1];
ufds[0].fd = device_fd; // 디바이스 노드 생성 이벤트 처리용
ufds[0].events = POLLIN;
ufds[1].fd = property_set_fd; // 프로퍼티 서비스 요청 이벤트 처리용
ufds[1].events = POLLIN;
ufds[2].fd = signal_recv_fd; // SIGCHLD 시그널 처리용
ufds[2].events = POLLIN;
fd_count = 3;
for(;;) {
drain_action_queue();
restart_processes();
nr = poll(ufds, fd_count, timeout);
if (ufds[2].revents == POLLIN){ // ufds[2] : SIGCHLD 이벤트 발생한 경우
read(signal_recv_fd, tmp, sizeof(tmp));
while(!wait_for_one_process(0));
continue;
}
if(ufds[0].revents == POLLIN) // ufds[0] : 안드로이드 동작 중 장치가 삽입된 경우
handle_device_fd(device_fd);
if(ufds[1].revents == POLLIN) // ufds[1] : 프로퍼티 변경 요청이 발생된 경우
handle_property_set_fd(property_set_fd);
}
...
9-1. 디바이스 노드 생성
리눅스시스템에선 디바이스 드라이버를 통해 하드웨어에 접근할 수 있다. 디바이스 노드 파일을 통해 디바이스 드라이버에 접근할 수있는데, mknod 유틸리티를 이용해 디바이스 노드를 생성할 수 있다. 하지만 안드로이드에서는 보안상의 문제로 mknod를 제공하지 않고 init 프로세스에게 요청하여 디바이스 노드를 생성할 수 있다.
리눅스커널 2.6이전에는 사용자가 직접 디바이스 노드 파일의 메이저, 마이너번호를 겹치지 않게 설정한 뒤 mknod를 통해 생성했는데, 2.6 이후부터는 udev라는 유틸리티가 데몬으로 동작하며 디바이스 드라이버가 로딩될 때 메이저, 마이너번호, 디바이스 타입을 파악해서 /dev 디렉터리에 생성하는 역할을 수행한다.
- 리눅스 시스템의 동적 디바이스 노드 생성 (핫플러그)
시스템 동작 중 usb 포트에 장치가 삽입되면 커널이 해당 장치에 대한 드라이버를 로딩한다. 드라이버는 드라이버 시작함수인 probe() 함수를 커널에의해 자동으로 호출하여 /sys 파일 시스템에 드라이버의 메이저, 마이너 번호, 디바이스 타입을 저장하고 udev 프로세스에 uevent를 발생시킨다.
* uevent : 디바이스가 추가/제거 될 때 커널이 디바이스의 정보(메이저, 마이너번호, 디바이스타입)를 사용자영역으로 전달하기 위한 메시지
udev 데몬은 uevent를 받으면 /sys 디렉터리에 등록된 디바이스 정보와 비교한 뒤 /dev 디렉터리에 디바이스 노드 파일을 생성한다.
- 리눅스 시스템의 정적 디바이스 노드 생성 (콜드플러그)
udev 데몬이 실행되기 이전부터 로딩되어있던 디바이스 드라이버는 uevent가 발생하지 않기 때문에 핫플러그 방식이 불가능한데, 부팅 시 미리 등록된 디바이스들의 초기화 함수가 /sys에 드라이버 정보를 등록하고 udev 데몬이 실행될 때 /sys 디렉토리에서 읽어들인 후 각 디바이스에 대해 uevent를 다시 발생시켜 핫플러그 방식과 같이 디바이스 노드파일을 생성한다.
- 안드로이드 콜드플러그
5. 정적 디바이스노드 생성 참고
- 안드로이드의 핫플러그
9.의 소스코드를 확인하면 poll() 함수를 통해 이벤트 처리를 등록하여 uevent를 감지하고 handle_device_fd() 함수를 호출한다. 이 함수는 5. 를 참고하면 되며, 결국엔 mknod로 디바이스 노드파일을 생성하게 된다.
9-2. SIGCHLD 시그널 처리
init은 sh(쉘), adbd(안드로이드 디버그브릿지), servicemanager(시스템 서비스 목록관리), vold(usb 스토리지나 sd카드 등 볼륨관리) 등 종료될 경우 시스템에 큰 영향을 미치는 프로세스를 실행하기 때문에 대부분 종료되더라도 init 프로세스에 의해 재시작된다.
1.에서 핸들러로 등록된 sigchld_handler() 함수는 SIGCHLD 시그널이 발생하면 인자로 SIGCHLD 시그널 번호를 받고 그 값을 signal_fd에 SIGCHLD 번호를 write한다.
signal_fd와 signal_recv_fd는 소켓쌍으로 생성되었기 때문에 poll로 감시(9.참고)중인 signal_recv_fd로 전달되며, POLLIN 상태가 되어 poll 함수는 블록상태에서 빠져나와 wait_for_one_process() 함수를 실행하게 된다.
waitpid() 함수는 시그널을 발생시킨 자식 프로세스(-1: 모든자식 프로세스를 의미)가 종료되면 해당 프로세스에 할당된 자원을 회수하며 두번째 인자인 status로 프로세스의 상태코드를 반환한다. 세번째 인자는 waitpid 함수를 블럭으로 처리할것인지 결정하며 SIGCHLD 시그널이 발생한 프로세스의 pid를 반환한다.
에러 없이 자식프로세스가 종료되어 SIGCHLD 시그널이 정상적으로 발생한 경우 while문을 빠져나올 수 있다.
종료된 프로세스가 ONESHOT(한번만 실행되고 종료되는 프로세스)이라면 재시작 하지않고 kill함수에 의해 종료되고 서비스가 정리된다. ONESHOT이 설정되지 않은 서비스 중 onrestart를 가진 프로세스는 재시작 시 명령어를 실행하고 서비스에 RESTARTING 플래그를 설정한다.
static int wait_for_one_process(int block){
...
while( (pid = waitpid(-1, &status, block ? 0 : WNOHANG)) == -1 && errno == EINTR);
svc = service_find_by_pid(pid); // 서비스 리스트에서 종료된 프로세스의 pid에 해당하는 항목을 가져온다.
if (!(svc->flags & SVC_ONESHOT)) // 서비스항목의 flag에 SVC_ONESHOT이 설정되어있는지 확인
kill(-pid, SIGKILL); // kill을 이용해 프로세스를 죽인다
for(si = svc->sockets; si; si= si->next) // 종료된 프로세스의 소켓 디스크립터를 제거(unlink)
unlink(tmp);
svc->pid = 0; // 서비스의 pid 제거
svc->flags &= (~SVC_RUNNING); // 서비스의 SVC_RUNNING(구동중인 서비스) 플래그 제거
if(svc->flags & SVC_ONESHOT) // ONESHOT이 설정되어있다면 플래그에 SVC_DISABLED를 추가
svc->flags |= SVC_DISABLED;
if(svc->flags & SVC_DISABLED) // DISABLED 설정되어있다면(ONESHOT) return 0를 통해 함수를 반환한다.
return 0;
list_for_each(node, &svc->onrestart.commands) { // 나머지 프로세스 중 onrestart(재시작시 실행할 명령어)옵션을 가진경우
cmd = node_to_item(node, struct command, clist);
cmd->func(cmd->nargs, cmd->args); // 재시작시 해당 명령어 실행
}
svc->flags |= SVC_RESTARTING; // 서비스 플래그에 SVC_RESTARTING 을 추가한다.
}
bootanim 서비스, flash_recovery서비스는 oneshot 프로세스라서 종료되어도 재실행 되지 않는다.
servicemanager 서비스는 onrestart 를 갖고 있기 때문에 종료되어 init에 의해 재시작이 된 경우 zygote등의 프로세스를 재시작하는 명령을 실행한다.
wait_for_one_process()가 종료되면 continue로 for문 시작부분으로 이동되며 거쳐 restart_process() 함수를 실행하게 된다. (9. 참고)
restart_process()는 서비스 리스트에서 SVC_RESTARTING 플래그를 가진 프로세스를 실행하는 역할을 수행한다.
static void restart_service_if_needed(struct service *svc){
svc->flags &= (~SVC_RESTARTING);
service_start(svc);
return;
}
static void restart_processes(){
process_needs_restart =0;
service_for_each_flags(SVC_RESTARTING, restart_service_if_needed);
}
9-3. 프로퍼티 변경요청 처리
6. 에서 프로퍼티를 초기화할 때 start_property_service() 함수를 호출한다.
각 프로퍼티 파일에 설정된 기본값을 읽어 프로퍼티 값을 설정한 후 /dev/socket/property_service 라는 이름의 유닉스 도메인 소켓을 생성 후 반환한다. (이 소켓을 poll 함수가 감시한다)
int start_property_service(void){
int fd;
load_properties_from_file(PROP_PATH_SYSTEM_BUILD);
load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);
load_properties_from_file(PROP_PATH_LOCAL_OVERRIDE);
load_persistent_properties(); // /data/property 디렉터리 하위의 프로퍼티 값을 로드
fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0);
if(fd < 0) return -1;
fcntl(fd, F_SETFD, FD_CLOEXEC);
fcntl(fd, F_SETFL, O_NONBLOCK);
listen(fd, 8);
return fd;
}
/data/property 디렉터리의 프로퍼티 값은 시스템 동작중 다른 프로세스에 의해 생성된 프로퍼티 값이나 변경된 프로퍼티 값들이 존재한다. key는 파일이름으로 사용되고 value는 파일 내용으로 저장된다.
poll 함수가 감시하고 있다가 프로퍼티 메시지가 수신되면 handle_property_set_fd() 함수가 호출된다. (9. 참고)
프로퍼티 메시지를 보낸 프로세스의 접근 권한을 확인하기 위해 SO_PEERCRED를 요청하여 프로세스의 uid, gid, pid 값을 cr (struct ucred)에 채워넣는다.
ctl로 시작하는 프로퍼티 메시지는 프로세스 종료 시작 관련 프로퍼티 메시지인데 system server, root, owner 만 사용할 수 있기 때문에 check_control_perms() 함수 호출을 통해 cr.uid를 기반으로 ctl 프로퍼티 메시지에 대한 권한을 가지고있는지 검사하고 실행시켜준다.
그 외의 메시지는 시스템의 프로퍼티를 변경하는데 사용되기 때문에 check_perms() 함수를 통해 권한을 확인 후 property_set() 함수로 프로퍼티 변경 요청을 처리한다.
void handle_property_set_fd(int fd){
...
if(getsocketopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0){
close(s);
ERROR("Unable to receive socket options\n");
return;
}
...
switch(msg.cmd {
case PROP_MSG_SETPROP:
...
if(memcmp(msg.name, "ctl.", 4) == 0) {
if(check_control_perms(msg.value, cr.uid))
handle_control_message((char*)msg.name + 4, (char*)msg.vlaue);
...
} else {
if (check_perms(msg.name, cr.uid))
property_set((char*)msg.name, (char*)msg.value);
...
}
...
}
init.rc에는 프로퍼티가 변경됨에 따라 수행할 동작이 정의되어 있다.
- ro.debuggable 프로퍼티 값이 1이 세팅된 경우 start console 명령 실행
property_set을 통해 프로퍼티 값이 정상적으로 변경되었다면 property_changed() 함수가 호출되어 프로퍼티가 변경됨을 알려 init.rc에서 프로퍼티 변경에 따른 동작을 수행하도록 할 수 있다.
- 프로퍼티 메시지 권한 (property_service.c)
struct {
const char *prefix;
unsigned int uid;
unsigned int gid;
} property_perms[] = {
{ "net.rmnet0.", AID_RADIO, 0 },
{ "net.gprs.", AID_RADIO, 0 },
{ "net.ppp", AID_RADIO, 0 },
{ "net.qmi", AID_RADIO, 0 },
{ "net.lte", AID_RADIO, 0 },
{ "net.cdma", AID_RADIO, 0 },
...
{ "persist.service.", AID_SYSTEM, 0 },
{ "persist.security.", AID_SYSTEM, 0 },
{ "persist.service.bdroid.", AID_BLUETOOTH, 0 },
{ "selinux." , AID_SYSTEM, 0 },
{ NULL, 0, 0 }
};