进程ID
每个进程都有一个非负整型表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性。虽然是唯一的,但是进程ID是可复用的。当一个进程终止后,其进程ID就成为复用的候选者。
#include<unistd.h>
pid_t getpid(void); // 返回值:调用进程的进程ID
pid_t getppid(void); // 返回值:调用进程的父进程ID
uid_t getuid(void); // 返回值:调用进程的实际用户ID
uid_t geteuid(void); // 返回值:调用进程的有效用户ID
gid_t getgid(void); // 返回值:调用进程的实际组ID
gid_t getegid(void); // 返回值:调用进程的有效组ID
函数fork
一个现有的进程可以调用fork函数创建一个新进程。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
if (pid > 0) {
printf("main process: pid = %d, getpid() = %d, getppid() = %d\n", pid, getpid(), getppid());
} else if (pid == 0) {
printf("Sub process: pid = %d, getpid() = %d, getppid() = %d\n", pid, getpid(), getppid());
} else {
// error
}
for (int i=0; i<3; i++) {
sleep(1);
}
return 0;
}
fork函数为什么会有两个返回值?
在上图中,当调用fork函数时,操作系统从用户态切换回内核态来进行进程的创建,依次调用fork函数中的CREATE函数和CLONE函数。
首先调用CREATE函数,子进程进行虚拟地址申请,在子进程的内核空间中进行不完全拷贝,为什么是不完全拷贝?因为PCB(进程控制块)作为每个进程的唯一标识符,就像每个人的身份证一样,所以这个地方时不完全拷贝,如pid就需要自己生成。
之后调用CLONE函数,向父进程拷贝必要资源,子进程的用户空间进行完全拷贝,子进程继承所有父进程资源,如临时数据堆栈拷贝,代码完全拷贝。
大体来说,fork函数可以分为三步:
1. 调用_CREATE函数,也就是进程的创建
2. 调用_CLONE函数,也就是资源的拷贝
3. 进程创建成功,return 子进程ID;失败,return -1
前2步是父进程通过fork函数创建子进程的步骤,在执行完_CLONE函数后,fork函数会有第一次返回,子进程的pid会返回给父进程。
需要注意的是,在第3步中,fork函数不是由父进程来执行,而是由子进程来执行,当父进程执行完_CLONE函数后,子进程会执行fork函数的剩余部分,执行完剩余部分之后,fork函数就会第二次返回,如果成功就返回0,失败就返回-1。
总结得出,父子进程都执行fork函数,但执行不同的代码段,获取不同的返回值。所以fork函数的返回值情况如下:
父进程调用fork,返回子线程pid(>0)
子进程调用fork,子进程返回0,调用失败的话就返回-1
整个Linux操作系统都是有父子进程结构构成的,每个进程都有创建者,也就是父进程,但有一个进程例外,也就是init进程,init进程是系统启动初始化后的第一个进程。
一个进程的子进程可以有多个,一个进程只会有一个父进程,子进程总是可以调用getppid以获得其父进程的进程ID
子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。
函数vfork
vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中;vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。
函数exit
进程有5种正常终止及3种异常终止方式。
5种正常终止方式具体如下。
(1)在main函数内执行return语句。
(2)调用exit函数。
(3)调用exit或Exit函数。
(4)进程的最后一个线程在其启动例程中执行return语句。
(5)进程的最后一个线程调用pthread_exit函数。
3种异常终止具体如下。
(1)调用abort。它产生SIGABRT信号。
(2)当进程接收到某些信号时。
(3)最后一个线程对“取消”(cancellation)请求作出响应。
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
如果进展正常终止,子进程会将其退出状态作为参数传递给父进程,
如果异常终止,内核产生一个指示起异常终止原因的终止状态。
在任意情况下,父进程都能用wait函数取得其终止状态。
如果父进程在子进程之前终止,又将如何?父进程会改变为init进程,称其为这些进程由init进程收养或者托管。
函数wait和waitpid
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件,所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。系统默认动作是忽略它。
现在需要知道的是调用wait或waitpid的进程可能会发生什么。
- 如果其所有子进程都还在运行,则阻塞。
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
- 如果它没有任何子进程,则立即出错返回。
#include <sys/wait.h>
pid_t wait(int*statloc);
pid_t waitpid(pid_t pid, int*statloc, int options);
// 返回值:若成功,返回进程ID;若出错,返回0或−1
这两个函数的区别如下。
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞。
- waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。
函数exec
提及用fork函数创建新的子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
有7种不同的exec函数可供使用,它们常常被统称为exec函数,我们可以使用这7个函数中的任一个。
#include <unistd. h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
// 返回值:若出错,返回−1;若成功,不返回
函数system
假定要将时间和日期放到某一个文件中,可以调用time得到当前日历时间,接着调用localtime将日历时间变换为年、月、日、时、分、秒、周日的分解形式,然后调用strftime对上面的结果进行格式化处理,最后将结果写到文件中。但是用system函数则更容易做到这一点:
system(“date>file”);
#include <stdlib. h>
int system(const char *cmdstring);
如果cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0值,这一特征可以确定在一个给定的操作系统上是否支持system函数。
因为system在其实现中调用了fork、exec和waitpid,因此有3种返回值。
(1)fork失败或者waitpid返回出错,则system返回−1,并且设置errno以指示错误类型。
(2)如果exec失败(表示不能执行shell),则其返回值如同shell执行了exit(127)一样。
(3)否则所有3个函数(fork、exec和waitpid)都成功,那么system返回值是设立了的终止状态。
#include <sys/wait.h>
#include <erron.h>
#include <unistd.h>
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL) {
return (1); //如果cmdstring为空,返回非零值,一般为1
}
if((pid = fork()) < 0) {
status = -1; //fork失败,返回-1
} else if(pid == 0) {
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
_exit(127); //exec执行失败返回127,注意exec只在失败时才返回现在的进程,成功的话现在的进程就不存在啦
} else {//父进程
while(waitpid(pid, &status, 0) < 0) {
if(errno != EINTR) {
status = -1; //如果waitpid被信号中断,则返回-1
break;
}
}
}
return status; //如果waitpid成功,则返回子进程的返回状态
}
信号
信号是软件中断。很多比较重要的应用程序都需处理信号。信号提供了一种处理异步事件的方法,例如,终端用户键入中断键,会通过信号机制停止一个程序,或及早终止管道中的下一个程序。
每个信号都有一个名字。这些名字都以3个字符SIG开头。例如,SIGABRT是夭折信号,当进程调用abort函数时产生这种信号。SIGALRM是闹钟信号,由alarm函数设置的定时器超时后将产生此信号。
守护进程
守护进程(daemon)是生存期长的一种进程。它们常常在系统引导装入时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是在后台运行的。
进程和线程
在Linux环境下,每个进程有自己各自独立的 4G 地址空间,大家互不干扰对方,如果两个进程之间通信的话,还需要借助第三方进程间通信工具 IPC 才能完成。不同的进程通过页表映射,映射到物理内存上各自独立的存储空间,在操作系统的调度下,分别轮流占用CPU去运行,互不干扰、互不影响,甚至相互都不知道对方。在每个进程的眼里,CPU就是他的整个世界,虽然不停地被睡眠,但是一旦恢复运行,一觉醒来,仿佛什么都没发生过一样,认为自己拥有整个CPU,一直在占有它。
在一个进程中,可能存在多个线程,每个线程类似于合租的每个租客,除了自己的私有空间外,还跟其它线程共享进程的很多资源,如地址空间、全局数据、代码段、打开的文件等等。在线程中,通过各种加锁解锁的同步机制,一样可以用来防止多个线程访问共享资源产生冲突,比如互斥锁、条件变量、读写锁等。
一、进程与线程的区别:
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位(也可以理解为进程当中的一条执行流程)
- 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
- 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
- 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
二、函数对应![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b57ffa2287f9ecb297c45e17f6d1568a.png#pic_center)
三、优缺点![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/2e1edb6e3a1a2d9f41c58ae4cb4d78f3.png#pic_center)
四、有了多进程,为什么还要有多线程
和进程相比,它是一种非常“节俭”的多任务操作方式。在Linux系统中,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护其代码段、堆栈段和数据段,这种多任务工作方式的代价非常“昂贵”。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且线程间彼此切换所需要时间也远远小于进程间切换所需要的时间。
线程间方便的通信机制。对不同进程来说它们具有独立的数据空间,要进行数据的传递只能通过IPC进行。这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其他线程所用,方便又快捷。