进程管理学习笔记

第三章 进程管理

3.1 进程

内核调度的对象是线程而不是进程。linux的线程实现十分特别,对线程和进程并不特别区分,而是将线程视为一种特殊的线程。线程之间可以共享虚拟内存,但是每个都拥有自己的虚拟处理器。

fork()系统调用从内核返回两次:一次回到父进程,另一次回到新的子进程。现代linux内核中,fork()实际上是由clone()系统调用实现的。最终,程序通过exit()系统调用退出执行,父进程用wait4()查询子进程是否终结,进场退出后被设置为僵死,直到父进程调用wait()或waitpid()。

3.2 进程描述符以及任务结构

task_struct

内核把进程的列表存放在任务队列的双向循环链表中。链表中的每一项都是类型为task_struct,称为进程描述符的结构,包含一个进程的所有信息。

#ifdef: 判断某个宏是否被定义,若已定义,执行随后的语句

#endif: #if, #ifdef, #ifndef这些条件命令的结束标志.

struct task_struct // include/linux/sched.h 723-1507行

struct thread_info {
	struct task_struct	*task;		/* main task structure */
	unsigned long		flags;		/* low level flags */
	unsigned long		status;		/* thread-synchronous flags */
	__u32			cpu;		/* current CPU */
	__s32			preempt_count;	/* 0 => preemptable,< 0 => BUG*/

	mm_segment_t		addr_limit;	/* thread address space */

	unsigned long		cpenable;
#if XCHAL_HAVE_EXCLUSIVE
	/* result of the most recent exclusive store */
	unsigned long		atomctl8;
#endif

	/* Allocate storage for extra user states and coprocessor states. */
#if XTENSA_HAVE_COPROCESSORS
	xtregs_coprocessor_t	xtregs_cp;
#endif
	xtregs_user_t		xtregs_user;
};

内核通过一个唯一的进程标识值或者PID来表示每个进程。**内核中,访问任务一般要直接通过task_struct来进行,需要current宏来查找当前正在运行的进程描述符的素的十分重要。**而这对于不同体系结构,做法不一样,对于寄存器数量足够的体系结构来说,可以单独拿一个寄存器放task_struct的指针,而对于x86这种寄存器数量少的来讲,就利用thread_info来计算偏移间接查找。

进程上下文

当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当发生进程调度时,进行进程切换就是上下文切换(context switch)。操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。用户态内陷内核态三种方式:系统调用,异常,中断。

进程家族树

linux系统下,所有的进程都是PID为1的init进程的后代。每个进程必有一个父进程。

3.3 进程创建

unix与众不同的实现方式,使用fork与exec分开依次执行。fork拷贝当前进程创建一个子进程,父子之间只差PID,PPID与某些资源和统计量。exec则负责读取可执行文件并载入地址空间开始执行。

linux采取的写时拷贝,把数据拷贝动作推迟到对子进程进行写入时才执行。其不需要写入时,则只是以只读方式共享,这样可以有效降低拷贝这一行为带来的资源浪费。这时fork的实际开销变成了复制父进程页表和给子进程创建唯一的进程描述符。有利于进程的快速执行。

fork与vfork

1.fork(): 子进程拷贝父进程的数据段,代码段

vfork(): 子进程与父进程共享数据段

2.fork(): 父子进程的执行次序不确定

vfork():保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。

3.vfork()保证子进程先运行,在她调用exec或exit之后,父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

为什么会有vfork,因为以前的fork很傻,它创建一个子进程时,将会创建一个新的地址空间,并且拷贝父进程的资源,而往往在子进程中会执行exec调用,这样,前面的拷贝工作就是白费力气了,这种情况下,聪明的人就想出了vfork,**它产生的子进程刚开始暂时与父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中运行,所以子进程不能进行写操作,**并且在儿子 霸占”着老子的房子时候,要委屈老子一下了,让他在外面歇着(阻塞),一旦儿子执行了exec或者exit后,相于儿子买了自己的
房子了,这时候就相于分家了。

#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>

int main()
{
	pid_t pid;
	int cnt = 0;

	pid = fork();
	if(pid < 0)
		printf("error in fork!\n");
	else if(pid == 0) {
		cnt ++;
		printf("cnt=%d\n",cnt);
		printf("I'm the children process, ID is %d\n", getpid());
	} else {
		cnt ++;
		printf("cnt=%d\n",cnt);
		printf("I'm the parent process, ID is %d\n", getpid());
	}

	return 0;
}

/*输出为
cnt=1
I'm the parent process, ID is 14354
cnt=1
I'm the children process, ID is 14360
原因:fork子进程拷贝父进程的数据段代码段,所以子进程执行的cnt是自己的,父进程也是自己的,都是0;
*/
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>

