linux进程编程

本文详细介绍了Linux进程的概念,包括进程与程序的区别、进程的多调度原理、进程环境变量的获取与修改,以及进程的自身属性如PID、PPID等。文章还探讨了如何创建、操作和终止进程,特别提到了fork()函数在创建进程中的作用,以及父子进程对文件操作的不同方式。最后,文章讨论了僵尸进程和孤儿进程的定义、危害以及如何通过wait / waitpid回收进程资源。
摘要由CSDN通过智能技术生成

一、什么是进程?

一般来讲,进程定义为正在运行的程序的实例,简单地说,进程就是一个正在运行的程序。

程序 (program):通常为二进制,放置在储存媒体中 (如硬盘、光盘、软盘、磁带等), 为实体文件的型态存在;

进程 (process):程序被触发后,运行者的权限与属性、程序的程序码与所需数据等都会被加载内存中, 操作系统并给予这个内存内的单元一个识别码 (PID),可以说,进程就是一个正在运行中的程序。
进程不仅仅包含了正在运行的代码,还包括了运行代码所需要的各种资源。内核中使用进程控制块PCB(process control block)管理一个进程的数据结构,每个进程都有一个PCB。

进程是资源所有资源分配的基本单位,线程则是CPU调度的基本单位。
二、多进程调度原理

在一个cpu上,可以存在多个进程,但是同一个时间段内,只能有一个进程工作。操作系统使用一定的算法管理所有进程,使得看起开像是并发执行,称之为“伪并发”;多核CPU可以实现真正意义上的并发。

三、进程环境与属性

1、环境变量

(1)、命令行使用export查看本机支持的环境变量的名称和内容

(2)在程序中获取环境变量

每个进程都有一个环境表,存储当前进程的环境变量。每个环境变量由“name=value”这样的字符串组成。

进程环境表其实是一个字符串数组,用environ变量指向它。 每一个环境变量都是一个字符串,所有的环境变量构成字符串数组。环境表的内存布局:

我们可以将所有的环境表中的变量打印出来:

#include <stdio.h>

int main(void)
{
extern char **environ; // 不用定义,声明就能用 二重指针
int i = 0;

while (NULL != environ[i])
{
    printf("%s\n", environ[i]);
    i++;
}

return 0;

}

此外,操作系统还给我们提供了相关API,来帮助我们获得与修改环境变量:

char * getenv(const char *name)–该函数用来获取名字为name的环境变量值,不存在时返回NULL。

int putenv(const char * string)–该函数用来改变或增加环境变量的内容。参数string的格式为“name=value”,如果该环境变量已经存在则会覆盖。

int setenv(const char *name, const char * value,int overwrite)–该函数用来改变或增加环境变量的内容。根据参数overwrite看是否覆盖。

int unsetenv(const char *name)–清除某个环境变量。

举例:

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

int main(void)
{
char *env_path = “PATH”;//想要寻找的环境变量
char *env_value = NULL;//存储环境变量的值

env_value = getenv(env_path);

if(NULL==env_value)
{
    printf("not founf!\r\n");
}

printf("env_value:%s\r\n",env_value);

return 0;
}

2、进程自身属性
一个进程除了能获得操作系统提供的环境变量之外,还具备自身的属性,主要包括:

进程号(PID:process ID),
父进程号(PPID parent PID),
进程组号(PGID:process group ID),操作系统允许对进程进行分组
真实用户号(UID:user ID),用户的唯一识别号,用于标识一个用户
真实组号(GID:group ID),用户组的唯一识别号,用于标识一个用户组
有效用户号(EUID:effective user ID),以其他用户身份访问文件使用
有效用户组号(EGID:effective group ID),以其他用户组身份访问文件使用
(1)、命令行使用PS查看进程号

(2)、在程序中获取进程号

#include <sys/types.h>
#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(vid);–获取当前进程使用用户组ID
gid_t getegid(void);–获取当前进程有效用户组ID
#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

int main(void)
{
pid_t p1 = -1, p2 = -1;

p1 = getpid();
printf(“pid = %d.\n”, p1);
p2 = getppid();
printf(“parent id = %d.\n”, p2);

return 0;
}

如何创建进程?
linux通过fork()系统调用创建新一个进程,老进程叫父进程,复制生成的新进程叫子进程。父子关系是相对的,每一代都有一个父子关系。

fork函数定义如下:

#include <unistd.h>

pid_t fork(void);
实例1:

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

int main (void)
{
pid_t fpid = -1; //fpid表示fork函数返回的值
printf(“this is fork test.\r\n”);
fpid=fork();

if (fpid < 0)   
    printf("error in fork!\r\n");  

if (fpid == 0) //子进程
{ 
    printf("\r\n");  
    printf("fpid is %d\r\n",fpid);
    printf("child process, my PID is %d\r\n",getpid());  
    printf("child process, my PPID is %d\r\n",getppid()); 

}  

if (fpid > 0) //父进程
{  
    printf("\r\n"); 
    printf("fpid is %d\r\n",fpid);
    printf("parent process, my PID is %d\r\n",getpid()); 
    printf("parent process, my PPID is %d\r\n",getppid());  

}  

printf("hello world, pid = %d.\r\n", getpid());

return 0;  

}

