目录
3.2 系统调用获取进程pid和ppid —— getpid(),getppid()
一,什么是进程,有什么用?
1.1 关于进程
教材概念:进程是一段程序的执行过程,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
内核概念:进程是操作系统动态执行的基本单位,是系统资源最基本的分配单元,也是系统最基本的执行单元
1,以我们写的代码为例,我们写完一个.c文件的代码并且编译好后,在Windows是一个.exe后缀的文件在Linux中是一个绿色的文件叫做可执行文件
2,这个时候我们把这玩意儿叫做“程序”,当程序运行起来后,就叫做“进程”。
3,程序本来是存在于磁盘上,当程序运行时会把程序拷贝一份放进内存中供CPU读取,此时程序不应该叫做程序了,而应该叫做进程,且这个时候程序和进程就不相关了
在Windows中我们可以Win+R键再输入taskmgr回车就能打开任务管理器,也可以按住Shift和Ctrl+Esc打开任务管理器查看进程:
在Linux中,我们可以输入下面命令来打开总进程列表:
我们也可以输入top命令打开类似Windows任务管理器那样的动态进程列表:
1.2 进程的重要特性
①竞争性:系统进程数目众多,而CPU资源只有一位数甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理地竞争相关资源,便有了优先级,后面会讲
②独立性:多进程运行,需要独享各种资源,进程与进程运行之间互不干扰
③并行:多个进程在多个CPU下同时运行地现象叫做并行
④并发:多个进程在一个CPU下通过时间片和优先级等方式告诉切换运行,让多个进程在我们用户视角看来是同时运行地现象名叫作并发
二,进程的描述:PCB结构体
2.1 关于PCB
主机开机时启动的第一个进程就是我们的操作系统,OS是第一个加载到内存的。而OS是做管理工作的,有很多上篇博客讲过,其中一个就是“进程管理”。但是我们都直到在OS中存在非常非常多的进程,那么OS是如何对很多个进程进行管理的呢?
①看到“管理”二字,就要想到“先描述,再组织”。
②OS作为管理者(校长)是不需要和被管理者(学生)直接沟通的
③当一个进程被创建时,OS立马对该进程进行描述,把描述一个进程的信息集合起来搞成一个struct结构体
④然后有很多个这样的结构体时,就把这些结构体用链表,树等其他合适的数据结构组织起来,最后OS管理进程就变成了对数据结构的增删查改
⑤这个描述进程的结构体称为 —— PCB(process control block),也叫做“进程控制块”
问题:为什么要先描述呢?属性是数据吗?属性与程序的代码和数据有关系吗?
解答:人认识世界是通过“属性”来认识的,而对应计算机上,“属性”就变成了“数据”,而程序本质上是文件,而文件=内容+属性,程序编程进程时只是把它的内容搞到了内存里,完成内容的拷贝,所以操作系统生成描述进程属性的PCB结构体数据与进程的代码和数据无关
2.2 task_struct
PCB是描述进程的,不同的操作系统中对应PCB的名字不一样,这里我们只讨论Linux下的PCB
Linux是C语言写的,所有在Linux中的PCB叫做task_struct,称为进程描述符,该结构中包含一个具体进程的所有信息。会被装载到内存(RAM)中并包含着进程的属性信息
task_struct相对较大,在32位机器上足足又1.7KB,下面代码来自Linux-2.6.22/include/linux/sched.h,稍微过一遍即可
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 load_weight; /* for niceness load balancing purposes */
int prio, static_prio, normal_prio;
struct list_head run_list;
struct prio_array *array;
unsigned short ioprio;
#ifdef CONFIG_BLK_DEV_IO_TRACE
unsigned int btrace_seq;
#endif
unsigned long sleep_avg;
unsigned long long timestamp, last_ran;
unsigned long long sched_time; /* sched_clock time spent running */
enum sleep_type sleep_type;
unsigned int policy;
cpumask_t cpus_allowed;
unsigned int time_slice, first_time_slice;
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
struct sched_info sched_info;
#endif
struct list_head tasks;
/*
* ptrace_list/ptrace_children forms the list of my children
* that were stolen by a ptracer.
*/
struct list_head ptrace_children;
struct list_head ptrace_list;
struct mm_struct *mm, *active_mm;
/* task state */
struct linux_binfmt *binfmt;
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;
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->parent->pid)
*/
struct task_struct *real_parent; /* real parent process (when being debugged) */
struct task_struct *parent; /* parent process */
/*
* children/sibling forms the list of my children plus the
* tasks I'm ptracing.
*/
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 */
/* 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 */
unsigned int rt_priority;
cputime_t utime, stime;
unsigned long nvcsw, nivcsw; /* context switch counts */
struct timespec start_time;
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
unsigned long min_flt, maj_flt;
cputime_t it_prof_expires, it_virt_expires;
unsigned long long it_sched_expires;
struct list_head cpu_timers[3];
/* process credentials */
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
struct group_info *group_info;
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
unsigned keep_capabilities:1;
struct user_struct *user;
#ifdef CONFIG_KEYS
struct key *request_key_auth; /* assumed request_key authority */
struct key *thread_keyring; /* keyring private to this thread */
unsigned char jit_keyring; /* default keyring to attach requested keys to */
#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;
int oomkilladj; /* OOM kill score adjustment (bit shift). */
char comm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by flush_old_exec */
/* file system info */
int link_count, total_link_count;
#ifdef CONFIG_SYSVIPC
/* ipc stuff */
struct sysv_sem sysvsem;
#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; /* To be restored with TIF_RESTORE_SIGMASK */
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
void *security;
struct audit_context *audit_context;
seccomp_t seccomp;
/* Thread group tracking */
u32 parent_exec_id;
u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings */
spinlock_t alloc_lock;
/* 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 30UL
u64 curr_chain_key;
int lockdep_depth;
struct held_lock held_locks[MAX_LOCK_DEPTH];
unsigned int lockdep_recursion;
#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. */
/*
* current io wait handle: wait queue entry to use for io waits
* If this thread is processing aio, this points at the waitqueue
* inside the currently handled kiocb. It may be NULL (i.e. default
* to a stack based synchronous wait) if its doing sync IO.
*/
wait_queue_t *io_wait;
#ifdef CONFIG_TASK_XACCT
/* i/o counters(bytes read/written, #syscalls */
u64 rchar, wchar, syscr, syscw;
#endif
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_stimexpd;/* stime since last update */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *mempolicy;
short il_next;
#endif
#ifdef CONFIG_CPUSETS
struct cpuset *cpuset;
nodemask_t mems_allowed;
int cpuset_mems_generation;
int cpuset_mem_spread_rotor;
#endif
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;
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
};
代码太多,我们不太需要关心,我们目前只需要直到task_struct立马包含下列信息:
①标识符(pid):描述本进程的唯一标示符,用来区别其他进程
②状态:表示进程所处状态,退出代码,退出信号
③优先级:相对于其它进程的优先级
④程序计数器(pc):程序中即将被执行的下一条指令的地址
⑤内存指针:包括程序代码和进程的相关数据结构的指针,还有和其他进程共享的内存块的指针
⑥上下文数据:进程执行时处理器的寄存器中的数据
⑦IO状态信息:包括显示的IO请求,分配给进程的IO设备和被进程使用的文件列表
⑧记账信息:可能包括处理器的时间综合,使用的时钟总和,时间限制,记帐号等
⑨其他信息
三,进程pid与ppid
3.1 pid是什么?怎么查看?
上面分析task_struct结构体内容时,就已经说过pid是描述进程的唯一标示符,用来区别其他进程
我们可以查看该目录看到pid
根目录下有一个proc的目录,这个目录里面有很多个数字为名字的子目录,这些数字对应的就是各自的进程的pid
文章开头也说过,可以用ps aux指令查看进程
也可以通过top命令查看
当然上面的都是查看所有进程,我们要想单独找一个进程,可以使用grep指令,筛选指定进程名查看指定进程,如下图:
3.2 系统调用获取进程pid和ppid —— getpid(),getppid()
代码编译后产生的可执行文件是程序,存在磁盘上,程序运行后就是进程,所以也会有自己的pid与ppid,ppid提前说下,就是父进程的id,所以我们可以用geipid()和getppid()获取进程的pid和ppid
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("This is a process, pid: %d, ppid: %d\n",getpid(), getppid());
sleep(1);
}
return 0;
}
四,系统调用创建子进程:fork函数
4.1 fork返回值
先看man文档对fork的解释:
如下代码:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("I am parent process:pid:%d\n",getpid());
pid_t ret =fork();
//变成两个进程,一个父进程一个子进程
printf("ret:%d,pid:%d,ppid:%d\n",ret,getpid(),getppid());
return 0;
}
问题:为什么给子进程返回0,给父进程返回子进程pid?
解答: 一个子进程只能有一个父进程,但父进程可以有多个子进程,父进程 : 子进程 = 1 : n
4.2 使用if分流分开父子进程
上面也说到过,fork会给父进程返回子进程pid,一般是正数,会给子进程返回0,所以我们可以使用if来进行分流,如下代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
// 创建子进程失败
perror("fork"); // perror打印fork失败的原因
return 1;
}
else if (id == 0) // 给子进程返回0
{
while (1)
{
printf("I am child, pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
else // 给父进程返回子进程pid,一般是正数
{
while (1)
{
printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
通过if,就能让父子进程都执行各自代码块中的代码了
五,进程状态
5.1 进程为什么会有状态
一个进程从创建至小王的整个生命期间,有时占用处理器执行,有时可运行但分不到处理器,有时处理器虽空间但因某个事件的发送而导致无法运行,这一切说明进程是活动的且随时被调度变化的,所以对于需要对这样的进程状态话以方便OS调度,于是就有了进程状态这一概念
5.2 时间片
当我们写一个while死循环的程序并运行,CPU就会全力去执行我们的死循环,那么CPU如果只执行我们这个程序,而且这个程序永远不会结束,那么其他所有的进程就会直接卡死,因为无法被CPU执行。然而事实是:可能会有点卡顿,但是其他进程还是照样正常运行,就像我们电脑手机可以同时开着微信,微信不退的同时照样可以打开QQ。
这是因为当代计算机操作系统都要一个时间片的概念,CPU会给一个进程一个运行的时间假设为1毫秒,表示你这个进程只能运行1毫秒,运行完就出去重新排队,别的进程还要运行呢。这个1毫秒就是时间片。
时间片:
时间片(timeslice)又称“量子(quantum)”或“处理器片(processor slice)”是操作系统分配给每个正在运行的进程微观上的一段CPU时间。现代操作系统(如Windows,Linux,Mac OS X等)通常可能有多个CPU,但是同一个CPU不可能同时运行多个任务,在只考虑一个CPU的情况下,这些进程“看起来像”是同时运行的,实际上是轮番穿插运行,由于时间片通常很短很短,为纳秒级别,所以用户基本上感觉不到。时间片由操作系统内核的调度程序徐分配给每个进程,当进程时间片耗尽时,内核会重新计算并分配时间片到每个进程
时间片分配:
通常情况下,一个系统中所有的进程被分配到的时间片长短并不是相等的,尽管初始时间片基本相等(在Linux系统中,初始时间片也不相等,而是各自父进程的一半),系统通过测量进程处于“睡眠”和“正在运行”状态的时间长短来计算每个进程的交互性,交互性和每个进程预设的静态优先级(Nice值,后面会讲)的叠加即是动态优先级,动态优先级的比例缩放就是要分配给那个进程的时间片的长短。一般地,为了获得更快的响应速度,交互性强的进程(这类进程一般设计IO操作),被分配到的时间片要长于交互性弱的进程(这类进程趋向于处理器消耗型)
5.3 OS理论体系下进程状态
①新建状态:字面意思(比如游戏新建存档),实际上没有这个状态,因为进程创建出来直接就运行了,该状态只是为了完善OS的理论体系
②运行状态:不是简单的CPU在执行我们的代码,应该是task_struct结构体在运行队列中排队,就叫做运行态(比如吃饭是进程,但是我到食堂要排队,排队期间还没迟到饭,但是排队的这个过程也是吃饭的过程)
③阻塞状态:进程在等待非CPU资源就绪时的状态就叫做阻塞状态。
1,系统中有很多资源的,比如CPU的运行队列,就是每个task_struct在等待CPU的资源,但是系统中还有网卡,磁盘,显卡等等,所以系统不止存在一种队列。比如CPU执行的一个代码要访问磁盘,于是COU就把这个代码放到另一个专门等待访问磁盘的队列里,这个等待的队列就叫做阻塞队列,这个状态就叫做阻塞状态。
2,举个更好的例子,我们scanf的时候,不输入,控制套光标就一直在闪,这其实就是代码在等待键盘数据(非CPU资源),这个庄涛就是阻塞状态,还有就是app运行卡的时候可能就是阻塞在等待网卡资源。
④挂起状态:该状态属于OS的内核状态,内存中内存块不足的时候,OS就会将某些长时间不执行的进程的代码和数据换出到磁盘的SWAP分区,需要用的时候会再拿出来。被暂时放进磁盘的进程所处的状态叫做挂起状态
5.4 Linux下进程状态
Linux系统的源代码中对于进程的状态有下列定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
我们可以用ps aux和ps ajx命令打印的进程列表查看进程状态,对应STAT那一列
①运行状态-R
一个进程处于运行状态,不意味着程序就是在运行中,也可以是在运行队列里,在运行队列里的进程也叫做“可调度进程”,OS需要切换进程运行时,也是直接在运行队列里选取进程运行。
②浅度睡眠状态-S
又叫做“可中断睡眠”,一个进程处于S状态时意味着该进程在等待某件事情的完成,处于浅度睡眠状态的进程可以随时被唤醒,也可以岁时被信号干掉。
我们运行下列代码和脚本命令可以发现该进程处于S状态,并且我们使用kill命令干掉的
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main()
{
printf("i am sleeping...\n");
sleep(1000);
return 0;
}
while :; do ps ajx | head -1 && ps axj | grep test | grep -v grep;sleep 1;done
③深度睡眠状态-D
深度睡眠又叫做不可中断睡眠,表示该进程不会被杀掉,即使是OS也不行,只有该进程自动唤醒才可以恢复,这个状态的进程通常会等待IO的结束
例如,一个进程要对磁盘进行写入,那么如果写入的的内容过多,那么写入需要时间,在这段时间里该进程就处于深度睡眠状态,因为进程需要等待磁盘对写入情况的回复(是否写入成功)以做出相应的应答,所以不会也不能被杀掉。
④暂停状态-T
我们可以发送SIGSTOP也就是19号信号给进程的pid,该进程就会处于stop状态,再次发送SIGCONT也就是18号信号后,进程就会继续运行
但是可以看到暂停再继续运行后,进程从S+状态变为S状态了,“+”没了,这表示把进程放到后台运行了,使用jobs命令可以查看后台进程
这时候要想杀掉进程需要使用kill -9 pid
(t状态也是暂停状态,这个状态一般用于调试,这里不细讲)
⑤死亡状态-X
死亡状态是一个瞬时状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以我们一般不能再任务列表观测到该状态
⑥僵尸状态-Z
僵尸状态的定义为:一个进程已经退出但是还不允许被OS释放,还处于一个被检测的状态
当一个进程将要退出的时候,在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间以供OS或是父进程进行读取,但是如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。
僵尸状态的存在是必要的,因为进程被创建出来就是要去执行任务的,那么创建进程的调用方也是需要知道任务执行的情况的,所以僵尸状态是有必要存在的。
然后就是关于调用方要知道执行情况,我们目前在main函数结尾习惯性地return 0,这个就是返回给OS地,告诉OS代码顺利执行结束,我们可以用ech $?获取最近一次进程退出时地退出码
六,僵尸进程
6.1 僵尸进程产生
#include <stdio.h>
#include <unistd.h>
#include<stdlib.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
// 创建子进程失败
perror("fork"); // perror打印fork失败的原因
return 1;
}
else if (id == 0) // 给子进程返回0
{
int n = 5;
while (n)
{
printf("I am child, pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
n--;
}
printf("chile quit...\n");
exit(1);
}
else // 给父进程返回子进程pid,一般是正数
{
while (1)
{
printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
首先父子进程各自打印pid和ppid,一切正常,当运行五秒后,子进程exit直接退出, 这时候进程就会变成僵尸状态,如下gif
6.2 僵尸进程危害
①僵尸进程的僵尸状态必须一直维持着,因为要等父进程来读取退出信息,如果父进程一直不读取,那么子进程也就一直处于僵尸状态
②进程的退出信息也是保存在PCB结构体中的,那么如果僵尸进程一直不退出,那么该PCB就要一直被维护
③如果一个父进程创建了很多子进程,但都不进行回收就会造成资源浪费,简单来说就是僵尸进程的数量如果越来越多,那么其产生的PCB结构体就越来越多,得不到释放,就会造成内存泄漏,而在大型服务器中内存泄漏是一件非常严重的错误。
④僵尸进程问题可以解决,就和曾经C语言malloc和free要搭配使用一样,对于僵尸进程也有对应的函数来解决,这个函数就是waitpid,叫做进程等待,这个我们到进程控制再详细讲解。
七,孤儿进程
在Linux中的进程大多数都是父子关系,如果父进程没有对资金的退出信息进行读取,进程会变为僵尸状态。那么如果父进程先退出了,那么就再也没有父进程来读取子进程退出信息了,此时子进程就称之为孤儿进程。
和僵尸进程一样,孤儿进程如果不想办法处理,也会造成内存泄漏问题。因为,OS的开发者们针对孤儿进程推出了“领养”的概念,当出现孤儿进程的时候,孤儿进程会被1号进程“领养”,此后孤儿进程进入僵尸状态后就会被int进程进行回收处理。如下代码:
#include <stdio.h>
#include <unistd.h>
#include<stdlib.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
// 创建子进程失败
perror("fork"); // perror打印fork失败的原因
return 1;
}
else if (id == 0) // 给子进程返回0
{
while (1)
{
printf("I am child, pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
else // 给父进程返回子进程pid,一般是正数
{
int n = 5;
while (n)
{
printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
n--;
}
printf("father quit...\n");
exit(0);
}
return 0;
}
我们fork创建子进程,然后父子进程各自打印pid和ppid,但是5秒后父进程退出,此时子进程变为孤儿进程,但是子进程变为孤儿进程的一瞬间被OS识别到,于是子进程被1号进程“领养”,之后我们在左边窗口Ctrl + C想要停止打印发现行不通,因为被1号进程领养后子进程也切换到后台进程去了,可以用kill -9 pid杀掉进程。
(孤儿进程我们只是认识一下,在后面的Tcp网络编程套接字会有大用。)
八,进程优先级
8.1 什么是优先级,为什么要有优先级?
①优先级实际上就是获取某种资源的先后顺序,在前面时间片那块讲过了,进程优先级就是进程获取CPU资源分配的先后顺序,优先级高的进程有优先执行的权力
②进程优先级存在的主要原因是CPU资源是优先的,CPU只能在同一时刻跑一个进程,而进程是有很多个的,所以需要有优先级,这点在前面时间片已经讲过。
③优先级其实是调度器调度的一个主要参考,
8.2 查看进程优先级
我们可以输入ps -l指令,会输出下面内容:
8.3 PRI与NICE值
①PRI是进程的优先级(priority),其值越小优先级越高
②NI代表nice值,可以修正进程被执行的优先级
③PRI值得计算为:新PRI = 旧PRI + NI,在Linux下,旧PRI默认为80,NI默认为0,所以上面的PRI为80
④如果NI为负数,那么该进程得PRI也会变小,其优先级会变高
⑤NI得取值范围是-20到19,一共40个等级,调整进程优先级,其实就是调整进程得nice值。
8.4 更改NICE值
8.4.1 使用top改变
top就是Linux下任务管理器,top之后输入r,它会要你输入进程的pid输入pid后回车就是接着让你输入要调整的nice值了,注意,当输入nice值为正数时正常操作,如果要改回原来的或者将nice设置为负数,代表要提高该进程的优先级,需要root权限,可以用sudo top之后再操作
设置之后再次ps -l就设置成功了:
8.4.2 使用renice改变
renice后面跟上更改后的nice值和进程的pid即可完成更改,和top一样的,如果要提升进程优先级,需要root权限:
九,环境变量(PATH)
9.1 关于环境变量
环境变量地定义是:一般指在操作系统中用来指定操作系统运行环境的一些参数。
环境变量通常具有某些特殊用途,并且在系统中通常具有全局特性
①比如我们编写C/C++代码在各个目标文件进行链接的时候,不论是Windows下还是Linux下,我们都没有显示地去写上我们代码所依赖地动静态库的详细地址,但是我们照样可以链接成功,原因就是有相关的环境变量帮助编译器进行查找
②在编写Java代码的时候,我们也需要把java对应的虚拟机地址填写到Windows的环境变量中,对应的JavaEDA才能正常编译
常见的环境变量有下面三个:
①PATH:指定命令的搜索路径
②HOME:指定用户的主工作目录,即各用户登录到Linux系统时默认所处的目录
③ SHELL:代表当前Shell,它的值通常是/bin/bash
9.2 查看环境变量
我们可以用echo来显示环境变量
9.3 PATH自定义和使用
我们在XShell上使用的各种命令其本质也是可执行程序,都存储在各大bin目录下,但是为何执行命令的时候不需要带任何路径,而我们执行我们自己的写的可执行程序是为什么要带上' ./ ' 呢?
这说明系统有办法自动找到各命令的位置,PATH就是做这个的,前面打印PATH时可以看到很多路径一样的字符,由句号隔开,每一个分割的字符串都是一个地址,我们使用命令时,系统会先去PATH里面的目录里找对应的可执行程序,ls命令就在某一个bin目录下,如下图:
当然,我们也可以让我们自己的可执行程序像命令一样不带路径直接执行,两种方法:
#include <stdio.h>
#include <unistd.h>
#include<stdlib.h>
int main()
{
while(1)
{
printf("this is a proc\n");
sleep(1);
}
return 0;
}
方法一:将可执行程序直接搞到环境变量任意路径下
方法二:将可执行程序所在的路径导入环境变量中
添加路径到环境变量后也不用担心太多,每次终端重启都会重新配置环境变量,当然也有办法把我们指定的路径永久性添加进环境变量,如果不是必要,不建议这么做。
9.4 HOME和SHELL
HOME:
任何一个用户给都要自己主工作目录,HOME环境变量保存的就是该用户主工作目录
SHELL:
Linux中命令有很多,但可以分为两种,一种是不带参数的命令,比如ls,可以直接用的;一种是带参数的,比如gcc编译器,所以实际上我们的命令是需要由命令行解释器进行解释的,在Linux中有多种命令行解释器(比如bash,sh)我们可以查看SHELL环境变量来知道当前所用命令行解释器种类
9.5 其它环境变量
env命令可以显示当前终端所有环境变量:
9.6 环境变量组织方式
其实所谓的环境变量就是存储在系统中的一个字符指针数组,每个指针指向一个以 ' \0 '结尾的字符串,构造大致如下:
每个程序都会收到这样一个char*指针指向的环境变量字符指针数组,那么既然是指针,那么我们可不可以在代码中获取环境变量呢?
9.7 在代码中获取环境变量
9.7.1 main函数的三个参数
main其实也有参数的,只是我们平常不用,没有显示写出来,main函数带参数大概长下面这样:
int main(int argc, char* argv[], char* envp[]);
先说前两个参数,我们前面也说过,Linux是C语言写的,所谓的命令也是Linux的设计者把C语言写的程序编译后形成的可执行文件通过某种方式放到环境变量里去。本质来说:命令其实也是C语言程序。那么我们也知道有些命令要使用后面需要带上参数,那么,命令是如何读取我们在后面带的参数的呢?就是通过main函数的后面两个参数。
如下代码:
#include<stdio.h>
int main(int argc, char* argv[])
{
int i = 0;
for(i = 0;i < argc; i++)
{
printf("argv[]第%d个参数是: %s\n",i ,argv[i]);
}
return 0;
}
结果很明确了,main的第二个参数argv是一个字符指针数组,存储的是可执行程序位置和若干选项,该字符数组大小就是main函数第一个参数argc。
接下来是main的第三个参数,直接抛结论:第三个参数是一个字符指针数组,就是这个参数接收的环境变量的那个指针,所以我们可以自己实现env指令打印全部环境变量,如下代码:
#include<stdio.h>
int main(int argc, char* argv[], char* envp[])
{
int i = 0;
while(envp[i])
{
printf("envp[%d]:%s\n", i, envp[i]);
i++;
}
return 0;
}
除了main的第三个参数,我们也可以使用外部全局变量来获取环境变量,如下代码也可以打印环境变量:
#include<stdio.h>
int main()
{
extern char** environ;
//libc中定义的全局变量environ没有被包含在任何头文件中,所以在使用时要用extern声明
int i = 0;
while(environ[i])
{
printf("environ[%d]:%s\n", i, environ[i]);
i++;
}
return 0;
}
9.7.2 getenv系统调用
getenv可以根据所给的环境变量名,在环境变量表中进行搜索,并返回一个char*指针,如下代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
//也可以获取用户名
printf("user is: %s\n", getenv("USER"));
return 0;
}
十,虚拟地址空间
10.1 内存分块
这张图大家应该都不模式,在C语言内存管理章节已经介绍过,但和那个相比,上面这个图多了一些东西,我们可以用代码进行验证:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int g_unval;
int g_val=100;
int main(int argc, char* argv[],char* env[])
{
printf("code addr:%p\n",main); //main充当代码段
printf("init global addr:%p\n",&g_val); //已初始化
printf("uninit global addr:%p\n",&g_unval); //未初始化
char* heap_men = (char*)malloc(10);
printf("heap addr:%p\n",heap_men); //堆区地址
printf("stack addr:%p\n",&heap_men); //栈区地址
int i=0;
for(i=0;i<argc;i++)
{
printf("argv[%d]:%p\n",i,argv[i]); //命令行参数
}
int j=0;
for(j=0;env[j];j++)
{
printf("env[%d]:%p\n",j,env[j]); //环境变量
}
return 0;
}
10.2 初见虚拟地址
先看下面代码和执行结果:
#include<unistd.h>
#include<stdio.h>
int g_val =100;
int main()
{
pid_t id=fork();
if(id==0) //chile
{
int cnt=0;
while(1)
{
printf("I am child, g_val:%d, &g_val:%p\n", g_val, &g_val);
sleep(1);
cnt++;
if(cnt==3)
{
g_val=200;
printf("child change g_val 100->200 success\n");
}
}
}
else //father
{
while(1)
{
printf("I am father, g_val:%d, &g_val:%p\n", g_val, &g_val);
sleep(1);
}
}
}
代码的逻辑大体上,先定义一个变量g_val,初始值为100,然后fork创建子进程,两个进程都开始打印g_val的值,三秒后子进程将g_val的值从100改为了200,之后,虽然父进程打印100,子进程打印200,但是后面打印的g_val的地址却是一样的,那么该现象表面,父子进程读取同一个地址过去到的值却不同,
所以我们看到的打印出来的地址绝对不是真正存储值得物理地址(比如两个人给我打电话问我在干什么,我说我在吃饭在跑步在打游戏,我不可能同时做这些事情,所以这是不可能发生得)
实际上,我们在语言层面上打印出来得地址都不是物理地址,而是虚拟地址。物理地址由操作系统进行管理 ,用户不能也无权访问。虚拟地址到物理地址得转化也由操作系统完成。
所以几乎所有的语言如果有地址这个概念,那一定是虚拟地址,不是物理地址,因为操作系统需要保证内存的安全性
10.3 进程地址空间
进程地址空间也是虚拟地址,其本质上是操作系统在内存中维护得一种内核数据结构,和PCB一样,不同得操作系统对该数据结构得定义与实现不同,这里我们只讲Linux下得进程地址空间:mm_struct结构体。
地址空间是线性的,由0x00000000到0xffffffff编号,而在这条线上又划分为很多区域,比如堆区,栈区,共享区等等。mm_struct就是记录个这各个区域的边界数值,如下图:
由于虚拟地址是由0x00000000到0xffffffff线性增长的,所以虚拟地址又叫做线性地址。
10.4 页表映射机制
前面有讨论过,操作系统会把虚拟地址转化为物理地址,那么是如何转化的呢?如下图:
10.5 回答10.2的疑问
创建进程时,进程PCB和进程地址空间两个结构体随之被创立,OS可以根据PCB找到进程地址空间,所以当父进程创建子进程时,子进程也有属于自己的PCB和地址空间,这时候父子进程的地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置:
当子进程刚刚被创建的时候,子进程和父进程的数据和代码是共享的,父子进程的代码和数据通过页表映射到物理内存的同一块空间。但是当子进程要将全局变量g_val从100修改到200的时候,将会把父进程的数据另外拷贝一份,然后子进程就映射到这块新拷贝出来的位置,并对这块位置进行修改,如下图:
最后就会发生10.2打印g_val地址相同打印的值不同的情况了
10.6 写时拷贝
像上面这样要进行数据修改时再进行拷贝的计数称为写时拷贝技术。
由于进程具有独立性,所以进程运行时要独享资源,进程间互补干扰,所以像上面的情况,刚开始父子进程指向同一块物理内存的,但是当子进程要修改数据时,为了不影响父进程,于是就有了写实拷贝。
而且不能在创建子进程的时候就进行拷贝,因为进程是流动的,变化的,其中也包括很多进程的创建和结束,而且数据的拷贝是有代价的,比如时间成本,空间成本等,在子进程不对数据进行写入的情况下,没有必要对所有数据进行拷贝,所以有了写实拷贝技术,这样可更高效地使用内存空间,也可以使整机运行效率提高。
十二,一些问题解答:
12.1 fork函数有两个返回值?
在前面我们学习fork时,可以发现 “fork好像有两个返回值”,可以返回0也可以返回子进程pid,为啥?
创建进程地时候,OS多个一个进程,所以要新建一个PCB,而且这个PCB地内部属性要以父进程为模板,这个类似于DNA遗传。然后当fork函数已经执行完核心代码准备return的时候,父子进程已经被创建出来了,所以子进程和父进程都有其独自的代码和数据,fork函数也是一样的,在fork内部,父子进程都会执行各自的return,所以会返回两次两种结果,所以不是有两个返回值,而是被返回了两次,两次结果不同罢了
12.2 什么是上下文数据?
还是前面我们讲时间片的时候,说过一个进程的时间片耗尽后,就会被切换走,放到运行队列里重新排队,那么有个问题,当这个进程再次被CPU运行时,CPU需要从该进程上次被执行暂停的位置继续执行,那么CPU是如何知道上次执行到哪里的呢?
CPU里面存在着大量的寄存器,如果一个进程正在运行,CPU里的寄存器一定保存着该进程的临时数据(寄存器检查EIP或pc程序计数器,记录的是下一行代码的位置),那么这个临时数据就叫做上下文数据,该数据包含执行顺序代码执行中断位置的地址等。
问题:上下文可以被丢弃吗?绝对不可以!当进程A的时间片耗尽了或者来了个优先级更高的进程B,开始切换的时候,需要进程A带走自己的上下文,目的就是为了下次切换回来的时候能够恢复上去,按照之前的逻辑继续运行
12.3 为什么要有地址空间?
凡是非法地访问或映射,OS都会识别出并终止,常量字符串不能被写,出现野指针会崩溃,和文件一样,页表转换时会加载权限,如果一个进程没有写权限,这时候写入就越界了,OS会直接杀掉该进程,野指针也一样,所以所有的进程出现非法操作而崩溃都是OS做的(至于如何做到识别的我们多线程再讲)
①地址空间使得操作系统能够在内存上对用户非法访问内存的行为进行了有效拦截,保护物理内存:因为地址空间和页表都是操作系统创建和维护的,所以想用地址空间和页表进行映射一定要在OS的监管之下进行,有效保护了物理内存中所有的合法数据,包括整个进程以及内核的所有相关数据
问题:因为有了地址空间和页表的映射存在,物理内存中是不是可以对数据进行任意位置的加载?使得,物理内存的分配和进程的管理没有关系
②地址空间完成了内存管理模块和进程管理模块的解耦,所以我们在C/C++上new或者malloc的时候都是在虚拟地址上申请的。
问题:如果我申请了物理内存但是不立马使用,是不是造成了空间浪费?是的,因为物理内存优先,所以申请内存的时候都是在地址空间上申请的,物理内存不会给你一个字节,真正进行物理地址空间访问的时候,才会执行内存的相关管理算法,帮你申请构建页表和映射关系。再让进程进行访问,这种类似写时拷贝的技术叫做“缺页中断”(此操作由操作系统完成,用户0感知)
③有了地址空间,OS就可以采用延迟分配的策略,提升整机的效率,保证内存的100%有效使用
问题:数据可以在物理内存上任意位置加载,那么是不是物理内存中几乎所有的代码和数据都是乱序的?而且因为页表存在,他讲虚拟地址映射到物理内存时,再进程视角的所有内存分布可以有序吗?
地址空间和页表的存在可以将内存分布有序化,有了地址空间,每一个进程都任务自己独享内存,就这样进程的独立性可以通过地址空间+页表的方式实现,使进程更合理地使用内存空间
④有了地址空间,我们编写代码地时候只需要关注虚拟内存地址,无需再关注数据在物理内存当中中实际地存储位置。
12.4 重新理解挂起
加载的本质就是创建进程,那么是不是非得立马把进程的所有代码和数据立马加载到内存中并创建内核数据结构建立映射关系呢?不是的,比如某款3A大作游戏有70个G,但是我们内存只有16G,所以全部加载进去是不可能的。
结论就是,加载大型项目时可以分批加载或换入,也可以分批换出,而进程的代码和数据被换出的时,叫做挂起。(页表映射不仅仅能映射内存,也能够映射磁盘)
12.5 创建进程时,操作系统做了什么?(面试常问)
首先程序 = 文件内容 + 属性,存在磁盘上,要创建进程,首先就要把程序的代码和数据加载到内存中,紧接着创建对应的PCB结构体来描述进程,然后就是创建出进程的地址空间和页表建立映射关系,这一切做好之后,OS把进程的PCB结构体放进CPU的运行队列里排队等待被调度运行。(该回答会根据学校内容增多改变)