int main()
{
	pid_t pid;
	int cnt = 0;

	pid = vfork();
	if(pid < 0)
		printf("error in fork!\n");
	else if(pid == 0) {
		cnt ++;
		printf("cnt=%d\n",cnt);
		printf("I'm the children process, ID is %d\n", getpid());
		_exit(0);
	} else {
		cnt ++;
		printf("cnt=%d\n",cnt);
		printf("I'm the parent process, ID is %d\n", getpid());
	}

	return 0;
}
/*想要cnt变为2,就要用vfork让两个进程共享数据,但一定要记得使用exit退出,否则父进程无法执行*/

3.4 线程在linux下的实现

在window等系统下,线程也可以被称为轻量化进程。但对于本身就轻亮的的linux来说,它只是一种进程间共享资源的手段。

3.5 进程终结

当系统调用了do_exit()之后,尽管线程已经僵死不能再运行了,但系统还是保留了它的进程描述符。这样系统就有办法让子进程在终结后仍然可以获得它的消息。因此进程终结后的清理工作与进程描述符的删除被分开执行。

如果父进程在子进程之前退出,必须有机制保证子进程找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处在僵死状态,白白消耗内存。解决方法是给子进程在当前线程组找一个线程做父亲,不行就让init来做。

这里涉及到了僵尸进程与孤儿进程的概念,现在我们来介绍一下。

1.僵尸进程:为了让父进程随时能调取到子进程的资料,子进程结束后并没有回收它的进程描述符,而如果此时子进程退出,但父进程没有用wait或者waitpid获取子进程,那么这个时候进程描述符不会被释放,子进程就会成为僵尸进程。大量的僵尸进程会导致资源以及进程号被占用,从而导致系统不能产生新的进程,需要被避免。

/*生成僵尸进程*/
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>

int main()
{
	pid_t pid;

	pid = fork();
	if (pid < 0) {
		perror("fork failed!\n");
		exit(1);
	} else if (pid == 0) {
		/*子进程先退出,把进程描述符留下*/
		printf("I am child process.I am exiting.\n");
		exit(0);
	}
	/*等待子进程先退出*/
	printf("I am father process.I will sleep two seconds\n");
	sleep(2);
	system("ps -o pid,ppid,state,tty,command | grep Z");
	printf("father process is exiting.\n");
	return 0;
}

父进程一次fork()后产生一个子进程随后立即执行waitpid(子进程pid, NULL, 0)来等待子进程结束,然后子进程fork()后产生孙子进程随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵尸进程了。

#include<sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main()
{
    pid_t  pid;
    //创建第一个子进程
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    //第一个子进程
    else if (pid == 0)
    {
        //子进程再创建子进程
        printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
        pid = fork();
        if (pid < 0)
        {
            perror("fork error:");
            exit(1);
        }
        //第一个子进程退出
        else if (pid >0)
        {
            printf("first procee is exited.\n");
            exit(0);
        }
        //第二个子进程
        //睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
        sleep(3);
        printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
        exit(0);
    }
    //父进程处理第一个子进程退出
    if (waitpid(pid, NULL, 0) != pid)
    {
        perror("waitepid error:");
        exit(1);
    }
    exit(0);
    return 0;
}

2.孤儿进程:当子进程还在工作时,父进程死掉了。这个时候子进程会成为孤儿进程,按照同组线程抽一个或者init为父亲的标准被人收养。孤儿进程并不会有什么危害,它们的新父亲会为他们收尸。

#include<stdio.h>
#include<stdlib.h>
#include<error.h>
#include<unistd.h>

int main()
{
	pid_t pid;

	pid = fork();
	if(pid < 0) {
		perror("fork failed!\n");
		exit(1);
	} else if (pid == 0){
		printf("I am the child process.\n");
		//输出进程ID和父进程ID
		printf("pid: %d\tppid:%d\n",getpid(),getppid());
		printf("I will sleep five seconds.\n");
		//睡眠5s,保证父进程先退出
		sleep(5);
		printf("pid: %d\tppid:%d\n",getpid(),getppid());
		printf("child process is exited.\n");
	} else {
		printf("I am father process.\n");
		//父进程睡眠1s,保证子进程输出进程id
		sleep(1);
		printf("father process is  exited.\n");
	}
	return 0;
}

补充!进程线程

进程与线程的区别

(1)进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
(2)进程有自己的独立地址空间,线程共享进程中的地址空间。
(3)进程的创建消耗资源大,线程的创建相对较小。
(4)进程的切换开销大,线程的切换开销相对较小。

线程库中的接口介绍

#include <pthread.h>
/*
pthread_create()用于创建线程
thread: 接收创建的线程的 ID
attr: 指定线程的属性//一般传NULL
start_routine:指定线程函数
arg: 给线程函数传递的参数
成功返回 0, 失败返回错误码
*/
int pthread_create(pthread_t * thread, const pthread_attr_t *attr,void *(*start_routine) ( void *),void *arg);



