用于创建子进程。
#include
#include
pid_t fork(void)
返回值:fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中.
在父进程中返回创建的子进程的ID,在子进程中返回0。
【如何区分父进程和子进程】通过fork的返回值。
【注意1】在父进程中返回-1,表示创建子进程失败,并且设置errno
在实际的fork
之后,就相当于两个进程去同时执行下面的代码了。如果想要父进程和子进程分别做自己想做的,就可以通过fork
返回的pid号来if
区别执行程序。
例如:
pid_t pid = fork();
if(pid > 0){
cout << "i am parent process" << endl;
}
else if(pid == 0) {
cout << "i am child process" << endl;
}
【注意2】如果直接多个fork,那么其实会产生2的n(fork函数的个数)次方-1
个子 进程。如
fork();
fork();
//执行结束后,总共有四个进程(包括主进程)
如果只想创造两个子进程
,则可以:
pid_t pid;
for(int i = 0; i < 2; i++) {
fork();
if(pid == 0) break;
}
//总共三个进程(包括主进程)
这个函数是属于c语言的函数:
#include
void exit(int status);
status是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
其执行的流程如下:
理论上,fork之后,子进程的用户区数据和父进程是一样的。内核区数据除了pid号也是一样的,也就是拷贝过去。
但为什么说是“理论上”呢?
因为实际上采用的是《读时共享,写时复制》的技术,即:
【定义】父进程运行结束.但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(orphan Process) 。
iniI
,而init
进程会循环地 wait()
它的已经退出的子进程。因此,当孤儿进程结束的时候,其内核的空间就回由init
进程来帮忙释放了。每个进程结束之后,都会释放自己地址空间中的用户区数据,内核区的 PCB没有办法自己释放掉,需要父进程去释放。当子进程终止时,父进程(可能仍然在忙)尚未回收.子进程残留资源(PCB)存放于内核中,变成僵尸( Zombie)进程。
PS:僵尸进程不能被kill -9
杀死。
这样就会导致一个问题,如果父进程不调用wait()
或waitpid ()
的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
调用wait函数的进程会被挂起(阻塞),知道它的一个子进程退出或者收到一个不能被忽略的信号
(与下面的信号量有关)时,才被唤醒(相当于继续向下执行)。若没有子进程或者子进程都已经结束了,则会返回-1。
#include
#include
pid_t wait(int *wstatus);
其中,wstatus是指进程退出时的状态信息,传入的时一个int类型的地址,传出参数(即子进程回改变这个状态值
返回出来)。[若为null
则表示不在乎子进程返回什么]
【返回值】
回收指定进程号的子进程,可以设置是否阻塞。
#include
#include
pid_t waitpid(pid_t pid, int *wstatus, int options);
【参数】
wait
(最常用)【返回值】
options=WNOHANG
,表示还有子进程。主要分为两大类:
管道也叫无名(匿名)管道,它是是UNIX系统IPC(进程间通信)的最古老形式,所有的UNIX系统都支持这种通信机制。
【案例】统计一个目录中文件的数目命令: ls | wc -l
,为了执行该命令, shell创建了两个进程来分别执行ls(获得目录) 和wc(统计个数)。
由ls产生得到的内容的进程,通过|
管道符,从(管道写段)将内容传送给wc的进程(管道读端)。
【特点】
内核
内存中维护的缓冲器,这个缓冲器的存储能力是有限的, 不同的操作系统大小不一定相同。lseek ()
来随机的访问数据。【为什么可以使用匿名管道进行进程间通信】
因为父进程刚fork
时,一开始的文件描述符都是共享的。而管道就是相当于把这个共享的文件描述符换成了管道。因此可以实现一个写一个读。
【管道的数据结构】
由线性队列来设计的循环队列。有一个读指针和一个写指针。
【使用】
创建一个匿名管道,用来进程间通信。
#include
int pipe(int pipefd[2]);
//成功则返回0,失败则返回-1
read()
函数阻塞)write()
函数阻塞)【查看管道缓冲大小】shell命令
ulimit -a
# 会显示有几块,以及每块的字节大小
该命令的中间一列会出现-x
的选项,通过加该选项,便可修改其类型的大小。例如:
那么此时,如果我向修改pipe size的值,由8改为16。那么我可以通过
ulimit -p 16
来修改。
【注意事项】
写
的话,就要close(pipefd[0])
把读端关闭。read
的时候,会阻塞write
的时候,会阻塞,直到管道中有空位置才能再次写入数据并返回。fcntl
函数来将管道的文件描述符设置为非阻塞来避免。匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO) ,也叫命名管道、FIFO文件。
open
函数打开它),就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/o系统调用了(如read()
、write()
和close()
)。与管道―样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO的名称也由此而来:先入先出
。lseek()
等文件定位操作。【使用之创建】创建fifo文件的两个方法:
1、使用shell命令
mkfifo XXX(名字)
2、使用函数
#include
#include
int mkfifo(const char *pathname, mode_t mode);
//成功创建则返回0,失败返回-1并设置errno
参数:
in
、out
、app
(尾部添加数据)、trunc
(清空内部存储的所有数据)、ate
(打开一个已有的文件,并将文件读指针指向文件末尾)等选项)。【使用之打开】打开管道直接就使用open()
打开管道,可以根据是该进程操作管道写还是读,来决定open()
的第二个参数是只读还是只写。
【注意事项】
fifo
的注意事项和pipe
的注意事项2、3情况都是一样的!例如:若所有指向管道读段的文件描述符都关闭了(即另一个进程的read关闭了),这个时候有向管道中写数据,那么该进程会收到一个信号SIGPIPE,通常会导致写进程异常终止。内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改
内存就能修改磁盘文件。因此,只要两个进程同时映射同一个内存区域,从而实现通信。
将一个文件或者设备的数据映射到内存中
#include
void* mmap(void* addr,size_t length,int prot,int flags,int fd, off_t offset);
//成功,返回创建的内存的首地址
//失败,则返回MAP_FAILED---这个其实就是(void*) -1。
参数:
lseek(fd, 0, SEEK_END)
】去获取文件的长度)。PS:这个并不是给多少,就是多少。是会以分页的大小为单位,生成一个整数倍的length。|
运算符来并起来)
用完内存映射记得释放。
int munmap(void *addr, size_t length);
参数:
【案例】使用内存映射实现文件拷贝的功能(因为内存不大,所以并不常用)
思路:
truncate("cpy.txt", len)
)不需要文件实体进程一个内存映射,因此也只能是用于父子进程之间的通信。
只不过要在flag上加入选项MAP_ANONYMOUS
,fd随意指定(反正会忽略),然后偏移量必须为0。即:
mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
信号是 Linux进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断.它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。
【概念】信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断.转而处理某一个突发事件(发往进程的诸多信号,通常都是源于内核)。
【使用信号的两个主要目的】
信号名称 | 对应的事件 | 默认动作 |
---|---|---|
SIGINT | 当用户按下了 |
终止进程 |
SIGQUIT | 当用户按下了 |
终止进程 |
SIGKILL | 无条件终止进程。该信号不能被忽略,处理和阻塞 | 终止进程 |
SIGSEGV | 指示进程进行了无效内存访问(段错误) | 终止进程并产生core文件 |
SIGPIPE | pipe向一个没有读端的管道写数据 | 终止进程 |
SIGCHID | 子进程结束时,父进程会收到这个信号 | 忽略这个信号(父进程可以靠处理这个信号来消除僵尸进程) |
SIGCONT | 如果进程已停止,则使其继续运行 | 忽略或者继续 |
SIGSTOP | 停止进程的执行。信号不能被忽略,处理和阻塞 | 暂停进程 |
【信号的五种默认处理动作】
【信号的几种状态】
产生、未决(还没到达)、递达(已到达处理)
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为sigset_t
。
在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”(处于未决之后,但未被处理,它是能够人为产生的),另一个称之为"未决信号集”(处于未决的状态,它无法人为地产生)。这两个信号集都是内核使用位图机制(即二进制的每一个位表示一个信号是否处于这个集合所代表的状态)来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。
而两个信号集的关系为:
这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集)﹐进行比较。如果没阻塞就会被处理;否则该信号会继续处于未决状态直至阻塞解除。
【创建一个信号集】这个信号集可以充当阻塞信号集!(通过《下章节介绍的函数》,系统内核根据这个定义在用户区的信号集区修改内核的阻塞信号集)也可以充当未决信号集(通过《下章节介绍的函数》,来承接未决信号集的信息。)
sigset_t set;
【sigemptyset函数】
清空信号集中的数据,将信号集中的所有的标志位置0
int sigemptyset(sigset_t *set);
//成功返回0,失败返回-1
【sigfillset函数】
将信号集中所有的标志位置1
int sigfillset(sigset_t *set);
//成功返回0,失败返回-1
【sigaddset函数】
-功能:设置信号集中的某一个信号对应的标志位为1(也可以看作将该信号加入到自定义的信号集中)
int sigaddset(sigset_t *set, int signum);
//成功返回0,失败返回-1
【sigdelset函数】
-功能:设置信号集中的某一个信号对应的标志位为0(也可以看作将该信号在自定义的信号集中删掉)
int sigdelset(sigset_t *set, int signum);
//成功返回0,失败返回-1
int sigismember(const sigset_t *set, int signum ) ;
//1表示signum位为1;0表示signum位为0;-1表示调用失败
将自定义信号集应用到内核的阻塞信号集(设置阻塞,解除阻塞,替换)
【sigprocmask函数】
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//成功返回0,失败返回-1,并设置错误号(EFAULT、EINVAL)
参数:
mask
,则操作为mask | set)mask
,则操作为mask &= ~set))【sigpending】
获取内核中的未决信号集
int sigpending(sigset_t *set);
//成功返回0,失败返回-1
【kill函数】功能:给任何进程和进程组pid,发送任何信号sig
#include
#include
int kil1(pid_t pid, int sig);
//成功则返回0,否则返回非0
参数:
【raise函数】
给当前进程发送信号
#include
int raise(int sig);
//成功则返回0,否则返回非0
参数:
【abort函数】
发送SIGABRT信号给当前的进程,杀死当前进程
void abort(void);
设置定时器。函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:SIGALARM(默认终止当前的进程,每一个进程都有且仅有唯一的一个定时器)
【alarm函数】一次定时该函数并不阻塞,并且无论进程处于什么状态,alarm都会计时(alarm的定时时间包含的是:用户+系统内核的运行时间)这里的系统内核的运行时间就例如打印到屏幕等的操作。
#include ;
unsigned int alarm(unsigned int seconds);
//返回值有两种
// 之前没有定时器,返回0
// 之前有定时器,返回之前的定时器剩余的时间
参数:
ararm(0)
【注意】当alarm
出现多次时,最后一次将覆盖上一次(上一次直接失效)。
int second = alarm(10) //second为0
过了一秒
econd = alarm(3) // second为9
等三秒后,进程终止
【setitimer函数】周期性定时, 该函数并不阻塞
设置定时器。可以替代alarm
函数,精度可以到微秒。并可以周期性定时。
#include
int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
//成功返回0,失败返回-1并设置错误号
参数:
struct itimerval { //定时器的结构体
struct timeval it_interval; //每个阶段的时间,间隔时间
struct timeval it_value; //延迟多长时间执行定时器
};
struct timeval { //时间的结构体
time_t tv_sec; //秒数
suseconds_t tv_usec; //微秒
};
【案例】过3s之后,每隔2s定时一次
struct itimerval new_val;
//设置间隔时间
new_val.it_interval.tv_sec = 2;
new_val,it_interval.tv_usec = 0;
//设置延迟时间
new_val.it_value.tv_sec = 3;
new_val,it_value.tv_usec = 0;
setitimer(ITIMER_REAL, &new_val, NULL);
捕捉到信号,检查或改变信号的处理。
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//返回值,0 表示成功,-1 表示有错误发生。
struct sigaction {
//这里的int指的是回调函数可以有int传入,传入的值就是捕捉到的信号编号
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
参数解释如下:
sigemptyest(&act.sa_mask)
来清空临时阻塞信号集)SIGCHID
此类提醒父进程要去回收的信号时,应循环1来调用非阻塞的waitpid
,通过判断标志位查看是否回收完或者是否需要退出循环。sigprocmask
解除阻塞。共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC(进程间通信)机制无需内核介入。
所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC技术的速度更快(比内存映射的效率都高(因为内存映射多了一步往磁盘写文件的过程)。
缺点就是会存在进程同步的问题
shmget
创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。shmat
来将进程和共享内存段关联起来,即使该段成为调用进程的虚拟内存的一部分。shmat
调用返回的 addr值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。shmdt
来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。shmctl
来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。【引用头】
#include
#include
【shmget函数】
注意,先创建的内存段的数据都会被初始化为0
int shmget (key_t key, size_t size, int shmflg);
//成功返回共享内存引用的ID(后面都用这个值),失败返回-1并设置错误号
【shmat函数】
void *shmat(int shmid,const void *shmaddr, int shmflg);
//成功则返回共享内存的首地址,失败返回(void*) -1
NULL
,由内核指定。【shmdt 函数】
int shmdt (const void *shmaddr) ;
//成功返回0,失败返回-1
【shmctl函数】
对共享内存进行操作(常用于删除)
int shmctl(int shmid, int cmd,struct shmid_ds *buf);
NULL
即可【ftok函数】
根据指定的路径名和int值,生成一个共享内存的key。(就是shmget
中所需要的第一个参数,可以不自己写,用这个生成)
key_t ftok (const char *pathname, int proj_id);
1、操作系统如何知道一块共享内存被多少个进程关联?
共享内存维护了一个结构体struct shmid_ds
,这个结构体里面有个成员,其记录了关联的进程个数。
2、可不可以对共享内存进行多次删除
可以,因为shmctl
只是标记删除,而不是直接删除。(其中,当共享内存被标记删除时,共享内存的key为0)
3、 共享内存和内存映射的区别
【会话的概念】
首先是进程,进程往外扩就是进程组,进程组再往外扩那就是遇到了会话。 其特点是:
SID
)。新进程会继承其父进程的会话ID。&
,即可变为后台进程组)。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。【守护进程的概念】
守护进程(Daemon Process)是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d 结尾的名字。
其特点是:
fork()
,之后父进程退出(就是控制终端退出),子进程继续执行。setsid()
开启一个新会话(这一步是为了脱离控制终端,用子进程是为了跟之前父进程的会话ID不冲突[毕竟子进程的进程ID是新的,创建新的进程组和会话ID都是新的,但是父进程已经是组长和会话长了])。umask
以确保当守护进程创建文件和目录时拥有所需的权限(常用umask(022)
)。chdir(''/")
,但是注意,有时候直接在根目录下是没有权限的,导致程序执行有误】 。上面的7步中,只有1、2、7是必须的。
本笔记是针对牛客的高境老师的《第二章Linux多进程开发》内容书写的。默默感谢一下高老师,确实很细致。