linux学习笔记_4.进程管理

进程管理

进程是Linux系统下资源管理的基本单位,每个进程有自己独立的运行空间。

用户级进程状态:

  • TASK_RUNNING:就绪/运行状态,处于该状态的进程可以被调度执行而成为当前进程.
  • TASK_INTERRUPTIBLE:可中断睡眠状态,处于该状态的进程在所需资源有效时被唤醒,也可以通过信号或者定时中断唤醒.
  • TASK_UNINTERRUPTIBLE:不可中断睡眠状态,处于该状态的进程仅当所需资源有效时被唤醒.
  • TASK_ZOMBLE:僵死状态,表示进程结束且释放资源.但其task_struct仍未释放.
  • TASK_STOPPED:暂停状态.处于该状态的进程通过其他进程的信号才能被唤醒

在这里插入图片描述

操作系统:
①运行状态(Running) 进程已经占用CPU,在CPU上进行
②就绪状态(Ready) 具备运行条件但是由于没有CPU可用,所以暂时不能运行
③阻塞状态(Block) 也叫等待状态(Wait)由于等待某项服务完成或者等待某个学号而不能运行的状态,比如等待系统调用,I/O操作等

在这里插入图片描述

  • *就绪->运行:进程调度
  • *运行->就绪:时间片到或者被强行占用
  • *运行->阻塞:请求服务后等待响应,或者等待某个信号的到来
  • *阻塞->就绪:请求的服务已经完成,或者等待的信号已经到来

五种状态:

  • 创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态

  • 就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行

  • 执行状态:进程处于就绪状态被调度后,进程进入执行状态

  • 阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用

  • 终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行
    在这里插入图片描述

进程基本属性

  1. 进程号(PID)
    唯一标识一个进程的正整数,ps aux,gitpid():返回类型pid_t(int)的pid号,失败返回-1.

  2. 父进程号(PPID)
    任何进程(除了init进程)都是由另一个进程创建。gitppid():返回类型pid_t(int)的pid号,失败返回-1.

  3. 进程组号(PGID)
    Linux中每个用户都拥有自己的用户号(UID)和用户组号(GUID),进程也拥有自己的进程号(PID)和进程组号(PGID),进程组是一个或多个进程的集合,它们与同一作业相关联,可以接收来自同一终端的各种信号。
    getpgid(pid_t pid):其中pid为0 返回当前进程的进程组号,则返回pid进程的进程组号,失败返回-1
    getpgrp():返回当前进程的进程组号
    setpgid(pid_t pid,pid_t pgid):指定进程所属的组识别码设为参数pgid 指定的组识别码。如果参数pid为0, 则会用来设置目前进程的组识别码, 如果参数pgid为0, 则会以目前进程的进程识别码来取代。

  4. 会话(SID)
    是一个或多个进程组的集合
    setsid():当进程是会话的领头进程时setsid()调用失败并返回(-1),调用成功后,返回新的会话的ID,调用setsid函数的进程成为新的会话的领头进程,并与其父进程的会话组和进程组脱离。由于会话对控制终端的独占性,进程同时与控制终端脱离。
    getsid(pid_t pid):getsid(0) 返回调用进程的会话ID. getsid§ 返回与进程ID的进程的会话ID,调用失败并返回(-1)

  5. 控制终端
    会话和进程组有一些其他特性:

    • 一个会话可以有一个控制终端。这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。

    • 建立与控制终端连接的会话首进程被称为控制进程。

    • 一个会话中的几个进程组可被分成一个前台进程组以及一个或几个后台进程组。

    • 如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。

    • 无论何时键入终端的中断键(常常是DELETE或Ctrl+C),就会将中断信号发送给前台进程组的所有进程。

    • 无论何时键入终端的退出键(常常是Ctrl+\),就会将退出信号发送给前台进程组中的所有进程。

    • 如果终端接口检测到调制解调器(或网络)已经断开连接,则将挂断信号发送给控制进程(会话首进程)。

    pid_t tcgetpgrp( int filedes );
    //返回值:若成功则返回前台进程组的进程组ID,若出错则返回-1
    //返回前台进程组的进程组ID,该前台进程组与在filedes上打开的终端相关联。
        
    int tcsetpgrp( int filedes, pid_t pgrpid );
    //将前台进程组ID设置为pgrpid
    //返回值:若成功则返回0,若出错则返回-1
    