解释:

(1)fork函数调用一次会返回2次,返回值等于0的就是子进程,而返回值大于0的就是父进程。

(2)p1 = fork()以后的代码会运行两次,一次是父进程运行的,一分是子进程运行的,谁先运行不一定,由操作系统调度器说了算。

(3)fork的返回值在子进程中等于0,在父进程中等于本次fork创建的子进程的进程ID。

二、父子进程对文件的操作

1.子进程继承父进程中打开的文件

父进程以O_TRUNC的方式打开文件,后续和子进程对文件进行写入

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

#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
// 首先打开一个文件
int fd = -1;
pid_t pid = -1;

fd = open("1.txt", O_RDWR | O_TRUNC);
if (fd < 0)
{
    perror("open");
    return -1;
}

// fork创建子进程
pid = fork();
if (pid > 0)
{
    // 父进程中
    printf("parent.\r\n");
    write(fd, "hello", 5);
    Sleep(1);//给系统调度充足的时间
}
else if (pid == 0)
{
    // 子进程
    printf("child.\r\n");
    write(fd, "world", 5);
    Sleep(1);//给系统调度充足的时间
}
else
{
    perror("fork");
    exit(-1);
}
close(fd);

return 0;

}

2、父子进程各自独立打开同一文件实现共享

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

int main(void)
{
// 首先打开一个文件
int fd = -1;
pid_t pid = -1;

// fork创建子进程
pid = fork();
if (pid > 0)
{
    // 父进程中
    fd = open("1.txt", O_RDWR );
    if (fd < 0)
    {
        perror("open");
        return -1;
    }

    printf("parent.\n");
    write(fd, "hello", 5);
    sleep(1);
}
else if (pid == 0)
{
    // 子进程
    fd = open("1.txt", O_RDWR );
    if (fd < 0)
    {
        perror("open");
        return -1;
    }

    printf("child.\n");
    write(fd, "world", 5);
    sleep(1);
}
else
{
    perror("fork");
    exit(-1);
}
close(fd);

return 0;

}
结果都是world,原因如下表,父子进程文件描述符所指向的文件表是独立的。当父进程先执行的时候,写入了hello,子进程执行的时候,由于文件表独立,所以文件指针也独立,子进程是从0偏移地址写,所以会将之前写的hello覆盖掉,只剩下world。

要想实现接续写的效果,只需在open时使用O_APPEND

原因:如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中. 每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度,这就使得每次写入的数据都追加到文件的当前尾端处.
如何终止进程?

终止一个进程大致有以下几种情况:

(1)函数执行完毕,main函数正常return;

(2)使用linux提供的退出进程相关的函数:exit()、exit()、atexit()、onexit()

exit()的作用是退出当前进程,并且尽可能释放当前进程占用的资源

_exit()的作用也是退出当前进程,但是不试图释放当前进程占用的资源,而且_exit()终止进程的时候不调用atexit()、on_exit()等注册的回调函数!

atexit()、on_exit()作用都是程序退出是指定调用用户的代码(可以理解为设置回调函数),区别在于on_exit()可以为设定的用户函数设定参数。
实例1、函数退出回调函数例程

#include <stdlib.h>

void exit(int status);

int atexit(void (*function)(void));
The atexit() function registers the given function to be called at normal process termination, either via exit(3) or via return from the program’s main().

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void bye(void)
{
printf(“That was all, folks\r\n”);
}

void bye1(void)
{
printf(“That was bye1, folks\r\n”);
}

int main(void)
{
long a;
int i;

a = sysconf(_SC_ATEXIT_MAX);
printf(“ATEXIT_MAX = %ld\r\n”, a);

i = atexit(bye);
if (i != 0)
{
fprintf(stderr, “cannot set exit function\r\n”);
exit(EXIT_FAILURE);//EXIT_FAILURE=1 返回给操作系统作为exit status
}

i = atexit(bye1);
if (i != 0)
{
fprintf(stderr, “cannot set exit function\r\n”);
exit(EXIT_FAILURE);//EXIT_SUCCESS=0 返回给操作系统作为exit status
}

exit(EXIT_SUCCESS);
}

我们看到bye是先注册的,但是确是bye1先执行的,这是因为atexit()函数是按照栈方式向系统注册的,所以后注册的函数会先调用。
僵尸进程与孤儿进程

1、定义