/*
pthread_exit()退出线程
retval:指定退出信息
*/
int pthread_exit( void *retval);



/*
pthread_join()等待 thread 指定的线程退出,线程未退出时,该方法阻塞
retval:接收 thread 线程退出时,指定的退出信息
*/
int pthread_join(pthread_t thread, void **retval);

##代码

pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,

[xt@xt-QiTianM450-N000:~/Desktop/recent/Linux/code]$ gcc -o pthread pthread.c -lpthread
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>

void* pthread_fun(void* arg)
{
	for (int i = 0; i < 5; i++) {
		printf("fun run\n");
		sleep(1);
	}
}

int main()
{
	pthread_t pid;
	int res = pthread_create(&pid, NULL, pthread_fun, NULL);
	assert(res==0);

	for(int i = 0; i < 10; i++)
	{
		printf("main run\n");
		sleep(1);
	}

	exit(0);
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>

void *pthread_fun(void* arg)
{
	for (int i = 0; i < 10; i++) {
		printf("fun run\n");
		sleep(1);
	}
}

int main(void)
{
	pthread_t tid;
	int res = pthread_create(&tid, NULL, pthread_fun, NULL);

	for (int i = 0; i < 5; i++) {
		printf("main run\n");
		sleep(1);
	}

	return 0;
}

主线程结束后,子线程并没有打印完也紧跟着结束了。
所以,**主线程不会因为其他线程的结束而结束,但是其它线程的结束会因为主线程的结束而结束。**这是因为主线程结束后会退出进程,所以进程里的其他线程都会终止结束。所以为了正常运行程序,一般我们都会让主线程等待其他线程结束后再结束。代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>

void* pthread_fun(void* arg)
{
    for(int i = 0; i < 10; i++)
    {
        printf("fun run\n");
        sleep(1);
    }
    pthread_exit(NULL);
}

int main()
{

    pthread_t tid;
    int res = pthread_create(&tid,NULL,pthread_fun,NULL);
    assert(res == 0);

    for(int i = 0; i < 5; i++)
    {
        printf("main run\n");
        sleep(1);
    }

    char* s = NULL;
    pthread_join(tid, (void**)&s);
    exit(0);
}

其他线程没有结束的话,主线程会在pthread_join()处阻塞。所以主线程会在其他线程结束之后再结束,程序正常退出。

线程并发运行

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>

void* pthread_fun(void* arg)
{
	int index = *(int*) arg;
	int i = 0;
	for (; i < 5; i++) {
		printf("index = %d\n", index);
		sleep(1);
	}
}

int main()
{
	pthread_t id[5];
	int i = 0;
	for (; i < 5; i++) {
		printf("i is %d\n", i);
		pthread_create(&id[i], NULL, pthread_fun, (void*)&i);
	}

	for (i = 0; i < 5; i++) {
		printf("i is %d\n", i);
		pthread_join(id[i], NULL);
	}

	exit(0);
}

下面是本人十分精妙的总结~
首先我们先看运行结果

i is 0
i is 1
index = 1
i is 2
index = 2
i is 3
index = 3
i is 4
index = 4
i is 0
index = 0
index = 1
index = 3
index = 2
index = 0
index = 4
index = 1
index = 3
index = 2
index = 0
index = 4
index = 1
index = 3
index = 0
index = 2
index = 4
index = 1
index = 0
index = 3
index = 2
index = 4
i is 1
i is 2
i is 3
i is 4

首先我们要分析的是,为什么线程的打印是混乱的。由于线程创建是需要时间的,而在主函数中的create函数并不是等到创建整个过程完成才进入下一个循环,而是得到可以创建的回复就立刻开始循环。而我们传给func的参数是一个指针,也就是说index其实并不是一个形参,而是要读取i地址里面的值来确定当前的i的值。而由上面这个结果可以分析,当i到了第二轮也就是下达创建第二个线程的指令时,第一个线程才刚刚创建好,才进入打印index的环节。

而之后顺序仍然是没有规律的,则是因为为了防止主线程结束后,子线程还没有执行完就被强行中止,我们采取了join函数来阻塞,要求主线程等到子线程执行完成后才可以继续。而按照线程创建顺序以及for循环顺序,第一个等待的则是线程0号,可以我们可以看到先打印出了0,接着五个子线程继续一起打印,而主线程停在原地等待第0号线程执行完毕,而由于五个线程创建时间十分接近,当第一个线程执行完时,其余四个也基本立刻执行完毕,所以一从0号的阻塞出来,其余四个也已经执行完毕不用再等待,直接打印即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值