有时不管标准输入、标准输出是否被重定向,程序都要与控制终端交换。**保证程序能读写控制终端的方法是打开文件/dev/tty,在内核中,此特殊文件是控制终端的同义语。**自然,如果程序没有控制终端,则打开此设备将失败。

在这里插入图片描述

  1. 每个会话有且只有一个前台进程组,但会有0个或者多个后台进程组。
  2. 产生在控制终端上的输入(Input)和信号(Signal)将发送给会话的前台进程组中的所有进程。对于输出(Output)来说,则是在前台和后台共享的,即前台和后台的打印输出都会显示在屏幕上。
  3. 终端上的连接断开时 (比如网络断开或 Modem 断开), 挂起信号将发送到控制进程(controlling process) 。
  4. 一个用户登录后创建一个会话。一个会话中只存在一个前台进程组,但可以存在多个后台进程组。第一次登陆后第一个创建的进程是shell,也就是会话的领头进程,该领头进程缺省处于一个前台进程组中并打开一个控制终端可以进行数据的读写。当在shell里运行一行命令后(不带&)创建一个新的进程组,命令行中如果有多个命令会创建多个进程,这些进程都处于该新建进程组中,shell将该新建的进程组设置为前台进程组并将自己暂时设置为后台进程组
守护进程的创建:

Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。

启动方式:

放到启动脚本中,/etc/rc.d

利用inetd超级服务器启动,如talnet等

由cron命令定时启动以及在终端用nohup命令启动的进程也是守护进程

首先屏蔽中断信号:

for(i=1;i<31;i++)
signal(SIGTTOU,SIG_IGN); //SIGSTOP和SIGKILL不能忽略
  1. 创建子进程,父进程退出
    由于守护进程是脱离控制终端的,因此,完成第一步后就会在shell终端里造成一程序已经运行完毕的假象。之后的所有后续工作都在子进程中完成,而用户在shell终端里则可以执行其他的命令,从而在形式上做到了与控制终端的脱离。
    由于父进程已经先于子进程退出,会造成子进程没有父进程,从而变成一个孤儿进程。在Linux中,每当系统发现一个孤儿进程,就会自动由1号进程收养。原先的子进程就会变成init进程的子进程。

  2. 在子进程中创建新会话
    setsid():

• 首先内核会创建一个新的会话,并让该进程成为该会话的leader进程,
• 同时伴随该session的建立,一个新的进程组也会被创建,同时该进程成为该进程组的组长。
• 该进程此时还没有和任何控制终端关联。若需要则要另外调用tcsetpgrp,前面讲前台进程组时介绍过。
• 让进程摆脱原会话的控制。
• 让进程摆脱原进程组的控制。
• 让进程摆脱原控制终端的控制。
  1. 改变当前目录为根目录
    必要的步骤。使用fork()创建的子进程继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统(如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦(如系统由于某种原因要进入单用户模式)。
    因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述问题。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir()。

  2. 重设文件权限掩码
    文件权限掩码是指屏蔽掉文件权限中的对应位。
    例如,有一个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork()函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。
    因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask()。在这里,通常的使用方法为umask(0)。即赋予最大的能力。

  3. 关闭文件描述符
    同文件权限掩码一样,用fork()函数新建的子进程会从父进程那里继承一些已经打开的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法被卸载。
    在上面的第(2)步之后,守护进程已经与所属的控制终端失去了联系,因此,从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf())输出的字符也不可能在终端上显示出来。
    所以,文件描述符为0、1和2的3个文件(常说的输入、输出和报错这3个文件)已经失去了存在的价值,也应被关闭。

