概述
1.进程概述
进程和程序的关系:
程序是指令的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个动态的概念。
程序可以作为一种软件资料长期存在,而进程是有一定生命期的。程序是永久的,进程是暂时的。
进程更能真实地描述并发,而程序不能;进程是由进程控制块、程序段、数据段三部分组成;进程具有创建其他进程的功能,而程序没有。
同一程序同时运行于若干个数据集合上,它将属于若干个不同的进程。也就是说同一程序可以对应多个进程。
(1)进程的基本概念
进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。
它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
具有如下特点:
●动态性:进程的状态是不断变化的。一般分为睡眠态、就绪态、运行态和挂起态。
●并行性:多个进程在宏观上并行,微观上串行。
●独立性:进程是一个独立运行、调度、申请资源的基本单位。
●异步性:各进程相互独立,通过操作系统提供的机制来协调各个进程的运行。
●可再现性:进程在相同的初始条件和环境,多次运行应该得到相同的结果。
一个进程主要包含以下三个元素:
●一个正在执行的程序;
●与该进程相关联的全部数据(变量,内存,缓冲区);
●程序上下文(程序计数器);
说明:
●进程是一个实体;
●进程是一个“执行中的程序”。
进程的状态
进程执行时的间断性,决定了进程可能具有多种状态。事实上,运行中的进程可能具有以下三种基本状态。
●就绪状态(Ready):
进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。
●运行状态(Running):
进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
●阻塞状态(Blocked):
由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理机分配给该进程,也无法运行。
关系图
(2)Linux进程
task_struct结构体
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
int lock_depth; /* BKL lock depth */
#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
int oncpu;
#endif
#endif
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
#ifdef CONFIG_PREEMPT_NOTIFIERS
/* list of struct preempt_notifier: */
struct hlist_head preempt_notifiers;
#endif
/*
* fpu_counter contains the number of consecutive context switches
* that the FPU is used. If this is over a threshold, the lazy fpu
* saving becomes unlazy to save the trap. This is an unsigned char
* so that after 256 times the counter wraps and the behavior turns
* lazy again; this to deal with bursty apps that only use FPU for
* a short time
*/
unsigned char fpu_counter;
#ifdef CONFIG_BLK_DEV_IO_TRACE
unsigned int btrace_seq;
#endif
unsigned int policy;
cpumask_t cpus_allowed;
#ifdef CONFIG_TREE_PREEMPT_RCU
int rcu_read_lock_nesting;
char rcu_read_unlock_special;
struct rcu_node *rcu_blocked_node;
struct list_head rcu_node_entry;
#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
struct sched_info sched_info;
#endif
struct list_head tasks;
struct plist_node pushable_tasks;
struct mm_struct *mm, *active_mm;
/* task state */
int exit_state;
int exit_code, exit_signal;
int pdeath_signal; /* The signal sent when the parent dies */
/* ??? */
unsigned int personality;
unsigned did_exec:1;
unsigned in_execve:1; /* Tell the LSMs that the process is doing an
* execve */
unsigned in_iowait:1;
/* Revert to default priority/policy when forking */
unsigned sched_reset_on_fork:1;
pid_t pid;进程的标识符
pid_t tgid;//线程组标识符
#ifdef CONFIG_CC_STACKPROTECTOR
/* Canary value for the -fstack-protector gcc feature */
unsigned long stack_canary;
#endif
/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
struct task_struct *real_parent; /* real parent process */
struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */
/*
* children/sibling forms the list of my natural children
*/
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
/*
* ptraced is the list of tasks this task is using ptrace on.
* This includes both natural children and PTRACE_ATTACH targets.
* p->ptrace_entry is p's link on the p->parent->ptraced list.
*/
struct list_head ptraced;
struct list_head ptrace_entry;
/* PID/PID hash table linkage. */
struct pid_link pids[PIDTYPE_MAX];
struct list_head thread_group;
struct completion *vfork_done; /* for vfork() */
int __user *set_child_tid; /* CLONE_CHILD_SETTID */
int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
cputime_t utime, stime, utimescaled, stimescaled;
cputime_t gtime;
cputime_t prev_utime, prev_stime;
unsigned long nvcsw, nivcsw; /* context switch counts */
struct timespec start_time; /* monotonic time */
struct timespec real_start_time; /* boot based time */
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread- specific */
unsigned long min_flt, maj_flt;
struct task_cputime cputime_expires;
struct list_head cpu_timers[3];
/* process credentials */
const struct cred *real_cred; /* objective and real subjective task
* credentials (COW) */
const struct cred *cred; /* effective (overridable) subjective task
* credentials (COW) */
struct mutex cred_guard_mutex; /* guard against foreign influences on
* credential calculations
* (notably. ptrace) */
struct cred *replacement_session_keyring; /* for KEYCTL_SESSION_TO_PARENT */
char comm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by setup_new_exec */
/* file system info */
int link_count, total_link_count;
#ifdef CONFIG_SYSVIPC
/* ipc stuff */
struct sysv_sem sysvsem;
#endif
#ifdef CONFIG_DETECT_HUNG_TASK
/* hung task detection */
unsigned long last_switch_count;
#endif
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
/* namespaces */
struct nsproxy *nsproxy;
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
struct audit_context *audit_context;
#ifdef CONFIG_AUDITSYSCALL
uid_t loginuid;
unsigned int sessionid;
#endif
seccomp_t seccomp;
#ifdef CONFIG_UTRACE
struct utrace *utrace;
unsigned long utrace_flags;
#endif
/* Thread group tracking */
u32 parent_exec_id;
u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed,
* mempolicy */
spinlock_t alloc_lock;
#ifdef CONFIG_GENERIC_HARDIRQS
/* IRQ handler threads */
struct irqaction *irqaction;
#endif
/* Protection of the PI data structures: */
spinlock_t pi_lock;
#ifdef CONFIG_RT_MUTEXES
/* PI waiters blocked on a rt_mutex held by this task */
struct plist_head pi_waiters;
/* Deadlock detection and priority inheritance handling */
struct rt_mutex_waiter *pi_blocked_on;
#endif
#ifdef CONFIG_DEBUG_MUTEXES
/* mutex deadlock detection */
struct mutex_waiter *blocked_on;
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
unsigned int irq_events;
int hardirqs_enabled;
unsigned long hardirq_enable_ip;
unsigned int hardirq_enable_event;
unsigned long hardirq_disable_ip;
unsigned int hardirq_disable_event;
int softirqs_enabled;
unsigned long softirq_disable_ip;
unsigned int softirq_disable_event;
unsigned long softirq_enable_ip;
unsigned int softirq_enable_event;
int hardirq_context;
int softirq_context;
#endif
#ifdef CONFIG_LOCKDEP
# define MAX_LOCK_DEPTH 48UL
u64 curr_chain_key;
int lockdep_depth;
unsigned int lockdep_recursion;
struct held_lock held_locks[MAX_LOCK_DEPTH];
gfp_t lockdep_reclaim_gfp;
#endif
/* journalling filesystem info */
void *journal_info;
/* stacked block device info */
struct bio *bio_list, **bio_tail;
/* VM state */
struct reclaim_state *reclaim_state;
struct backing_dev_info *backing_dev_info;
struct io_context *io_context;
unsigned long ptrace_message;
siginfo_t *last_siginfo; /* For ptrace use. */
struct task_io_accounting ioac;
#if defined(CONFIG_TASK_XACCT)
u64 acct_rss_mem1; /* accumulated rss usage */
u64 acct_vm_mem1; /* accumulated virtual memory usage */
cputime_t acct_timexpd; /* stime + utime since last update */
#endif
#ifdef CONFIG_CPUSETS
nodemask_t mems_allowed; /* Protected by alloc_lock */
#ifndef __GENKSYMS__
/*
* This does not change the size of the struct_task(2+2+4=4+4)
* so the offsets of the remaining fields are unchanged and
* therefore the kABI is preserved. Only the kernel uses
* cpuset_mem_spread_rotor and cpuset_slab_spread_rotor so
* it is safe to change it to use shorts instead of ints.
*/
unsigned short cpuset_mem_spread_rotor;
unsigned short cpuset_slab_spread_rotor;
int mems_allowed_change_disable;
#else
int cpuset_mem_spread_rotor;
int cpuset_slab_spread_rotor;
#endif
#endif
#ifdef CONFIG_CGROUPS
/* Control Group info protected by css_set_lock */
struct css_set *cgroups;
/* cg_list protected by css_set_lock and tsk->alloc_lock */
struct list_head cg_list;
#endif
#ifdef CONFIG_FUTEX
struct robust_list_head __user *robust_list;
#ifdef CONFIG_COMPAT
struct compat_robust_list_head __user *compat_robust_list;
#endif
struct list_head pi_state_list;
struct futex_pi_state *pi_state_cache;
#endif
#ifdef CONFIG_PERF_EVENTS
#ifndef __GENKSYMS__
void * __reserved_perf__;
#else
struct perf_event_context *perf_event_ctxp;
#endif
struct mutex perf_event_mutex;
struct list_head perf_event_list;
#endif
#ifdef CONFIG_NUMA
struct mempolicy *mempolicy; /* Protected by alloc_lock */
short il_next;
#endif
atomic_t fs_excl; /* holding fs exclusive resources */
struct rcu_head rcu;
/*
* cache last used pipe for splice
*/
struct pipe_inode_info *splice_pipe;
#ifdef CONFIG_TASK_DELAY_ACCT
struct task_delay_info *delays;
#endif
#ifdef CONFIG_FAULT_INJECTION
int make_it_fail;
#endif
struct prop_local_single dirties;
#ifdef CONFIG_LATENCYTOP
int latency_record_count;
struct latency_record latency_record[LT_SAVECOUNT];
#endif
/*
* time slack values; these are used to round up poll() and
* select() etc timeout values. These are in nanoseconds.
*/
unsigned long timer_slack_ns;
unsigned long default_timer_slack_ns;
struct list_head *scm_work_list;
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
/* Index of current stored adress in ret_stack */
int curr_ret_stack;
/* Stack of return addresses for return function tracing */
struct ftrace_ret_stack *ret_stack;
/* time stamp for last schedule */
unsigned long long ftrace_timestamp;
/*
* Number of functions that haven't been traced
* because of depth overrun.
*/
atomic_t trace_overrun;
/* Pause for the tracing */
atomic_t tracing_graph_pause;
#endif
#ifdef CONFIG_TRACING
/* state flags for use by tracers */
unsigned long trace;
/* bitmask of trace recursion */
unsigned long trace_recursion;
#endif /* CONFIG_TRACING */
/* reserved for Red Hat */
unsigned long rh_reserved[2];
#ifndef __GENKSYMS__
struct perf_event_context *perf_event_ctxp[perf_nr_task_contexts];
#ifdef CONFIG_CGROUP_MEM_RES_CTLR /* memcg uses this to do batch job */
struct memcg_batch_info {
int do_batch; /* incremented when batch uncharge started */
struct mem_cgroup *memcg; /* target memcg of uncharge */
unsigned long bytes; /* uncharged usage */
unsigned long memsw_bytes; /* uncharged mem+swap usage */
} memcg_batch;
#endif
#endif
};
Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。
●状态(state):除了上述三种进程的基本状态之外,Linux进程还有stopped和zombie状态。
●调度信息:系统根据这些信息判定哪个进程最迫切需要运行。
●进程标志号(identifiers):用来区分进程的标识。
●进程间通信机制:Linux支持经典的Unix IPC机制,如信号、管道和信号灯,以及System V中机制,包括信号量、消息队列和共享内存。
(3)进程的识别号(ID)
Linux系统中所有进程都有一个唯一的,称为进程标识的正整数与之相联,称为进程ID,简称PID。
除了init进程(PID=1,所有进程的祖先),任一进程都有唯一的父进程。若干进程可以属于一个进程组,进程组也有一个唯一的进程组标识号。
真正用户标识号(UID):该标识号负责标识运行进程的用户。
有效用户标识号(EUID):该标识号负责标识以什么用户身份来给新创建的进程赋所有权、检查文件的存取权限和检查通过系统调用kill向进程发送软中断信号的许可权限。
真正用户组标识号(GID):负责标识运行进程的用户所属的组ID。
有效用户组标识号(EGID):用来标识目前进程所属的用户组。可能因为执行文件设置set-gid位而与gid不同。
进程的各种识别号:
●uid_t getuid(void);返回真实用户识别号;
●uid_t geteuid(void);返回有效用户识别号;
●gid_t getgid(void);返回真实组识别号;
●gid_t getegid(void);返回有效组识别号;
●pid_t getpid(void);返回进程的识别号;
●pid_t getppid(void);返回进程父进程的识别号;
●pid_t getpgrp(void);返回进程所属组的识别号;
(4)进程调度
进程的调度算法包括:
●FIFO(First Input First Output 先进先出法);
●RR(时间片轮转算法);
●HPF(最高优先级算法)。
2.进程控制
(1)进程的创建
pid_t fork()函数:
fork()函数是用来创建子进程的,当一个程序创建了一个子进程,那么原先的进程称为该子进程的父进程。
操作系统的进程表的每个表项中存放着一个进程的基本情况。首先,操作系统在进程表中为该进程建立新的表项。
子进程与父进程共享代码段,但数据空间是相互独立的。子进程数据空间的内容是父进程的完整拷贝,上下文也完全相同。
(使用fork()函数得到的子进程是父进程的复制品,子进程完全复制了父进程的资源,包括进程上下文、代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述符、 信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等信息, 而子进程与父进程的区别有进程号、资源使用情况和计时器等。)
pid在父进程与子进程的返回值是不同的。如果pid<0创建失败,在子进程中返回的pid=0,因为子进程可以通过getpid()得到自己的进程ID。
在父进程中返回的是子进程实际的PID。
子进程是从fork()函数之后开始执行,此后,父进程与子进程的运行就无关了。
fork出错可能有两种原因:
①当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
②系统内存不足,这时errno的值被设置为ENOMEM。
注:
●子进程copy父进程的变量、内存与缓冲区,即整个数据空间的内容,但两者内存空间是独立的,不是共享的。
●父、子进程对打开文件的共享: fork之后,子进程会继承父进程所打开的文件表,即父、子进程共享文件表,
该文件表是由内核维护的,两个进程共享文件状态,偏移量等。
当在父进程中关闭文件时,子进程的文件描述符仍然有用,相应的文件表也不会被释放。
●当fork函数执行成功,子进程和父进程的运行无关,所以处理机可能先调度子进程也可能先调度父进程,二者的返回顺序不固定;
●fork函数是一个单调用双返回的函数,即执行两次返回,它将从父进程和子进程中分别返回。父进程的返回值是子进程的ID,子进程的返回值是0。
pid_t vfork()函数:
vfork调用方法与fork函数完全相同。用vfork创建的进程主要目的是用exec函数执行另外的程序,与fork的第二个用途相同。
fork与vfork的区别:
●fork要拷贝父进程的数据段;而vfork则不需要完全拷贝父进程的数据段,在子进程没有调用exec和exit之前,子进程与父进程共享数据段;
●fork不对父、子进程的执行次序进行任何限制;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后, 父、子进程的执行次序才不再有限制。
(2)exec函数
①exec函数说明:
exec用被执行的程序完全替换调用它的程序的影像。fork创建一个新的进程就产生了一个新的PID,exec启动一个新程序,替换原有的进程,
因此这个新的被exec执行的进程的PID不会改变,和调用exec函数的进程一样。
②在Linux中使用exec函数族主要有以下两种情况:
●当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec 函数族让自己重生。
●如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。
③exec函数族语法:
int execl(const char *pathname,const char *arg0,...NULL);
//函数说明:execl()用来执行参数path字符串所代表的的文件路径,接下来的参数代表执行该文件时传递过去的argv[0]、argv[1] …,最后一个参数必须用空指针(NULL)作结束。
//返回值:如果函数执行成功则不会返回,执行失败直接返回-1,失败原因存在于errno中。
如:execl(“/bin/ls”, “/bin/ls”, “-al”, “/etc”,(char*)0);
第一个参数和pathname一样;ls命令的选项; 显示所有文件详细信息;文件所在的目录;
int execle( const char *pathname,const char *arg0,..,NULL,char * const envp[ ]);
int execv( const char *pathname,char *const argv[ ]);
函数说明:execv()用来执行参数pathname字符串所代表的文件路径,与execl()不同的地方在于其只需要两个参数,
第二个参数利用数组指针来传递给执行文件。
返回值:如果函数执行成功则不会返回,执行失败直接返回-1,失败原因存在于errno中。
如:char *argv[]={“ls”, “-l”, “/etc/password”,NULL};
execv(“/bin/ls”,argv); //显示/etc/password文件的详细信息。
int execve( const char *pathname,char * const argv[ ],char *const envp[ ]);
函数说明:execve()用来执行参数pathname字符串所代表的文件路径,第二个参数利用数组指针来传递给执行文件,最后一个参数则为传递给执行文件的新环境变量数组。
返回值:如果函数执行成功则不会返回,执行失败直接返回-1,失败原因存在于errno中。
如:char *argv[ ]={“ls”, “-l”, “/etc/password”,NULL};
char *envp[ ]={“PATH=/bin”,NULL};
execve(“/bin/ls”,argv,envp);
int execlp( const char *filename,const char *arg0,... NULL);
函数说明:execlp()会从PATH环境变量所指的目录中查找符合参数filename的文件名,找到后便执行该文件,然后将第二个以后的参数当作该文件的argv[0]、argv[1] …,最后一个参数必须用空指针(NULL)结束。
返回值:如果函数执行成功则不会返回,执行失败直接返回-1,失败原因存在于errno中。
如:execlp(“ls”, “ls”, “-al”, “/etc/passwd”,(char*)0); //显示/etc/passwd目录下的所有文件。
int execvp( const char *filename,char *const argv[ ]);
④总结
●l表示以参数列表的形式调用;
●v表示以参数数组的方式调用;
●e表示可传递环境变量;
●p表示PATH中搜索执行的文件,如果给出的不是绝对路径就会去PATH搜索相应名字的文件,如PATH没有设置,则会默认在/bin,/usr/bin下搜索。
●调用时参数必须以NULL结束。原进程打开的文件描述符是不会在exec中关闭的,除非用fcntl设置它们的“执行时关闭标志(close_on_exec)”
而原进程打开的目录流都将在新进程中关闭。
exec函数有六种不同的使用格式,有以下三个不同点:
●pathname是一个目标文件的完整路径名,而filename是目标文件名,它可以通过环境变量PATH来搜索。
●由pathname或filename指定的目标文件的命令行参数是完整的参数列表还是通过指针数组argv来给出的。
●环境变量是系统自动传递还是通过envp传递的。
(3)结束进程
①正常结束:
●在main函数中调用return,这相当于调用exit。
●调用exit函数。关闭所有的标准I/O流,但是不处理文件描述符、多进程、作业等,因而对Linux系统并不完善。
●调用_exit函数。关闭一些特有的退出句柄。
②异常终止:
●调用abort。产生SIGABORT信号。非正常终止进程。
●进程收到特定信号。
③exit函数
头文件#include<stdlib.h>
定义函数 void exit(int status);
函数说明:exit()用来正常终止目前进程的执行,并把参数status返回给父进程,而进程所有的缓冲区数据会自动写回并关闭未关闭的文件。
④_exit函数
头文件 #include<unistd.h>
定义函数 void _exit(int status);
函数说明:_exit()用来立刻结束目前进程的执行,并把参数status返回给父进程,并关闭未关闭的文件。
附加说明:_exit()不会处理标准I/O缓冲区,如要更新缓冲区请使用exit()。
⑤结束方式比较
●exit和return的区别。exit是一个函数,有参数,exit执行完后将控制权交给系统。return是函数执行完后的返回,将控制权交给调用函数。
●exit是正常终止进程,而abort是异常终止进程。
●exit和_exit都是终止进程,无条件的停止剩下的所有操作,释放数据结构。当status为0时表示正常结束,非0时表示有错误发生。
⑥exit和_exit的区别
●两者的头文件不同;
●_exit执行后立即返回给内核,而exit要先执行一些清除操作然后将控制权交给内核。
●调用_exit函数时,其会关闭进程的所有文件描述符、清理内存以及执行一些其它内核清理函数,但不会刷新流。
●exit函数是在_exit函数之上的一个封装,其会调用_exit函数,并在调用之前刷新流。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf("hi, mike, you are so good"); // 打印,没有换行符"n"
exit(0); // 结束进程,标准库函数,刷新缓冲区,printf()的内容能打印出来
// _exit(0); // 结束进程,系统调用函数,printf()的内容不会显示到屏幕
while(1); // 不让程序结束
return 0;
}
僵尸进程概念:
僵尸进程(Zombie Process):就是已经结束了的进程,但是没有从进程表中删除。太多了会导致进程表里面条目满了,进而导致系统崩溃,倒是不占用其他系统资源。
在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度, 仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸, 如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。
僵尸进程产生的原因:
每个Linux进程在进程表里都有一个进入点(entry),核心程序执行该进程时使用到的一切信息都存储在进入点。当用ps命令察看系统中的进程信息时,看到的就是进程表中的相关数据。当以fork()系统调用建立一个新的进程后,核心进程就会在进程表中给这个新进程分配一个进入点,然后将相关信息存储在该进入点所对应的进程表内。这些信息中有一项是其父进程的识别码。当这个进程走完了自己的生命周期后,它会执行exit()系统调用,此时原来进程表中的数据会被该进程的退出码(exit code)、执行时所用的CPU时间等数据所取代,这些数据会一直保留到系统将它传递给它的父进程为止。
由此可见,defunct进程的出现时间是在子进程终止后,但是父进程尚未读取这些数据之前。
僵尸进程的查看:
用top命令,可以看到
Tasks: 123 total, 1 running, 122 sleeping, 0 stopped, 0 zombie
zombie前面的数量就是僵尸进程到数量;
ps -ef
出现:
root 13028 12956 0 10:51 pts/2 00:00:00 [ls] <defunct>
最后有defunct的标记,就表明是僵尸进程。
僵尸进程解决办法:
a.改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行 waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。
b.把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程.它产生的所有僵尸进程也跟着消失。
⑦父进程先结束
当一个进程结束时,系统逐一检查所有的活动进程,如果某进程的父进程是这个被结束的进程,系统就将这个活动进程的父进程ID置为1,即init的ID号。也即父进程先于子进程结束时,init进程就会自动成为该子进程的父进程。
⑧总结
●进程不管如何终止,最后都会执行内核的同一段代码,关闭所有文件描述符并释放内存。
●若父进程在子进程之前终止,子进程的父进程都将成为init,保证每个进程都有父进程。
●当子进程先终止,父进程如何知道子进程的终止状态字?内核保留每个终止进程的终止状态字,父进程调用wait函数获取信息。
●当父进程调用wait等函数后,内核将释放终止进程所使用的所有内存,并关闭其打开的所有文件。
●对于已终止,但父进程尚未对其调用wait等函数的进程,成为僵尸进程。
●对于父进程先终止,被init领养的进程,init都会调用wait等函数获取其终止状态字,不会成为僵尸进程。
●子进程调用exit函数后,父进程必须调用wait等待函数。
(4)进程等待
①wait函数
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *staloc);
函数说明:当进程调用wait函数时,它将进入睡眠状态直到有一个子进程结束。wait函数返回子进程的PID,statloc中返回子进程的终止状态字。
返回值:如果执行成功则返回子进程进程号PID,如果有错误发生则返回-1,失败原因存于errno中。
函数说明:
在调用wait函数时,还可以通过宏的调用来判断进程是否正常退出和获得子进程的返回值。
●WIFEXITED(status):当子进程正常结束时返回真
●WIFSIGNALED(status):当子进程异常结束时返回真
●WEXITSTATUS(status):当WIFEXITED(status)为真时调用,返回状态字的低8位;
●WTERMSIG(status):当 WIFSIGNALED (status)为真时调用,返回引起终止的信号代号;
wait函数应该和fork函数一起使用,如果在fork函数之前使用wait函数,那么父进程将挂起。
②waitpid函数
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int* statloc,int options);
waitpid的第一个参数pid的意义:
pid>0:等待进程ID为PID的子进程;
pid==0:等待与自己同组的任意子进程;
pid==-1:等待任意一个子进程结束,相当于wait();
pid<-1:等待进程组号为-pid的任意子进程。
因此,wait(&stat)等价于waitpid(-1,&statloc,0)
waitpid的第二个参数statloc是一个整数的指针。如果它不为空,子进程的终止状态字就被存放在该参数指定的内存位置。
如果不关心终止状态字,转入一个空指针就可以了。
waitpid第三个参数option可以是0、WNOHANG、WUNTRACED或者它们的按位或(“|”)。
WNOHANG表示不进入睡眠状态,即如果指定的子进程都还没有僵死掉,立即返回零。不会像wait函数样永远等下去。
WUNTRACED作业控制。子进程进入暂停则马上返回,但结束状态不予理会。
③说明
一个进程调用wait或waitpid函数,可能产生3种情况:
如果所有子进程都还在运行,进程挂起;
如果恰有子进程结束,它的终止状态字正等待父进程提取,立即得到该终止状态字并返回,其返回值为该子进程的进程号;
如果该进程没有子进程,立即返回,返回值为-1。
父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。
(5)system函数
#include<stdlib.h>
int system(const char *cmdstring)
该函数时用fork,exec,waitpid这3个系统函数实现的,返回值相对比较复杂。
●如果cmdstring为空指针,当系统实现了system函数时,返回非零指针,否则返回零,这是个用来测试系统的system函数是否有效的方法。在一般的Linux系统中,system函数都是有效的。
●如果cmdstring不空,就要根据fork,exec,waitpid这3个系统函数的执行情况确定返回值。若fork出错或waitpid中出现非EINTR错误,system返回-1。
●如果exec错误返回,表示shell无法执行这个命令行。返回值与shell执行exit(127)的返回值相同。
●否则,若3个系统函数调用都成功了,返回值为shell的结束状态。
system函数的实现代码:
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>
#include<unistd.h>
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL){
return (1);
}
if((pid = fork())<0){
status = -1;
}
else if(pid == 0){
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
-exit(127); //子进程正常执行则不会执行此语句
}
else{
while(waitpid(pid, &status, 0) < 0){
if(errno != EINTER){
status = -1;
break;
}
}
}
return status;
}
(6)进程的用户标识号管理
每个进程的用户标识号有两个:实际用户标识号和有效用户标识号。
系统提供了一组系统调用,用来管理进程的用户标识号。
系统中的每个进程都有唯一的非负整数作为其标识,它被称为进程标识号(pid)。
系统使用进程标识号来管理当前系统中的进程。
相关函数:
#include <sys/types.h>
#include <unistd.h>
unsigned short getuid0;
unsigned short geteuidO;
unsigned short getgidO;
unsigned short getegidO;
int setuid(uid_t uid);
int setgid(gid_t gid);
参数和功能说明:前4个系统函数没有参数,分别返回调用进程的实际用户标识号、有效用户标识号、实际用户组标识号和有效组标识号。
这些系统调用总能执行成功,不会发生任何错误。系统调用setuid和setgid用于设置进程的实际用户(组)标识号和有效用户(组)标识号。
如调用进程的有效用户标识号是超级用户标识号,则将调用的进程实际用户(组)标识号和有效用户(组)标识号设置为uid或gid,
如调用进程的有效用户标识号不是超级用户标识号,但它的实际用户(组)标识号等于uid或gid时,则其有效用户(组)标识号被置成uid或gid;
如调用进程的实际用户(组)标识号不等于uid或gid,并且它的有效用户标识号不是超级用户标识号,则setuid或setgid调用失败。
调用setuid或setgid成功时,返回零,否则返回-1。
(7)进程标识号管
相关函数:
#include<sys/types.h>
int getpid();
int getpgrp();
int getppid();
int setpgrp();
参数和功能说明:前3个系统函数分别返回调用进程的进程标识号、进程组标识号和其父进程标识号。它们总能成功地返回。
第4个调用设置进程组标识号,它将调用进程的进程组标识号改为调用进程的进程标识号,使其成为进程组首进程,并返回这一新的进程组标识号。
最后
以上就是高挑羊为你收集整理的第五章 进程操作——Linux C的全部内容,希望文章能够帮你解决第五章 进程操作——Linux C所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复