在每个进程退出的时候,内核自动释放该进程所有的资源,包括打开的文件,占用的内存,malloc申请的内容没有free时等。 但是仍然为其保留一定的信息,主要是task_struct和栈内存,(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放(也就是需要父进程来收尸)。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。

僵尸进程:即子进程先于父进程退出后,子进程的PCB需要其父进程释放,但是父进程并没有释放子进程的PCB,这样的子进程就称为僵尸进程。

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

2、问题与危害:

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

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

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

int waitid(idtype_t idtype, id_t id, siginfo_t infop, int options);
/
This is the glibc and POSIX interface; see NOTES for information on the raw system call.*/
1、使用wait回收进程

形参status用来返回子进程结束时的状态,父进程通过wait得到status后就可以知道子进程的一些结束状态信息。wait的返回值pid_t,这个返回值就是本次wait回收的子进程的PID。当前进程有可能有多个子进程,wait函数阻塞直到其中一个子进程结束wait就会返回,wait的返回值就可以用来判断到底是哪一个子进程本次被回收了。若父进程没有任何子进程则wait返回错误。

原理:父进程调用wait函数后阻塞,等待子进程的SIGCHILD信号。子进程结束时,系统向其父进程发送SIGCHILD信号,父进程被SIGCHILD信号唤醒然后去回收僵尸子进程。父子进程之间是异步(各自双方不知道对方是什么状态)的,SIGCHILD信号机制就是为了解决父子进程之间的异步通信问题,让父进程可以及时的去回收僵尸子进程。

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

int main(void)
{
pid_t pid = -1;
pid_t ret = -1;
int status = -1;

pid = fork();
if (pid > 0)
{
//因为wait函数是阻塞的,当父进程先执行,子进程没有结束的情况下,父进程会在wait函数这个地方阻塞住
    printf("parent.\n");
    ret = wait(&status);

    printf("子进程已经被回收,子进程pid = %d.\n", ret);
    printf("子进程已经被回收,子进程status = %d.\n", status);
}
else if (pid == 0)
{
    // 子进程
    printf("child pid = %d.\n", getpid());
    //while(1);//子进程不可能结束,所以wait函数被阻塞住,子进程不可能被回收
}
else
{
    perror("fork");
    return -1;
}

return 0;

}

我们还可以通过使用相关宏来判断子进程的返回状态,可以参考man手册对wait函数的解释:

WIFEXITED(status)
returns true if the child terminated normally, that is, by call‐
ing exit(3) or _exit(2), or by returning from main().
WEXITSTATUS(status)
returns the exit status of the child. This consists of the
least significant 8 bits of the status argument that the child
specified in a call to exit(3) or _exit(2) or as the argument
for a return statement in main(). This macro should be employed
only if WIFEXITED returned true.
WIFSIGNALED(status)
returns true if the child process was terminated by a signal.
WTERMSIG(status)
returns the number of the signal that caused the child process
to terminate. This macro should be employed only if WIFSIGNALED
returned true.
WCOREDUMP(status)
returns true if the child produced a core dump. This macro
should be employed only if WIFSIGNALED returned true. This
macro is not specified in POSIX.1-2001 and is not available on
some UNIX implementations (e.g., AIX, SunOS). Only use this
enclosed in #ifdef WCOREDUMP … #endif.
WIFSTOPPED(status)
returns true if the child process was stopped by delivery of a
signal; this is possible only if the call was done using WUN‐
TRACED or when the child is being traced (see ptrace(2)).
WSTOPSIG(status)
returns the number of the signal which caused the child to stop.
This macro should be employed only if WIFSTOPPED returned true.
WIFCONTINUED(status)
(since Linux 2.6.10) returns true if the child process was
resumed by delivery of SIGCONT.
2、使用WAITPID回收进程

基本功能一样,都是用来回收子进程,waitpid可以回收指定PID的子进程,waitpid可以阻塞式或非阻塞式两种工作模式。

pid_t waitpid(pid_t pid, int *status, int options);
pid>0 只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1 等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1 等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
WNOHANG 若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0
WUNTRACED 返回终止子进程信息和因信号停止的子进程信息
WCONTINUED 返回收到SIGCONT信号而恢复执行的已停止子进程状态信息
(1)使用waitpid实现wait的效果

ret = waitpid(-1, &status, 0); -

1表示不等待某个特定PID的子进程而是回收任意一个子进程,0表示用默认的方式(阻塞式)来进行等待,返回值ret是本次回收的子进程的PID

(2)ret = waitpid(pid, &status, 0);

等待回收PID为pid的这个子进程,如果当前进程并没有一个ID号为pid的子进程,则返回值为-1;如果成功回收了pid这个子进程则返回值为回收的进程的PID

(3)ret = waitpid(pid, &status, WNOHANG);

这种表示父进程要非阻塞式的回收子进程。此时如果父进程执行waitpid时子进程已经先结束等待回收则waitpid直接回收成功,返回值是回收的子进程的PID;如果父进程waitpid时子进程尚未结束则父进程立刻返回(非阻塞),但是返回值为0(表示回收不成功)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值