进程用户属性

  1. 进程真实用户号(RUID)
    创建该进程的用户UID为进程的真实用户号(RUID);
    读取:getuid()

  2. 进程有效用户号(EUID)
    EUID主要用于权限检查,多数情况下,EUID和UID相同,但是当设置了setuid位之后,任何运行该程序的进程均可获得该文件拥有者的权限,即EUID在此时等于程序拥有者的UID。
    参考:passwd进程,用户可以修改自身的密码信息,而passwd文件的属主是root。
    设置setuid位:chmod u+s file
    读取:geteuid()

  3. 进程用户组号(GID)
    创建该进程的用户 所在的组号。
    读取:getgid()

  4. 有效进程用户组号(EGID)
    类似于EUID,但是影响的权限位为setgid。
    读取:getegid()

进程管理与控制

创建进程

fork(): 父进程返回子进程PID,子进程返回0,若失败则返回-1。

子进程完全复制了父进程的资源,包括进程上下文、代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等信息,而子进程与父进程的区别有进程号、资源使用情况和计时器等。

当然了Linux不是在子进程一fork出来就将父进程的资源复制出一个副本给子进程的,Linux采用了写时复制,一开始两个进程是共享数据的,但是当子进程尝试修改数据,就会触发写时复制,为子进程创建一个该数据的副本。

fvoke():
①子进程必定先运行,等到子进程调用exit或者exec后,父进程才能运行,如果在调用这两个函数之前子进程依赖于父进程的进一步操作,将会导致死锁。
②父子进程共享空间(共享内存数据)

fork:子进程拷贝父进程的数据段、堆栈段

vfork:父子进程共享数据段

进程中运行新代码

用fork()函数创建子进程后,如果希望在当前子进程中运行新的程序,则可以调用execX系列函数。
当进程调用exec函数后,该进程的用户空间资源完全有新程序代替。

execX函数簇不会创建新的进程。这些函数的区别在于:
  a. 标志新程序所在位置是使用路径还是文件名;
  b. 参数的传递是使用参数列表还是argv[]数组的数组形式;
  注意,如果是使用文件名来标识新程序的,也就是你没有指定可执行程序的路径,所以它会去$PATH环境变量所记录的路径去搜索该程序。
  a. 函数名中含有’p’表示该函数不需要指定可执行程序的路径,否则需要;
  b. 函数名中含有’l’表示该函数是以参数列表的形式接收形参,否则argv[]数组形式;

int execl(const char *pathname,const char *arg0,.../*(char *) 0 */);
/*execl函数是对新程序的标识需要指定路径,且形参是列表形式的
  execl("/bin/ls", "ls", "-l", "/home/tom", NULL);
 参数1指定可执行程序
  参数2开始到最后一个参数之前: 可执行程序的参数列表
  最后一个参数: NULL是参数列表的结束标志 */
int execv(const char *pathname,char *const argv[]);
/*execv()函数对新程序需要指定路径,形参是以”argv[]”数组形式传进来的
    char* argv[] = {"hello", "word", NULL};  //注意argv[]与形参列表的区别,列表还需要传入再可执行程序名称
    execv("./new_pro", argv);
*/
int execle(const char *pathname,const char *arg0,.../*(char *) 0
 ,char *const envp[] */);
/*execle()函数跟execl()函数的差异在于execl()可传入环境变量envp[],也就是说进程启动新程序的时候可以为新程序指定环境变量。(当然,对于其他不能为新程序指定环境变量的函数,默认是把当前进程的环境变量赋给新程序)
extern char **environ;
execle("./new_pro", "", NULL, environ); 
*/
int execve(const char *pathname,char *const argv[],char *const envp[]);
/*execve()*execv()函数对新程序需要指定路径,形参是以”argv[]”数组形式传进来,可传入环境变量envp[]*/
int execlp(const char *filename,const char*arg0,.../*(char *) 0*/);
/*execlp函数对新程序不需要指定路径,它是从$PATH环境变量所指定的目录中查找文件名为第一个参数指示的字符串,找到后执行它。第二个及以后的参数表示执行该文件时传递的参数列表,同理,最后一个参数必须为NULL。
execlp("ls", "ls", "-l", "/home/tom", (char* )0); 
*/
int execvp(const char *filename, char *const argv[]);
/*execvp()函数对新程序不需要指定路径,它会从$PATH指定的路径查找,形参是以argv[]的形式。*/
回收用户进程空间资源

