Linux Process
task_struct
/linux.x.x.x/include/linux/sched.h
- struct task_struct
-
{
/*
* offsets of these are hardcoded elsewhere - touch with care
*/
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
unsigned long flags; /* per process flags, defined below */
int sigpending;
mm_segment_t addr_limit; /* thread address space:
0-0xBFFFFFFF for user-thead
0-0xFFFFFFFF for kernel-thread*/
struct exec_domain *exec_domain;
volatile long need_resched;
unsigned long ptrace;
int lock_depth; /* Lock depth */
/*
* offset 32 begins here on 32-bit platforms. We keep
* all fields in a single cacheline that are needed for
* the goodness() loop in schedule().
*/
long counter;
long nice;
unsigned long policy;
struct mm_struct *mm;
int processor;
/*
* cpus_runnable is ~0 if the process is not running on any
* CPU. It's (1 << cpu) if it's running on a CPU. This mask
* is updated under the runqueue lock.
*
* To determine whether a process might run on a CPU, this
* mask is AND-ed with cpus_allowed.
*/
unsigned long cpus_runnable, cpus_allowed;
/*
* (only the 'next' pointer fits into the cacheline, but
* that's just fine.)
*/
struct list_head run_list;
unsigned long sleep_time;
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct list_head local_pages;
unsigned int allocation_order, nr_local_pages;
/* task state */
struct linux_binfmt *binfmt;
int exit_code, exit_signal;
int pdeath_signal; /* The signal sent when the parent dies */
/* ??? */
unsigned long personality;
int did_exec:1;
unsigned task_dumpable:1;
pid_t pid;
pid_t pgrp;/* tty_old_pgrp는 session의 리더가 자신과 연관된 터미널을 제거할 때 터미널의 session과 관련된
* 프로세스 그룹의 리더들이 자신이 어떤 터미널에 연관되어 있었냐를 기억하기 위해서 사용한다.tgid는
* thread의 그룹 ID값을 나타낸다.*/
pid_t tty_old_pgrp;
pid_t session;
pid_t tgid;
/* boolean value for session group leader */
int leader;
/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->p_pptr->pid)
*/
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
struct list_head thread_group;
/* PID hash table linkage. */
struct task_struct *pidhash_next;
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; /* for wait4() */
struct completion *vfork_done; /* for vfork() */
unsigned long rt_priority;
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_incr;
struct timer_list real_timer;
struct tms times;
unsigned long start_time;
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1;
/* process credentials */
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups;
gid_t groups[NGROUPS];
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:1;
struct user_struct *user;
/* limits */
struct rlimit rlim[RLIM_NLIMITS];
unsigned short used_math;
char comm[16];
/* file system info */
int link_count, total_link_count;
struct tty_struct *tty; /* NULL if no tty */
unsigned int locks; /* How many file locks are being held */
/* ipc stuff *//*semundo와 semsleeping은 Process가 semaphore자원을 사용하려 할때 사용된다.
*semsleeping은 현재 수면상태에 있는 프로세스들의 세마포어 대기 큐를 나타낸다.
*semundo는 세마포어의 list를 나타낸다.*/
struct sem_undo *semundo;
struct sem_queue *semsleeping;
/* CPU-specific state of this task *//* thread는 processor의 register값을 보관*/
struct thread_struct thread;
/* filesystem information *//* 파일 접근시 확인할 정보 */
struct fs_struct *fs;
/* open file information *//* 프로세스에 의해 열려진 파일 기술자들에 대한 포인터*/
struct files_struct *files;
/* namespace */
struct namespace *namespace;
/* signal handlers */
spinlock_t sigmask_lock; /* Protects signal and blocked */
struct signal_struct *sig;
sigset_t blocked;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);void *notifier_data;
sigset_t *notifier_mask;
/* Thread group tracking *//* 부모의 exec_id와 자신의 exec_id. fork()시에는 부모의 self_exec_id
- * 를 생성되는 프로세스의 parent_exec_id를 받게 되며 exit()시에 이곳에
- * 저장된 값들을 확인해서 어떤 시그널들을 전달해 줄 지를 결정하게 된다.
- */
u32 parent_exec_id;
u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty */ -
/* 메모리 및 파일, 혹은 파일시스템이나 터미널에 대한 할당 및
- * 제거 시에 사용되는 일종의 lock 역할
- */
spinlock_t alloc_lock;
/* journalling filesystem info */
void *journal_info;
#ifdef CONFIG_SYSCALLTIMER
int curr_syscall;
#endif
- };
[리눅스 프로세스 간 관계 표현도]
자식 프로세스들간에는 생성된 순서에 따라서 p_osptr과 p_ysptr이 자신보다 오래된 것가 자신보다 늦게 생성된 자식프로세스를 가리킨다. 또한 모든 자식 프로세스들은 p_pptr로 자신의 부모프로세스를 가리키도록 한다.
또한 모든 프로세스들은 init_task라는 전역변수에 의해서 2중으로 연결된 구조를 가지게 되며, 이 연결리스트를 이용해서 각 프로세스들에 대한 접근이 가능하다. 때때로 이렇게 연결된 전체list를 다 훑어보는 것이 필요하게 되며, 예로는 priority를 새로 계산하거나 현재 system내의 모든 process를 보여줘야 할 때 사용될 수 있다.
init 프로세스는 system이 booting할때 인위적으로 만들어지며 system이 돌아가는 중에는 종료되지 않는 프로세스이다. init프로세스는 kernel thread로 생성되며 process id는 0이다. 이 프로세스는 /init/main.c에 있다.
/linux.x.x.x/init/main.c
- /*
* linux/init/main.c
*
* Copyright (C) 1991, 1992 Linus Torvalds
*
* GK 2/5/95 - Changed to support mounting root fs via NFS
* Added initrd & change_root: Werner Almesberger & Hans Lermen, Feb '96
* Moan early if gcc is old, avoiding bogus kernels - Paul Gortmaker, May '96
* Simplified starting of init: Michael A. Griffith <grif@acm.org>
*/
#define __KERNEL_SYSCALLS__
#include <linux/config.h>
#include <linux/proc_fs.h>
#include <linux/devfs_fs_kernel.h>
#include <linux/unistd.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/delay.h>
#include <linux/utsname.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/smp_lock.h>
#include <linux/blk.h>
#include <linux/hdreg.h>
#include <linux/iobuf.h>
#include <linux/bootmem.h>
#include <linux/file.h>
#include <linux/tty.h>
#include <asm/io.h>
#include <asm/bugs.h>
#if defined(CONFIG_ARCH_S390)
#include <asm/s390mach.h>
#include <asm/ccwcache.h>
#endif
#ifdef CONFIG_ACPI
#include <linux/acpi.h>
#endif
#ifdef CONFIG_PCI
#include <linux/pci.h>
#endif
#ifdef CONFIG_DIO
#include <linux/dio.h>
#endif
#ifdef CONFIG_ZORRO
#include <linux/zorro.h>
#endif
#ifdef CONFIG_MTRR
# include <asm/mtrr.h>
#endif
#ifdef CONFIG_NUBUS
#include <linux/nubus.h>
#endif
#ifdef CONFIG_ISAPNP
#include <linux/isapnp.h>
#endif
#ifdef CONFIG_IRDA
extern int irda_proto_init(void);
extern int irda_device_init(void);
#endif
#ifdef CONFIG_X86_LOCAL_APIC
#include <asm/smp.h>
#endif
/*
* Versions of gcc older than that listed below may actually compile
* and link okay, but the end product can have subtle run time bugs.
* To avoid associated bogus bug reports, we flatly refuse to compile
* with a gcc that is known to be too old from the very beginning.
*/
#if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 91)
#if !defined(CONFIG_V850E) && !defined(CONFIG_NIOS)
#error Sorry, your GCC is too old. It builds incorrect kernels.
#else
#warning Sorry, your GCC is too old. It builds incorrect kernels.
#endif
#endif
#ifdef CONFIG_FRV
extern const char _stext[], _etext[];
#else
extern const char _stext, _etext;
#endif
extern char linux_banner[];
static int init(void *);
extern void init_IRQ(void);
extern void init_modules(void);
extern void sock_init(void);
extern void fork_init(unsigned long);
extern void mca_init(void);
extern void sbus_init(void);
extern void ppc_init(void);
extern void sysctl_init(void);
extern void signals_init(void);
extern int init_pcmcia_ds(void);
extern void free_initmem(void);
#ifdef CONFIG_ACPI_BUS
extern void acpi_early_init(void);
#else
static inline void acpi_early_init(void) { }
#endif
#ifdef CONFIG_TC
extern void tc_init(void);
#endif
extern void ecard_init(void);
#if defined(CONFIG_SYSVIPC)
extern void ipc_init(void);
#endif
/*
* Boot command-line arguments
*/
#define MAX_INIT_ARGS 8
#define MAX_INIT_ENVS 8
extern void time_init(void);
extern void softirq_init(void);
int rows, cols;
char *execute_command;
static char * argv_init[MAX_INIT_ARGS+2] = { "init", NULL, };
char * envp_init[MAX_INIT_ENVS+2] = { "HOME=/", "TERM=linux", NULL, };
static int __init profile_setup(char *str)
{
int par;
if (get_option(&str,&par)) prof_shift = par;
return 1;
}
__setup("profile=", profile_setup);
static int __init checksetup(char *line)
{
struct kernel_param *p;
p = __setup_start;
do {
int n = strlen(p->str);
if (!strncmp(line,p->str,n)) {
if (p->setup_func(line+n))
return 1;
}
p++;
} while (p < __setup_end);
return 0;
}
/* this should be approx 2 Bo*oMips to start (note initial shift), and will
still work even if initially too large, it will just take slightly longer */
unsigned long loops_per_jiffy = (1<<12);
/* This is the number of bits of precision for the loops_per_jiffy. Each
bit takes on average 1.5/HZ seconds. This (like the original) is a little
better than 1% */
#define LPS_PREC 8
#ifndef CONFIG_FRV
void __init calibrate_delay(void)
{
unsigned long ticks, loopbit;
int lps_precision = LPS_PREC;
#ifdef FIXED_BOGOMIPS
int bogus;
/* FIXED_BOGOMIPS converted to __delay units. */
#define FIXED_LOOPS_PER_JIFFY (unsigned long)(FIXED_BOGOMIPS * (500000 / HZ))
/* The maximum error in FIXED_LOOPS_PER_JIFFY that we will tolerate. */
#define FIXED_LPJ_TOLERANCE (unsigned long)(FIXED_LOOPS_PER_JIFFY * 0.10)
/* Make sure fixed delay - T% is zero ticks. */
ticks = jiffies;
while (ticks == jiffies) /* Synchronize with start of tick */
/* nothing */;
ticks = jiffies;
__delay(FIXED_LOOPS_PER_JIFFY - FIXED_LPJ_TOLERANCE);
bogus = (jiffies != ticks);
if (! bogus) {
/* Make sure fixed delay + T% is one tick. The delay here
is very short because we're actually continuing timing from
the tick synchronization above (we don't resynchronize). */
__delay(2 * FIXED_LPJ_TOLERANCE);
bogus = (jiffies != ticks + 1);
}
if (! bogus) {
/* Use the precomputed value. */
loops_per_jiffy = FIXED_LOOPS_PER_JIFFY;
printk("Delay loop constant: %lu.%02lu BogoMIPS (precomputed)\n",
(unsigned long)FIXED_BOGOMIPS,
((unsigned long)(FIXED_BOGOMIPS * 100)) % 100);
return;
} else {
printk("Precomputed BogoMIPS value (%lu.%02lu) inaccurate!\n",
(unsigned long)FIXED_BOGOMIPS,
((unsigned long)(FIXED_BOGOMIPS * 100)) % 100);
/* ... and fall through to normal bogomips calculation. */
}
#endif /* FIXED_BOGOMIPS */
loops_per_jiffy = (1<<12);
printk("Calibrating delay loop... ");
while (loops_per_jiffy <<= 1) {
/* wait for "start of" clock tick */
ticks = jiffies;
while (ticks == jiffies)
/* nothing */;
/* Go .. */
ticks = jiffies;
__delay(loops_per_jiffy);
ticks = jiffies - ticks;
if (ticks)
break;
}
/* Do a binary approximation to get loops_per_jiffy set to equal one clock
(up to lps_precision bits) */
loops_per_jiffy >>= 1;
loopbit = loops_per_jiffy;
while ( lps_precision-- && (loopbit >>= 1) ) {
loops_per_jiffy |= loopbit;
ticks = jiffies;
while (ticks == jiffies);
ticks = jiffies;
__delay(loops_per_jiffy);
if (jiffies != ticks) /* longer than 1 tick */
loops_per_jiffy &= ~loopbit;
}
/* Round the value and print it */
printk("%lu.%02lu BogoMIPS\n",
loops_per_jiffy/(500000/HZ),
(loops_per_jiffy/(5000/HZ)) % 100);
}
#endif
static int __init debug_kernel(char *str)
{
if (*str)
return 0;
console_loglevel = 10;
return 1;
}
static int __init quiet_kernel(char *str)
{
if (*str)
return 0;
console_loglevel = 4;
return 1;
}
__setup("debug", debug_kernel);
__setup("quiet", quiet_kernel);
/*
* This is a simple kernel command line parsing function: it parses
* the command line, and fills in the arguments/environment to init
* as appropriate. Any cmd-line option is taken to be an environment
* variable if it contains the character '='.
*
* This routine also checks for options meant for the kernel.
* These options are not given to init - they are for internal kernel use only.
*/
static void __init parse_options(char *line)
{
char *next,*quote;
int args, envs;
if (!*line)
return;
args = 0;
envs = 1; /* TERM is set to 'linux' by default */
next = line;
while ((line = next) != NULL) - {
quote = strchr(line,'"');
next = strchr(line, ' ');
while (next != NULL && quote != NULL && quote < next) {
/* we found a left quote before the next blank
* now we have to find the matching right quote
*/
next = strchr(quote+1, '"');
if (next != NULL) {
quote = strchr(next+1, '"');
next = strchr(next+1, ' ');
}
}
if (next != NULL)
*next++ = 0;
if (!strncmp(line,"init=",5)) - {
line += 5;
execute_command = line;
/* In case LILO is going to boot us with default command line,
* it prepends "auto" before the whole cmdline which makes
* the shell think it should execute a script with such name.
* So we ignore all arguments entered _before_ init=... [MJ]
*/
args = 0;
continue;
}
if (checksetup(line))
continue;
/*
* Then check if it's an environment variable or
* an option.
*/
if (strchr(line,'=')) {
if (envs >= MAX_INIT_ENVS)
break;
envp_init[++envs] = line;
} else {
if (args >= MAX_INIT_ARGS)
break;
if (*line)
argv_init[++args] = line;
}
}
argv_init[args+1] = NULL;
envp_init[envs+1] = NULL;
}
extern void setup_arch(char **);
extern void cpu_idle(void);
unsigned long wait_init_idle;
#ifndef CONFIG_SMP
#ifdef CONFIG_X86_LOCAL_APIC
static void __init smp_init(void)
{
APIC_init_uniprocessor();
}
#else
#define smp_init() do { } while (0)
#endif
#else
/* Called by boot processor to activate the rest. */
static void __init smp_init(void)
{
/* Get other processors into their bootup holding patterns. */
smp_boot_cpus();
wait_init_idle = cpu_online_map;
clear_bit(current->processor, &wait_init_idle); /* Don't wait on me! */
smp_threads_ready=1;
smp_commence();
/* Wait for the other cpus to set up their idle processes */
printk("Waiting on wait_init_idle (map = 0x%lx)\n", wait_init_idle);
while (wait_init_idle) {
cpu_relax();
barrier();
}
printk("All processors have done init_idle\n");
}
#endif
/*
* We need to finalize in a non-__init function or else race conditions
* between the root thread and the init thread may cause start_kernel to
* be reaped by free_initmem before the root thread has proceeded to
* cpu_idle.
*/
static void rest_init(void)
{
kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
unlock_kernel();
current->need_resched = 1;
cpu_idle();
}
/*
* Activate the first processor.
*/
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern char saved_command_line[];
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
lock_kernel();
printk(linux_banner);
setup_arch(&command_line);
printk("Kernel command line: %s\n", saved_command_line);
parse_options(command_line);
trap_init();
init_IRQ();
sched_init();
softirq_init();
time_init();
/*
* HACK ALERT! This is early. We're enabling the console before
* we've done PCI setups etc, and console_init() must be aware of
* this. But we do want output early, in case something goes wrong.
*/
console_init();
#ifdef CONFIG_MODULES
init_modules();
#endif
if (prof_shift) {
unsigned int size;
/* only text is profiled */
prof_len = (unsigned long) &_etext - (unsigned long) &_stext;
prof_len >>= prof_shift;
size = prof_len * sizeof(unsigned int) + PAGE_SIZE-1;
prof_buffer = (unsigned int *) alloc_bootmem(size);
}
kmem_cache_init();
sti();
calibrate_delay();
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok &&
initrd_start < min_low_pfn << PAGE_SHIFT) {
printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
"disabling it.\n",initrd_start,min_low_pfn << PAGE_SHIFT);
initrd_start = 0;
}
#endif
mem_init();
kmem_cache_sizes_init();
pgtable_cache_init();
/*
* For architectures that have highmem, num_mappedpages represents
* the amount of memory the kernel can use. For other architectures
* it's the same as the total pages. We need both numbers because
* some subsystems need to initialize based on how much memory the
* kernel can use.
*/
if (num_mappedpages == 0)
num_mappedpages = num_physpages;
fork_init(num_mappedpages);
proc_caches_init();
vfs_caches_init(num_physpages);
buffer_init(num_physpages);
page_cache_init(num_physpages);
#if defined(CONFIG_ARCH_S390)
ccwcache_init();
#endif
signals_init();
#ifdef CONFIG_PROC_FS
proc_root_init();
#endif
check_bugs();
acpi_early_init(); /* before LAPIC and SMP init */
printk("POSIX conformance testing by UNIFIX\n");
/*
* We count on the initial thread going ok
* Like idlers init is an unlocked kernel thread, which will
* make syscalls (and thus be locked).
*/
smp_init();
#if defined(CONFIG_SYSVIPC)
ipc_init();
#endif
rest_init();
}
struct task_struct *child_reaper = &init_task;
static void __init do_initcalls(void)
{
initcall_t *call;
call = __initcall_start;
do {
(*call)();
call++;
} while (call < __initcall_end);
/* Make sure there is no pending stuff from the initcall sequence */
flush_scheduled_tasks();
}
/*
* Ok, the machine is now initialized. None of the devices
* have been touched yet, but the CPU subsystem is up and
* running, and memory and process management works.
*
* Now we can finally start doing some real work..
*/
static void __init do_basic_setup(void)
{
/*
* Tell the world that we're going to be the grim
* reaper of innocent orphaned children.
*
* We don't want people to have to make incorrect
* assumptions about where in the task array this
* can be found.
*/
child_reaper = current;
#if defined(CONFIG_MTRR) /* Do this after SMP initialization */
/*
* We should probably create some architecture-dependent "fixup after
* everything is up" style function where this would belong better
* than in init/main.c..
*/
mtrr_init();
#endif
#ifdef CONFIG_SYSCTL
sysctl_init();
#endif
/*
* Ok, at this point all CPU's should be initialized, so
* we can start looking into devices..
*/
#if defined(CONFIG_ARCH_S390)
s390_init_machine_check();
#endif
#ifdef CONFIG_ACPI_INTERPRETER
acpi_init();
#endif
#ifdef CONFIG_PCI
pci_init();
#endif
#ifdef CONFIG_SBUS
sbus_init();
#endif
#if defined(CONFIG_PPC)
ppc_init();
#endif
#ifdef CONFIG_MCA
mca_init();
#endif
#ifdef CONFIG_ARCH_ACORN
ecard_init();
#endif
#ifdef CONFIG_ZORRO
zorro_init();
#endif
#ifdef CONFIG_DIO
dio_init();
#endif
#ifdef CONFIG_NUBUS
nubus_init();
#endif
#ifdef CONFIG_ISAPNP
isapnp_init();
#endif
#ifdef CONFIG_TC
tc_init();
#endif
/* Networking initialization needs a process context */
sock_init();
start_context_thread();
do_initcalls();
#ifdef CONFIG_IRDA
irda_proto_init();
irda_device_init(); /* Must be done after protocol initialization */
#endif
#ifdef CONFIG_PCMCIA
init_pcmcia_ds(); /* Do this last */
#endif
}
static void run_init_process(char *init_filename)
{
argv_init[0] = init_filename;
execve(init_filename, argv_init, envp_init);
}
extern void prepare_namespace(void);
static int init(void * unused)
{
struct files_struct *files;
lock_kernel();
do_basic_setup();
prepare_namespace();
/*
* Ok, we have completed the initial bootup, and
* we're essentially up and running. Get rid of the
* initmem segments and start the user-mode stuff..
*/
free_initmem();
unlock_kernel();
/*
* Right now we are a thread sharing with a ton of kernel
* stuff. We don't want to end up in user space in that state
*/
files = current->files;
if(unshare_files())
panic("unshare");
put_files_struct(files);
if (open("/dev/console", O_RDWR, 0) < 0)
printk("Warning: unable to open an initial console.\n");
(void) dup(0);
(void) dup(0);
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command)
run_init_process(execute_command);- //위 세개는 init 실행 정 안되면 쉘이라도 뛰운다.
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh"); - //아래 코드는 실행될리가 없으므로 panic이 나야 마땅...(run_init_process내부에서 execve로 변신하므로)
panic("No init found. Try passing init= option to kernel.");
}
init process는 기본적인 초기화 작업을 수행한 후 /dev/console을 open한 다음 disk에서 init프로그램을 읽어와서 실행하는 역할을 한다. 이후에는 단순히 cpu를 소모하는 역할만을 하게 된다.
- cpu_idle();
이와 같은 것이 필요한 이유는 실행 가능한 process가 존재하지 않을 때를 대비한 것이라고 보면 된다. 즉 아무 일도 하지 않으면서 어떤 event가생기기를 기다리던지 아니면 어떤 프로세스가 실행가능하게 되기를 기다리는 것이다 만약 새로운 process가 실행가능하게 된다면 그프로세스로 제어가 넘어가서 실행될 것이다.
프로세스들은 여러 개가 묶여서 프로세스 그룹을 이룬다. 프로세스는 고유한 id로 pid를 운영체제로부터 할당받게 되며, 속한 그룹의 값으로는 pgrp를 가진다. 또한 프로세스는 session에 속하게 되며, session에는 leader가 되는 프로세스가 존재하게 된다.
process group이란 여러 프로세스를 묶어서 만든 것으로 커널에서 그룹에 속한 프로세스들에게 어떤 작용을 가하기 위해 만든것이다. 일반적으로 프로세스들은 pgrp값을 부모로부터 상속하며 그룹마다 login,terminal(이것을 controlling terminal이라고 한다.)을 하나씩 가지고 있고 /dev/tty file에 관련된다. 나아가 여러개의 그룹을 묶어서 Session을 만들며, 모든 세션은 리더를 가진다. 리더가 되는 프로세스는 자신의 pid로 pgid와 session값을 초기화하게 된다.
Process의 상태
TASK_RUNNING
TASK_INTERRUPTIBLE
TASK_UNINTERRUPTIBLE
task가 현재 signal에 대해서 interrupt될 수 있냐 없냐를 구분한다.
TASK_ZOMBIE
process가 종료할 때 부모에 의해서 지워지기 전의 상태에 있는 process이다. 이상태에서 가지는 process의 자원은 일반적인 UNIX에서는 proc 구조체뿐이다. 이미 memory를해제한 상태이며 부모에 알려줄 정보만을 가지고 있는 상태이다. 가지는 정보는 exit status와 자원사용 정보등이 있으며 부모process는 wait()을 호출해서 이것을가져오게 된다. 만약 자식process가 종료하기 전에 부모process가 먼저 종료하게 되면 자식 process를 init process가 넘겨받게 되며 init이 wait을 호출하여 자식process가 가진 proc구조체를 해제하게 된다.
TASK_STOPPED
TASK_EXCLUSIVE
Process Context
Process Context 구성요소 :
사용자 주소공간 :
제어 정보 :
process에 대한 정보를 kernel에서 관리하기위해 있는것으로 보통의 UNIX의 경우에는 u area와 proc(리눅스에서는 task_struct)를 들수있다.
credentials :
이것은 process와 연관된 보안을 유지하기 위해 필요한 정보로 user id나 group id등이 있다.
환경 변수 :
프로그램이 수행되기 위해 필요한 것들을 주로 command라인 상에서 변수에 값을 입력하는 방식으로 전달된다.
hardware context
hardware register들에 대한 내용으로 general purpose register와 system에서 사용하는 특별한 register들이다.(PC , IP , Stack Pointer ,PSW, EFLAGS ... )
이들 중에 새로운 process로 환경전환시 필요한 것들로는 주로 hardware context이며 새로운 process로의 전환 이전에 PCB라는 구조에 저장될 필요가 있다. 나중에 다시 schedule이 되어서 실행되어야 할 경우 이곳에 있는 값들이 각각의 register로 새로이 읽혀들어오게 된다. 새로운 process로의 실행변경은 register들을 읽어서 복구한 후, PC로 long jump하면 될 것이다.
또한 여기서 중요한 것은 interrupt가 발생했을 경우에 어떤 context 하에서 실행되는가이다. interrupt는 asynchronous하게 발생되는데, interrupt하에서는 현재의 process가 요청한 결과에 대한 것인지, 아니면 어떤 process가 요청을 기다리고 있는지를 판단할 수 없다. 따라서 system programmer의 입장에서는 interrupt시에도 자신의 process context를 가지고 interrupt를 처리하고 있다고 봐서는 안 될 것이다.
Process실행 모드
User Mode , Kernel Mode
Process Scheduling
SCHED_FIFO
우선순위의 변화가 불가능한 실시간 프로세스
SCHED_RR
우선순위 변화가능한 실시간 프로세스
SCHED_OTHER
일반적인 time-sharing 프로세스
Linux Process Scheduling Algorithm
이러한 multi-level feedback queue를 사용하는 경우 시간의 경과에 따라서 process들은 자신이 CPU를 사용한 만큼씩 priority를 감소시켜나가면서 priority queue에서 자리를 이동하게 된다. 따라서 CPU의 사용을 많이 하는 process일수록 priority가 낮아지게 되면서 다른 process가 실행될 수 있는 기회를 만들어준다.
리눅스는 전체 process를 선형적으로 찾아나가면서 priority를 계산하고, 그 중에서 실행가능한 가장 높은 priority를 가지는 process를 선택해서 실행시켜준다. 각각의 process는 priority와 연관된 field로 counter라는 field를 가지고 있으며, 이 값의 매 scheduling요구마다 새롭게 계산된다.
/*
* 'schedule()' is the scheduler function. It's a very simple and nice
* scheduler: it's not perfect, but certainly works for most things.
*
* The goto is "interesting".
*
* NOTE!! Task 0 is the 'idle' task, which gets called when no other
* tasks can run. It can not be killed, and it cannot sleep. The 'state'
* information in task[0] is never used.
*/
asmlinkage void schedule(void)
{
struct schedule_data * sched_data;
struct task_struct *prev, *next, *p;
struct list_head *tmp;
int this_cpu, c;
/*먼저 task_queue에 등록된 모든 routine들을 처리한다. 이는 bottom half로 주어진 것으로 빠른 처리를 요하지는 않아서 interrupt handle에서 나누어진 부분들이다 scheduler에서는 process를 scheduling하기 전에 이렇게 등록된 routine들을 처리함으로서 기다리고 있을지도 모를 process들을 실행가능 상태로 바꾼다.*/
spin_lock_prefetch(&runqueue_lock);
BUG_ON(!current->active_mm);
need_resched_back:
prev = current;
this_cpu = prev->processor;
if (unlikely(in_interrupt())) {
printk("Scheduling in interrupt\n");
BUG();
}
release_kernel_lock(prev, this_cpu);
/*
* 'sched_data' is protected by the fact that we can run
* only one process per CPU.
*/
sched_data = & aligned_data[this_cpu].schedule_data;
spin_lock_irq(&runqueue_lock);
/* move an exhausted RR process to be last.. */
if (unlikely(prev->policy == SCHED_RR))
if (!prev->counter) {
prev->counter = NICE_TO_TICKS(prev->nice);
move_last_runqueue(prev);
}
switch (prev->state) {
case TASK_INTERRUPTIBLE:
if (signal_pending(prev)) {
prev->state = TASK_RUNNING;
break;
}
default:
del_from_runqueue(prev);
case TASK_RUNNING:;
}
prev->need_resched = 0;
/*
* this is the scheduler proper:
*/
/*- 현재의 cpu에서 idle task로 돌고 있는 process를 선택한 다음, process들의 우선순위를 결정하는 계산에 들어간다. 이와 같은 계산은 goodness()에서 일어난다. 먼저 process가 가장 최근에 수행된 cpu와 현재의 cpu가 같을 경우 그 process의 weight를 높여준다.그리고 mm에 대한 advantabe를 준 다음, nice값에 대한 조정을 한다. 실시간 process들에 대해서는 1000을 더한 값을 advantage로 준다. 이 계산에서 주어지는 weight값은 process가 다음에 선택되어 실행할 수 있느냐를 따지는 것으로, 큰 값을 가질수록 다음에 선택되어 실행될 가능성이 많다고 보면 된다.
- */
repeat_schedule:
/*
* Default process to select..
*/
next = idle_task(this_cpu);
c = -1000;
list_for_each(tmp, &runqueue_head) {
p = list_entry(tmp, struct task_struct, run_list);
if (can_schedule(p, this_cpu)) {
int weight = goodness(p, this_cpu, prev->active_mm);
if (weight > c)
c = weight, next = p;
}
}
/* Do we need to re-calculate counters? */
if (unlikely(!c)) {
struct task_struct *p;
spin_unlock_irq(&runqueue_lock);
read_lock(&tasklist_lock);
for_each_task(p)
p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
read_unlock(&tasklist_lock);
spin_lock_irq(&runqueue_lock);
goto repeat_schedule;
}
/*- *가장 큰 계산된 goodness()의 weight값을 지니는 process를 찾았다. 따라서 다음 번에 수행할 procss를 찾은것이다. 따라서 이 process로 진행하기 위한 정보들을 save하자. 만약 이전에 수행하던 process가 선택되었다고 해도,process가 rescheduling을 필요로 한다면, 다른 process를 선택하도록 해준다. 그리고 나서 SMP machine일 경우에는 마지막에*scheduling이 일어난 시점을 저장해둔다.
- */
/*
* from this point on nothing can prevent us from
* switching to the next task, save this fact in
* sched_data.
*/
sched_data->curr = next;
task_set_cpu(next, this_cpu);
spin_unlock_irq(&runqueue_lock);
if (unlikely(prev == next)) {
/* We won't go through the normal tail, so do this by hand */
prev->policy &= ~SCHED_YIELD;
goto same_process;
}
#ifdef CONFIG_SMP
/*
* maintain the per-process 'last schedule' value.
* (this has to be recalculated even if we reschedule to
* the same process) Currently this is only used on SMP,
* and it's approximate, so we do not have to maintain
* it while holding the runqueue spinlock.
*/- /*smp machine일 경우에는 마지막에 scheduling이 일어난 시점을 저장해둔다.*/
sched_data->last_schedule = get_cycles();
/*
* We drop the scheduler lock early (it's a global spinlock),
* thus we have to lock the previous process from getting
* rescheduled during switch_to().
*/
#endif /* CONFIG_SMP */
/*이제는 context switching하는 일만 남았다. 먼저 mm을 switch하고 register와 stack에 대한 switching을 한다. 그리고 이전의 process를 queue의 끝으로 보낸다.- */
kstat.context_swtch++;
/*
* there are 3 processes which are affected by a context switch:
*
* prev == .... ==> (last => next)
*
* It's the 'much more previous' 'prev' that is on next's stack,
* but prev is set to (the just run) 'last' process by switch_to().
* This might sound slightly confusing but makes tons of sense.
*/
prepare_to_switch();
{
struct mm_struct *mm = next->mm;
struct mm_struct *oldmm = prev->active_mm;
if (!mm) {
BUG_ON(next->active_mm);
BUG_ON(!oldmm);
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next, this_cpu);
} else {
BUG_ON(next->active_mm != mm);
switch_mm(oldmm, mm, next, this_cpu);
}
if (!prev->mm) {
prev->active_mm = NULL;
mmdrop(oldmm);
}
}
/*
* This just switches the register state and the
* stack.
*/
#ifdef CONFIG_SYSCALLTIMER
timepeg_schedule_switchout();
#endif
switch_to(prev, next, prev);
#ifdef CONFIG_SYSCALLTIMER
timepeg_schedule_switchin();
#endif
__schedule_tail(prev);
same_process:
reacquire_kernel_lock(current);
if (current->need_resched)
goto need_resched_back;
return;
}
schedule()함수는 전체 process중에서 우선순위가 가장 높은 프로세스를 찾아서 이 프로세스로 context switching을 한다. 또한 이 함수에서 그러한 우선 순위 값에 영향을 주는 요소로는 goodness()값과 nice값이 있으며 이 값을 적절히 활용해서 최상의 우선 순위를 가지는 프로세스를 찾는 일을 하고 있음을 알 수 있다.
goodness()함수에서는 real-time프로세스와 time-sharing 프로세스에 대해 각각 weight값을 달리 계산한다는 것을 알 수 있다. 즉 SCHED_OTHER인 scheduling policy를 가지는 프로세스에 대해서는 counter+20-nice+advantage를 weight값으로 주게되나, SCHED_FIFO나 SCHED_RR인 scheduling policy를 가지는 프로세스에 대해서는 weight값으로 1000 + rt_priority로 주어서 상대적으로 weight값이 높도록 만들어주고 있다. 물론 SCHED_YIELD로 되어있는 경우의 프로세스에 대해서는 weight값이 -1로 설정되어 scheduling되지 않도록 만들어주고 있다.
같은 cpu에서 실행되는것에 대해 advantage를 부과한다.(계속 같은 cpu에서 실행되도록)
nice값은 process가 자신에게 직접 줄 수 있는 priority를 변동시키는 값이다.
지금까지 우리는 process scheduling에 대한 이야기를 code를 보면서 살펴보았다.
중요한 것은
Uniprocessor를 가진 system과 multi-processor를 가진 system이 scheduling algorithm상 차이가 있다는 점과
현재의 process에서 실행중인 것이 상대적으로 더 높은 우선순위 값을 가지도록 만들어 줌으로써, 같은 processor상에서 수행되도록 해준다는 점이다. 또한
실시간 process에 대해서 상대적으로 normal process보다 높은 우선 순위를 가지도록 해준다는 점도 눈에 띈다.
프로세스의 생성
리눅스에서의 스레드의 구현은 task의 구현과 다르지 않다. 즉 리눅스에서는 프로세스와 스레드를 같은 task_struct를 사용해서 나타낸다. 스레드를 생성한 프로세스의 주소공간과 file descriptor 및 signal handler등의 정보를 공유하게 된다. 심지어 process id까지도 공유가 가능하다. 이와 같은 것들을 가능하게 하기 위해서 리눅스에서는 fork()의 특별한 형태인 clone()이라는 함수를 가지고 있다. fork()와 clone()모두~/kernel/fork.c에 있는 do_fork()를 호출한다.
- asmlinkage int sys_fork(struct pt_regs regs) {
-
return do_fork( SIGCHLD , regs_esp , ®s , 0);
- }
- asmlinkage int sys_clone( struct pt_regs regs) {
-
unsigned long clone_flags;
-
unsigned long newsp;
-
clone_flags = regs.ebx;
-
newsp = regs.ecx;
-
if( !newsp )
-
newsp = regs.esp;
-
return do_fork( clone_flags , newsp , ®s , 0);
- }
[fork와 clone의 system call interface]
do_fork()에서 하는 일은 시스템 프로세스의 정보를 복사하고 적절하게 각 레지스터들을 설정한다. 데이터 세그먼트의 내용을 전체를 복사하고, 스택으로 사용할 영역을 지정해주게 된다.
- int do_fork( unsigned long clone_flags , unsigned long stack_start , struct pt_regs* regs , unsigned long stack_top )
- {
-
int retval = -ENOMEN;
-
struct task_struct* p;
-
DECLARE_MUTEX_LOCKED( sem );
-
if( clone_flags & CLONE_PID ) {
-
/* this is only allowed from the boot up thread*/
-
if( current->pid)
-
return -EPERM;
-
}
[do_fork()함수 - pid의 복제확인]
먼저 PID를 복제하는지 확인한다. 이는 시스템이 부팅할 때만 프로세스 ID를 복사할 수 있도록 하기 때문에, 만약 프로세스 ID가 0이 아닌 프로세스가 이것을 호출했다면 에러를 돌려준다.
-
current->vfork_sem = &sem;
-
p = alloc_task_struct();
-
if( !p )
-
goto fork_out;
-
*p = *current;
-
retval = -EAGAIN;
-
if( atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur )
-
goto bad_fork_free;
-
atomic_inc( &p->user->__count );
-
atomic_inc(&p->user->processes);
-
if(mr_threads >= max_threads)
-
goto bad_fork_cleanup_count;
-
if(p->exec_domain && p->exec_domain->module)
-
__MOD_INC_USE_COUNT(p->exec_domain->module);
-
if(p->binfmt && p->binfmt->module )
-
__MOD_INC_USE_COUNT(p->binfmt->module);
-
p->did_exec = 0;
-
p->swappable = 0;
-
p->state = TASK_UNINTERRUPTIBLE;
[do_fork()함수 (계속) task_struct할당 ]
세마포어를 획득한 후 task_struct를 할당받고, 현재 프로세스의 내용을 복사해 넣어둔다.
그리고 나서 자원사용한계 확인하고 새로 생성된 프로세스의 사용자와 관련된 count값을 증가시킨다. 물론 이때는 인터럽트가 허용되지 않는 atomic increment를 사용해주어야 한다.
실행하려는 프로세스의 실행 domain과 이진포맷의 모듈 사용 카운터의 값을 증가시켜주고, 아직 exec system call을 하지 않았다는 것과 swap되지 않았다는 것을 표시한 다음, 현재 프로세스의 상태를 TASK_UNINTERRUPTIBLE로 둔다.
이는 아직 완전히 생성되지 않았기에 이벤트들에 의해서 반응하지 않도록 만들어주는 것이다.
-
copy_flags(clone_flags , p );
-
/*프로세스의 PID 결정*/
-
p->pid = get_pid(clone_flags);
-
/*run list 초기화 */
p->run_list.next = NULL;
-
p->run_list.prev = NULL;
-
if( (clone_flags & CLONE_VFORK ) || !(clone_flags & CLONE_PARENT ) ) {
-
/*원래의 부모 프로세스를 가리키는 포인터를 현재의 프로세스로 설정 */
- p->p_opptr = current;
- if( !(p->ptrace & PT_PTRACED) )
- /*새로운 프로세스가 디버깅을 위해 trace되고 있다면 부모 프로세스를 가리키는 포인터도 현재의 프로세스를 가리키도록 설정 */
- p->p_pptr = current;
- }
- /*자식은 아직 없으므로 NULL. */
- p->p_cptr = NULL;
clone flag의 값들은 ~/include/linux/sched.h에 정의되어 있으며 아래와 같은 값들을 가진다.
| 이름 | 값 | 설명 |
| CLONE_VM | 0x100 | 가상메모리 공유 |
| CLONE_FS | 0x200 | 파일시스템 정보 공유 |
| CLONE_FILE | 0x400 | 열린파일 공유 |
| CLONE_SIGHAND | 0x800 | signal handler들과 block된 signal공유 |
| CLONE_PID | 0x1000 | process id공유 |
| CLONE_PTRACE | 0x2000 | 새로생성될 프로세스도 debugging을 위해 trace되기를 원할 때 설정 |
| CLONE_VFORK | 0x4000 | 자식프로세스가 mm_release()할때 부모프로세스가 깨어나기를 원할때 |
| CLONE_PARENT | 0x8000 | 생성될 프로세스가 현재 생성하고 있는 프로세스와 같은 부모를 가지도록 만들고 싶은 경우에 사용 |
| CLONE_THREAD | 0x10000 | 같은 thread group에 속하게 될지를 결정 |
| CLONE_SIGNAL | CLONE_SIGHAND | CLONE_THREAD |
-
init_waitqueue_head( &p->wait_chldexit);
-
p->vfork_sem = NULL;
-
spin_lock_init(&p->alloc_lock);
-
p->sigpending = 0;
-
init_sigpending(&p->pending);
[do_fork()함수 계속]
그리고 나서 프로세스의 wait4()시스템 콜을 위한 wait queue및 vfork_sem , alloc_lock , sigpending , pending 필드들에 대한 초기화를 수행한다.
-
p->it_real_value = p->it_virt_value = p->it_prof_value = 0;
-
p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0;
-
init_timer(&p->real_timer);
-
p->real_timer.data = (unsigned long)p;
-
p->leader = 0; /*session leadership doesn't inherit */
-
p->tty_old_pgrp = 0;
-
p->times.tms_utime = p->times.tms_stime = 0;
-
p->times.tms_cutime = p->times.tms_cstime = 0;
- #ifdef CONFIG_SMP
- {
- int i;
- p->has_cpu = 0;
- p->processor = current->processor;
- /*?? should we just memset this ?? */
- for(i = 0 ; i < smp_num_cpus ; i++ )
- p->per_cpu_utime[i] = p->per_cpu_stime[i] = 0;
- spin_lock_init( &p->sigmask_lock );
- }
- #endif
- p->lock_depth = -1; /* -1 = no lock */
- p->start_time = jiffies;
- [do_fork()함수 계속 - 시간값들에 대한 초기화 및 프로세스 그룹에 대한 초기화]
이제는 프로세스의 프로파일링을 위한 각종 정보들을 초기화한다. 또한 프로세스가 속한 session에 대한 초기화 및 SMP기계인 경우 각각의
CPU당 사용자 시간과 시스템을 초기화해 준다. 이와 같은 일이 끝나면 새로이 생성된 프로세스의 시작시간을 기록한다.
-
retval = -ENOMEM;
-
/*copy all the process information */
-
if( copy_files(clone_flags , p ))
-
goto bad_fork_cleanup;
-
if( copy_fs(clone_flags , p ) )
-
goto bad_fork_cleanup_files;
-
if( copy_sighand( clone_flags , p))
-
goto bad_fork_cleanup_fs;
-
if( copy_mm(clone_flags , p ))
-
goto bad_fork_cleanup_sighand;
-
retval = copy_thread(0 , clone_flags , stack_start , stack_top , p , regs );
-
if(retval)
-
goto bad_fork_cleanup_sighand;
-
p->semundo = NULL;
[do_fork() 함수 계속 - 부모로부터 필요한 필드 복제]
부 모 프로세스의 각종 정보들을 복사하는 부분이다. 파일 디스크립터 및, 파일 시스템에 대한 정보와 시그널 핸들러, 메모리, 스택과 레지스터 값들을 복제한다. 그리고나서 아직 아무런 semaphore와 관련된 동작을 하지않았으므로 semundo를 NULL값으로 초기화한다.
-
p->parent_exec_id = p->self_exec_id;
-
p->swappable = 1;
-
p->exit_signal = clone_flags & CSIGNAL;
-
p->pdeath_signal = 0;
여기까지 진행했다면, 이미 생성될 프로세스에 대해서 메모리 자원이 할당된 상태이다.
해당 실행 도메인의 초기화,
프로세스의 스웹(swap)가능성 ,
종료상황에서 보내고자 하는 시그널(exit_signal),
부모 프로세스가 종료될 때 받을 시그널을 초기화 한다
-
p->counter = (current->counter + 1) >> 1;
current->counter >>= 1;
if (!current->counter) -
current->need_resched = 1;
프로세스의 우선순위(counter)값을 정해주는 단계에서는 자식은 부모 프로세스가 가진 우선 순위를 반으로 나누어서 가지도록 만든다. 이것은 높은 우선순위의 프로세스가 자식 프로세스를 많이 거느리게 될 경우, 관련되지 않은 낮은 우선순위의 프로세스들에게 미치는 영향을 줄이기 위한 노력이다. 만약 부모 프로세스의 우선순위가 위와 같은 과정에서 0값을 가진다면 새로운 프로세스를 실행하도록 스케줄링을 요청한다(need_resched=1).
-
retval = p->pid;
-
p->tgid = retval;
-
INIT_LIST_HEAD(&p->thread_group);
-
write_lock_irq(&tasklist_lock);
-
if (clone_flags & CLONE_THREAD) {
-
p->tgid = current->tgid;
-
list_add(&p->thread_group, ¤t->thread_group);
-
}
-
SET_LINKS(p);
-
hash_pid(p);
-
nr_threads++;
-
write_unlock_irq(&tasklist_lock);
[pid와 thread그룹의 초기화]
자신이 돌려받을 값(프로세스의 ID)와 쓰레드그룹의 아이디를 설정한 다음, 자신이 가지는 쓰레드 그룹의 리스트를 초기화 한다. 이젠 생성한 프로세스를 태스크의 리스트에 삽입할 차례이다.
SET_LINKS ()가 init_task로 시작하는 태스크의 연결 리스트에 생성한 프로세스를 삽입한다. 그리고, 프로세스의 아이디를 가지고 찾을 때 사용하도록 hash 테이블에도 등록하게 되며, 생성된 프로세스의 수(nr_threads)를 증가 시켜준다.
프로세스가 생성되면, 새로 생성된 프로세스는 부모 프로세스와 같은 상황에서 수행된다. 따라서, 부모와 자식을 구분할 수 있는 방법이 필요한데, 이것은 시스템 콜에서 돌려받는 값으로 확인하게 된다. 부모 프로세스의 경우에는 자식 프로세스의 PID값을 돌려받게 되며, 자식 프로세스는 0을 받는다.
-
if (p->ptrace & PT_PTRACED)
-
send_sig(SIGSTOP, p, 1);
-
wake_up_process(p); /* do this last */
-
++total_forks;
- fork_out:
-
if ((clone_flags & CLONE_VFORK) && (retval > 0))
-
down(&sem);
-
return retval;
[do_fork()함수 계속 - do_fork()함수의 종료상황 처리]
만약 프로세스가 현재 trace가 되고 있다면, 멈추도록(send_sig())한다. 그렇지 않을 경우에는 새로 생성될 프로세스를 깨운 후, 마지막으로 전체 fork한 횟수를 증가 시켜주고나서 함수를 복귀하게 된다.
- bad_fork_cleanup_sighand:
-
exit_sighand(p);
- bad_fork_cleanup_fs:
-
exit_fs(p); /* blocking */
- bad_fork_cleanup_files:
-
exit_files(p); /* blocking */
- bad_fork_cleanup:
-
put_exec_domain(p->exec_domain);
-
if (p->binfmt && p->binfmt->module)
-
__MOD_DEC_USE_COUNT(p->binfmt->module);
- bad_fork_cleanup_count:
-
atomic_dec(&p->user->processes);
-
free_uid(p->user);
- bad_fork_free:
-
free_task_struct(p);
-
goto fork_out;
- }
프로세스의 생성과 관련된 마지막은 앞에서 생성도중에 생긴 문제들에 대한 해결일 것이다. 즉, 원래 상태로의 복귀및 에러코드의 리턴이 되겠다. 이와 같은 에러는 주로 복사중에 일어나는 문제들로 해당하는 에러 코드를 현재의 프로세스에 알려주어야 할 것이다.
지금까지는 do_fork()함수를 이용해서 새로이 프로세스를 생성하는 과정에 대해서 알아보았다. 이것은 단순히 부모 프로세스의 복제본을 만드는 것에 지나지않다. 다음으로 볼것은 이렇게 생성된 프로세스를 이용해서 부모 프로세스와는 다른 프로그램의 실행 이미지를 만드는 것을 볼 것이다.
프로세스의 실행
프 로세스는 생성시에는 부모의 실행이미지를 그대로 가지고 실행상태에 있게된다. 따라서, 새로운 프로그램을 실행하려고 할때는 넘겨받은 부모의 실행이미지를 버리고, 다시 자신만의 실행이미지를 가져야 한다. 즉, 디스크로부터 새로이 실행될 프로그램을 불러오는 과정이 필요하다.
프로세스를 실행하기 위해서는 시스템 콜 ~/arch/i386/kernel/process.c에 있는 sys_execve()를 사용한다. 이는 다시 내부적으로 do_execve()함수를 호출하게 되어있다. 코드는 아래와 같다.
- asmlinkage int sys_execve(struct pt_regs regs) {
- int error;
-
char * filename;
-
filename = getname((char *) regs.ebx);
-
error = PTR_ERR(filename);
-
if (IS_ERR(filename))
-
goto out;
-
error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, ®s);
-
if (error == 0)
-
current->ptrace &= ~PT_DTRACE;
-
putname(filename);
- out:
-
return error;
- }
[sys_execve()함수 정의 - exec 시스템 콜]
넘 겨 받은 레지스터에서 해당하는 프로그램의 파일 이름을 찾은 다음, do_execve()함수를 호출하고, 현재의 프로세스의 PT_DTRACE flag를 off시킨다. 파일 이름을 가지고 있는 dentry의 구조체를 없애고 do_execve()의 복귀값을 돌려준다.
실제적인 실행과 관련된 일은 ~/fs/exec.c의 do_execve()에서 일어난다. 가장먼저 하는 일은 디스크 상의 파일을 여는 일일 것이다. 여기서 한가지 실제로 사용자의 입장에서 보면, 여러가지의 형태의 exec()시스템 콜이 정의되어 있으나, 이것은 프로세스가 어떤 환경에서 실행될 것인가의 차이만을 가지는 것으로 결과적으로는 do_exec()가 호출되게 된다.
- int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs){
-
struct linux_binprm bprm;
-
struct file *file;
-
int retval;
-
int i;
-
-
file = open_exec(filename);
-
retval = PTR_ERR(file);
-
if (IS_ERR(file))
-
return retval;
-
[do_execve()함수 - 파일 열기]
넘겨받은 파일이름에 대해 열기(open_exec())를 하고 열려진 화일에 대해 에러가 있는지를 검사한다. 만약 에러가 있다면 곧바로 복귀하고 그렇지 않다면 아래로 진행한다.
리눅스가 바이너리를 실행하기 위해서 사용하는 linux_binprm구조체에 대해 살펴보기로 하자.
- struct linux_binprm{
- /*실행 파일의 첫 128bytes를 가진다. 이부분은 실행화일의 format을 나타내는
- *정보를 가진다.가령 예를 들어서 ELF파일의 포맷을 나타내는 MAGIC NUMBER등이 있다.
- */
-
char buf[BINPRM_BUF_SIZE];
-
/*실행 argument들을 보관할 page들에 대한 포인터 배열*/
struct page *page[MAX_ARG_PAGES];
-
-
- /*현재 메모리 top*/
- unsigned long p; /* current top of mem */
-
/*현재 실행하고자 하는 file이 shell script인지의 여부를 나타냄 */
int sh_bang;
-
/*열려진 파일(실행할 파일)에 대한 포인터 */
-
struct file * file;
-
/*Effective User ID와 Effective group ID(inode로부터 넘겨받음) */
int e_uid, e_gid;
-
/*capability를 나타내는 field*/
kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
-
/*argument cout와 environment count */
int argc, envc;
-
char * filename; /* Name of binary */
-
/*실행화일의 loader와 exec domain */
-
unsigned long loader, exec;
- };
[linux_binprm 구조체 정의]
linux_binprm구조체는 실행할 프로그램에 대한 정보를 넘겨 받아서 저장하며, 실행하기 위해서 필요한 로더(loader)나 번역기(interpreter) 프로그램을 알기위해서 사용한다.
-
bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
-
memset(bprm.page, 0, MAX_ARG_PAGES*sizeof(bprm.page[0]));
-
bprm.file = file;
-
bprm.filename = filename;
-
bprm.sh_bang = 0;
-
bprm.loader = 0;
-
bprm.exec = 0;
[do_execve()함수 계속 - linux_binprm구조체 초기화]
바이너리(binary) 파일을 실행하기위한 절차로서, 먼저 넘겨 받은 argument들을 저장하기 위한 linux_binprm 구조체를 초기화 한다. 즉, argument에 대한 저장공간의 초기화와 파일이름 및 파일 포인터, loader와 exec필드에 대한 초기화를 담당한다.
-
/*argument수 및 env. parameter 갯수 세기 */
if ((bprm.argc = count(argv, bprm.p / sizeof(void *))) < 0) {
-
allow_write_access(file);
-
fput(file);
-
return bprm.argc;
-
}
-
if ((bprm.envc = count(envp, bprm.p / sizeof(void *))) < 0) {
-
allow_write_access(file);
-
fput(file);
-
return bprm.envc;
-
}
-
-
retval = prepare_binprm(&bprm);
-
-
if (retval < 0)
-
goto out;
-
retval = copy_strings_kernel(1, &bprm.filename, &bprm);
-
if (retval < 0)
-
goto out;
-
bprm.exec = bprm.p;
-
retval = copy_strings(bprm.envc, envp, &bprm);
-
if (retval < 0)
goto out; -
retval = copy_strings(bprm.argc, argv, &bprm);
-
if (retval < 0)
goto out;
retval = search_binary_handler(&bprm,regs); //이진 핸들러 검색색
if (retval >= 0)
/* execve success */ -
return retval;
[do_execve()함수 계속 - 넘겨받는 변수의 검사와 linux_binprm의 초기화 및 binary format에 대한 handler 구하기 ]
실행을 위해서 넘겨받은 argument와 환경(environment) 변수들(argv, envp)의 count값을 저장하고,
파일과 관련된 inode로부터 실행과 관련된 정보를 넘겨받는다(prepare_binprm()).
그리고, 실행하려는 파일의 이름과 현재 최상위 메모리가 어딘가를 표시한 다음,
argument및 환경 변수들을 저장한다.
그리고나서 이진 포맷(binary format)에 대해서 핸들러를 검색해서, 현재 실행하고자 하는 이진(binary) 파일을 다루어줄 프로그램에 대한 정보를 찾는다.
여기까지 모든 과정이 제대로 되었다면 exec()는 성공했으며, 복귀하면 된다.
- out:
-
allow_write_access(bprm.file);
-
if (bprm.file)
-
fput(bprm.file);
-
for (i = 0 ; i < MAX_ARG_PAGES ; i++) {
-
struct page * page = bprm.page[i];
-
if (page)
-
__free_page(page);
-
}
-
return retval;
- }
[do_execve()함수 계속 - 에러상황의 처리 ]
뭔가 실행에 관련된 연산에서 에러가 있을 경우 실행되는 부분이다. 주로 하는 일은 할당받았던 자원을 운영체제로 돌려주는 것이다. 관련된 것은 실행하려는 파일과 관련된 inode및 argument를 위한 페이지들을 놓아준다(release).
프로세스의 종료
프로세스의 마지막이 종료이다. 이것은 exit()시스템 콜을 사용해서 가능하다. 프로세스의 종료는 생성시에 했던 일들에 대해서 반대로 해주면 될 것이다. 즉, 할당받은 시스템의 자원이 있다면, 이것을 놓아주고, 자신의 수행 상황에 대한 각종 정보를 커널에 알려주게 되며, 프로세스의 종료정보를 부모프로세스로 넘겨준다. 마지막으로 다른 프로세스가 수행될 수 있도록 스케줄링을 요청하게 된다.
- NORET_TYPE void do_exit(long code)
{ -
struct task_struct *tsk = current;
-
if (in_interrupt())
-
panic("Aiee, killing interrupt handler!");
-
if (!tsk->pid)
-
panic("Attempted to kill the idle task!");
-
if (tsk->pid == 1)
-
panic("Attempted to kill init!");
-
tsk->flags |= PF_EXITING;
-
del_timer_sync(&tsk->real_timer);
- fake_volatile:
- #ifdef CONFIG_BSD_PROCESS_ACCT
-
acct_process(code);
- #endif
[do_exit()함수 - PID 확인과 timer지우기]
먼저 exit()시스템 콜을 사용하는 프로세스가 인터럽트가 진행중인지를 확인하고 다시 시스템의 idle task인지와 init process인지를 확인한다. 그리고 나서 프로세스가 exit하고 있음을 표시(PF_EXITING)한 다음, 동적 타이머 큐(dynamic timer queue)로부터 프로세스의 real_timer를 제거한다. 그리고 만약 BSD 프로세스 account를 사용한다면 해당하는 함수를 불러준다.
-
__exit_mm(tsk);
-
lock_kernel();
-
sem_exit();
-
__exit_files(tsk);
-
__exit_fs(tsk);
-
exit_sighand(tsk);
-
exit_thread();
-
if (current->leader)
-
disassociate_ctty(1);
-
put_exec_domain(tsk->exec_domain);
- if (tsk->binfmt && tsk->binfmt->module)
__MOD_DEC_USE_COUNT(tsk->binfmt->module);
tsk->exit_code = code;
exit_notify();
schedule();
BUG();
goto fake_volatile;
[do_exit()함수 계속 - 할당받은 자원의 해제와 종료상황에 대한 보고]
프로세스와 관련된 메모리와 세마포어, 파일 및 파일시스템 정보, 시그널 처리, 스레드에 대한 정보들을 release하고
종료하는 현재 프로세스가 terminal을 가지고 있다면, 이것역시 떼어낸다.
그리고 나서 프로세스를 실행하기 위해서 필요한 실행 도메인(execution domain)을 가지고 있는 module에 대한 count값을 낮추는 put_exec_domain()을 호출하고 나서
다시 이진 포맷과 관련된 모듈도 사용값을 낮춘다.
전달받은 종료 코드 값을 저장한 다음,
종료중인 프로세스와 관련된 프로세스들에게 자신이 종료되고 있음을 알려준다. (exit_notify()).
프로세스의 상태도 ZOMBIE상태로 바뀐다.
마지막으로 새로운 프로세스를 스케줄링하도록 알려주게 되며
현재의 프로세스는 실행리스트에서 제외되어 있으므로 더 이상 스케줄링받지 않게 된다.
ZOMBIE상태는 부모프로세스에 종료 상태를 알리지 않은 프로세스의 상태를 말하는 것으로 이는 부모 프로세스가 기동해서 적절한 행동을 취해야 한다. 하지만 만약 부모프로세스가 먼저 종료했을 경우에, 남는 프로세스의 부모프로세스는 init process가 된다. 즉 init프로세스가 부모를 읽은 프로세스의 종료를 관리하게 된다.
이상에서 프로세스의 생성과 실행, 종료에 대해 알아보았다 프로세스는 실행되기 위해서 시스템의 자원을 필요로 하며, 종료 시에는 이렇게 할당된 자원들을 다시 반환해야 한다. 모든 프로세스는 부모를 가지게 되며, 종료된 프로세스를 부모로 가진 프로세스는 init 프로세스를 새로운 부모로 가진다. 또한 프로그램은 실행될 수 있는 format을 인식하는 관련된 loader가 존재하며 이러한 format으로는 ELF와 A.OUT형식 등이 존재한다.
Comments (0)