结束进程的方式:

  • 显式调用exit()或_exit()系统调用。

    • exit(0):正常运行程序并退出程序;
    • exit(1):非正常运行导致退出程序;
    • _exit或_Exit会立即进入内核。
  • 在main函数中执行return

  • 隐含的离开main函数,如遇到}

  • 异常终止:
    (1)调用abort
    (2)接收一个信号
    (3)最后一个线程对取消请求做出响应

进程在正常退出前都需要执行注册的退出处理函数,刷新流缓冲区等操作,然后释放进程用户空间所有资源。而进程控制块 PCB 并不在这时释放,仅调用退出函数的进程属于一个僵死进程。

return返回函数值,是关键字; exit 是一个函数。
return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束。
return是函数的退出(返回);exit是进程的退出。
return是C语言提供的,exit是操作系统提供的(或者函数库中给出的)。
return用于结束一个函数的执行,将函数的执行信息传出个其他调用函数使用;exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关,一般是 0 为正常退出,非0 为非正常退出。
非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的。

注册退出处理函数:

int atexit(void (*func)(void));

int on_exit(void (*function)(int , void *), void *arg);
on_exit()用来注册终止处理程序,当程序通过调用exit()或从main 中返回时被调用, 终止处理程序有两个参数,第一个参数是来自最后一个exit()函数调用中的status,第二个参数是来自on_exit()函数中的arg;
  同一个函数若注册多次,那它也会被调用多次;
  当一个子进程是通过调用fork()函数产生时,它将继承父进程的所有终止处理程序。在成功调用exec系列函数后,所有的终止处理程序都会被删除。
  成功返回0,失败返回非0值。

回收用户进程空间资源

进程退出释放用户空间的资源,但是进程PCB并没有释放。由当前进程的父进程完成,父进程可以显式调用wait和waitpid完成。

pid_t wait(int *status)

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:

pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

pid_t waitpid(pid_t pid,int *status,int options)

参数PID:

从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。

  1. pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
  2. pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
  3. pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
  4. pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

参数status:

为调用函数中某个变量地址,如果执行成功,则用来存储结束进出的结束状态。

WIFEXITED(status):如果子进程正常结束则为非0 值.
WEXITSTATUS(status):取得子进程exit()返回的结束代码, 一般会先用WIFEXITED 来判断是否正常结束才能使用此宏.
WIFSIGNALED(status):如果子进程是因为信号而结束则此宏值为真
WTERMSIG(status):取得子进程因信号而中止的信号代码, 一般会先用WIFSIGNALED 来判断后才使用此宏.
WIFSTOPPED(status):如果子进程处于暂停执行情况则此宏值为真. 一般只有使用WUNTRACED时才会有此情况.
WSTOPSIG(status):取得引发子进程暂停的信号代码, 一般会先用WIFSTOPPED 来判断后才使用此宏.

参数options:

options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用

ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);

WNOHANG:如果没有任何已经结束的子进程则马上返回, 不予以等待. 不阻塞。
WUNTRACED:如果子进程进入暂停执行情况则马上返回, 但结束状态不予以理会. 子进程的结束状态返回后存于status。

如果我们不想使用它们,也可以把options设为0

**返回值:**如果执行成功则返回子进程识别码(PID), 如果有错误发生则返回-1. 失败原因存于errno 中.

孤儿进程和僵死进程

孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并由 init 进程对它们完成状态收集工作。

危害
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了 init 进程身上,init 进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会出面处理它的一切善后工作。因此孤儿进程并不会有什么危害

僵死进程

一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,即回收了用户资源但内核空间的PCB还没有释放,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。

危害:
僵尸进程虽然不占有任何内存空间,但如果父进程不调用 wait() / waitpid() 的话,那么保留的信息就不会释放,其进程号就会一直被占用,而系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。

解决方法:

  • 父进程通过 wait 和 waitpid 等函数等待子进程结束:这会导致父进程挂起,所以这并不是一个好办法,父进程如果不能和子进程并发执行的话,那我们创建子进程就没有意义。并且一个 wait 只能解决一个子进程,如果有多个子进程就要用到多个 wait。

  • 通过信号机制:子进程退出时,向父进程发送 SIGCHILD 信号,父进程处理 SIGCHILD 信号,在信号处理函数中调用 wait 处理僵尸进程。

  • fork两次:原理是使进程成为孤儿进程,从而其父进程变为 init 进程,通过 init 进程处理僵尸进程。具体操作为:父进程一次 fork() 后产生一个子进程随后立即执行 wait(NULL) 来等待子进程结束,然后子进程再次 fork() 后产生孙子进程,随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵死进程了。

  • kill父进程:严格地来说,僵尸进程并不是问题的根源,罪魁祸首是产生出大量僵尸进程的那个父进程。因此可以把产生大量僵尸进程的那个元凶枪毙掉(也就是通过 kill 发送 SIGTERM 或者 SIGKILL 信号)。父进程被结束之后,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被 init 进程接管,init 进程会 wait() 这些孤儿进程,释放它们占用的系统进程表中的资源。

修改进程用户相关信息

int access(const char* pathname, int mode);

参数介绍:

pathname 是文件的路径名+文件名

mode:指定access的作用,取值如下:

F_OK 值为0,判断文件是否存在
X_OK 值为1,判断对文件是可执行权限
W_OK 值为2,判断对文件是否有写权限
R_OK 值为4,判断对文件是否有读权限
注:后三种可以使用或“|”的方式,一起使用,如W_OK|R_OK

返回值:成功0,失败-1

int setuid(uid_t uid)

1.若进程具有超级用户权限,则将有效用户ID,实际用户ID和保存的设置用户ID都设置为uid

2.若进程无超级用户权限,且uid=进程的实际用户ID或者uid=进程的保存设置用户ID,那么就将进程的有效用户ID设置为uid.但是不改变进程的实际用户ID和保存的设置用户ID。

3。若上述2个条件都不满足,则出错,errno置为EPERM,

int seteuid(uid_t uid)

  1. 若进程具有超级用户权限,则setuid只将有效用户ID设置为uid
  2. 若进程没有超级用户权限,则setuid只将有效用户ID设置为uid, 但是uid必须等于实际用户ID或保存的设置用户ID,

进程通信

无名管道

ps -aux | grep init

无名管道用于有亲缘关系的进程间的通信,管道字如其名,它就像在两个进程之间铺设了一条管道,进程通过管道进行数据交互。无名管道是没有名字的,它由pipe或者pipe2函数创建。

  1. 只能用于有亲缘关系(父子进程)的进程间通信
  2. 半双工通信方式,具有固定的读写端
  • 半双工:同一时刻,数据只能往一个方向传输
  • 全双工:同一时刻,数据可以往两个方向传输
  1. Pipe 被当作特殊文件来对待(Linux 下一切都是文件),内核中特殊类型的文件完全由操作系统管理和维护。

int pipe(int pipefd[2]);
pipefd[2]:无名管道的两个文件描述符,int型的数组,大小为2,pipefd[0]为读端,pipefd[1]为写端
返回值:成功:返回0;失败:返回-1

(1)读–特性:
​ <1>写端存在:
管道有数据:返回读到的字节数
管道无数据:阻塞
​ <2>写端不存在:
​ 管道有数据:返回读到的字节数
管道无数据:返回0

(2)写–特性:
​ <1>读端存在:
​ 管道有空间:返回写入的字节数
​ 管道无空间:阻塞,直到有空间为止
​ <2>读端不存在:
无论管道是否有空间,管道破裂

文件描述符重定向

0:stdin 标准输入 1:stdout 标准输出 2:stderr 标准错误

> 输出重定向 覆盖 cat > text1

**>> **输出重定向 追加cat >> text1

< << 输入重定向 cat < text1

> 1.txt 就等于 1> 1.txt,fd1作为标准输出可以省略不写

标准输入0,当0在<左边的时候也是可以省略不写。

& 表示联合,

&> 等于 1&2> ; 意思为把标准输出和标准错误输出都重定向至一个文件下

>& 为两边都是重定向文件描述符定制的,例如“2>&1” 的含义就是把标准错误输出(2)整合到标准输出(1)末尾,而标准输出(1)因为没有重定向所以默认输出到控制台上,所以当使用该指令你可以看到的是错误以及标准输出都输出到了控制台上.

int dup(int oldfd);

从系统中寻找最小的可用文件描述符,作为已有文件描述符oldfd的副本,新文件描述符和旧文件描述符指向同一文件,新文件描述符提供dup的返回值返回。

int dup2(int oldfd, int newfd);

将newfd作为oldfd的副本。
如果newfd事先存在,dup2会先close掉newfd,然后将newfd作为oldfd的副本。

有名管道

有名管道的创建之后会在文件系统中以管道文件的形式存在;

有名管道可以用于任意两个进程间的通信,没有固定的读端和写端。

有名管道和普通文件一样具有磁盘存放路径、文件权限和其他属性;但是,有名管道和普通文件又有区别,有名管道并没有在磁盘中存放真正的信息,他存储的通信信息在内存中,两个进程结束后自动消失,拥有一个磁盘路径仅仅是一个接口,其目的是使进程间信息的编程更简单统一。通信的两个进程结束后,有名管道的文件路径本身依然存在,这是和无名管道不一样的。

shell : mknod PIP p

extern int mkfifo(__const char *__path, __mode_t __mode);

mkfifo会根据参数建立特殊的有名管道文件,该文件必须不存在,而参数mode为该文件的权限,mkfifo建立的fifo文件的其他进程都可以用读写一般文件的方式存取。当使用open函数打开fifo文件时,O_NONBLOCK会有影响。

读写有名管道
有名管道是一种特殊的文件,实质仍然是一段内核管理的内存空间。但在通过write和read系统调用来执行读写操作前,需要调用open函数打开该文件。另外,操作有名管道的阻塞位置在open位置,而不是无名管道的读写位置。
(1)如果希望以写的方式打开管道,则需要另一个进程以读的方式打开管道。一个进程可以以可读可写的方式打开管道,当前进程充当了读写两个身份,进程不会阻塞。
(2)两进程已打开管道操作,阻塞读操作按以下方式进行:

  • 如果管道中无数据,读操作默认阻塞;
  • 如果管道现数据大于读出数据,立即读取期望大小的数据;
  • 如果管道现有数据小于读出数据,立即读取现有所有数据。

(3)两进程已打开管道操作,阻塞写操作按以下方式进行:

  • 管道中没有空间,写操作阻塞
  • 管道中有空间,但空间小于欲写入数据,写满空间后阻塞;
  • 管道中有空间,空间大于欲写入数据,写入数据后返回。

(4)两进程已经完成打开管道操作,中途其中一个进程退出。

  • 未退出一端是写操作,将返回SIGPIPE信号。
  • 未退出一端是阻塞读操作,读操作将不再阻塞,直接返回0.

无名管道和有名管道具有以下特点:
(1)管道是特殊类型的文件,在满足先入先出的原则下可能进行读写。但不能定位读写位置。
(2)管道是单向的,要实现双向,需要两个管道。而无名管道一般只用于亲缘关系进程间通信(非亲缘关系进程只能传递文件描述符)。有名管道以磁盘文件的方式存在,可以实现本机任意两进程通信。
(3)阻塞问题。无名管道无需显式打开,创建时直接返回文件描述符,而在读写时需要确定对方的存在,即阻塞于读写位置,而有名管道在打开时需要确定对方的存在,否则阻塞。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值