Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> การเขียนโปรแกรม C

การเรียนรู้ fork() และ exec() ใน C:อธิบายการสร้างและเปลี่ยนกระบวนการ

ในบทความ Linuxhint นี้ คุณจะได้เรียนรู้วิธีใช้ฟังก์ชัน fork() และ exec() เพื่อสร้าง รัน หรือแทนที่กระบวนการด้วยฟังก์ชันอื่น เราจะดูคำอธิบายของทั้งสองฟังก์ชันและอธิบายไวยากรณ์และวิธีการเรียก นอกจากนี้เรายังจะดูตัวอย่างการใช้งานจริงโดยย่อของแต่ละฟังก์ชันทั้งสองด้วย จากนั้นเราจะอธิบายวิธีใช้ fork() และ execv() ร่วมกันเพื่อสร้างกระบวนการและแทนที่ด้วยกระบวนการอื่น

Fork() ฟังก์ชั่นในภาษา C

ฟังก์ชัน fork() จะสร้างกระบวนการที่ซ้ำกัน แม้ว่ากระบวนการย่อยจะซ้ำกับกระบวนการหลัก แต่ก็ไม่ได้แชร์คุณสมบัติบางอย่าง เช่น พื้นที่หน่วยความจำที่จัดสรร, PID ฯลฯ ต่อไป มาดูไวยากรณ์ของฟังก์ชัน fork() กัน:

ฟังก์ชัน fork() ส่งคืน PID ของกระบวนการลูกซึ่งเป็นผลลัพธ์ในกระบวนการหลัก ในขณะที่การเรียกเดียวกันไม่มีผลกระทบต่อกระบวนการลูกและส่งกลับ 0 ตามผลลัพธ์ กลไกนี้ช่วยให้เราสามารถรันโค้ดที่แตกต่างกันในทั้งสองกระบวนการผ่านเงื่อนไข "if" โดยที่เงื่อนไขคือค่าที่ส่งคืนของ fork() มาดูแนวคิดต่อไปนี้:

ถ้า (ส้อม () ==0)
   {
    รหัสสำหรับกระบวนการลูก
   

อย่างอื่น
    {
     รหัสสำหรับกระบวนการหลัก
   

ด้วยวิธีนี้ กระบวนการหลักจะดำเนินการโค้ดที่อยู่ระหว่างเครื่องหมายปีกกาของคำสั่ง "else" ในขณะที่กระบวนการลูกดำเนินการโค้ดที่อยู่ในคำสั่ง "if"

เรามาดูสิ่งนี้ด้วยตัวอย่างง่ายๆ รหัสต่อไปนี้ที่เราเห็นประกอบด้วยสองลูปไม่สิ้นสุด ในกระบวนการหลัก โปรแกรมจะอยู่ในลูปที่สอดคล้องกับคำสั่ง “else” ซึ่งแสดงข้อความ “เขียนสำหรับกระบวนการหลัก” ในขณะที่กระบวนการลูกที่สร้างโดย fork() จะตกอยู่ในคำสั่ง “if” ซึ่งแสดงข้อความ “เขียนสำหรับกระบวนการลูก”

#รวม
#รวม

เป็นโมฆะ main(){

ถ้า (ส้อม () ==0)
   {
    ในขณะที่ (1){
    printf ("เขียนโดยกระบวนการลูก \n");
    นอน(5);}
   

อย่างอื่น
    {
      ในขณะที่ (1){
     printf ("เขียนโดยกระบวนการหลัก \n");
     นอน(2);}
    }

รูปภาพต่อไปนี้แสดงการรวบรวมและการทำงานของโค้ดนี้ ดังที่เห็นในคอนโซลคำสั่ง แต่ละกระบวนการดำเนินการโค้ดที่แตกต่างกัน:

การเรียนรู้ fork() และ exec() ใน C:อธิบายการสร้างและเปลี่ยนกระบวนการ

ฟังก์ชัน ExecXXX() ในภาษา C

ฟังก์ชันตระกูล execXXX() จะแทนที่กระบวนการที่กำลังทำงานอยู่ด้วยกระบวนการใหม่ รูปภาพของกระบวนการใหม่จะถูกคัดลอกไปยังพื้นที่หน่วยความจำที่จัดสรรให้กับกระบวนการที่ถูกแทนที่ เก็บรักษา PID และทรัพยากรที่จัดสรร เหนือสิ่งอื่นใด

ฟังก์ชันในกลุ่มนี้ซึ่งกำหนดไว้ในส่วนหัว "unistd.h" ใช้วิธีการเรียกที่แตกต่างกัน ขึ้นอยู่กับอินพุตและเป็นประเภท "variadics" เพื่อให้สามารถส่งผ่านรายการอาร์กิวเมนต์หรือพอยน์เตอร์ที่ไม่ระบุจากกระบวนการเก่าไปยังกระบวนการใหม่ได้ ต่อไป มาดูไวยากรณ์ของแต่ละฟังก์ชันกัน

int execl(const char *path, const char *arg, ...(char  *) NULL );
int execlp(const char *ไฟล์, const char *arg, ... (ถ่าน  *) NULL );
int execle(const char *path, const char *arg, ... , (char *) NULL, char     * const envp[] );

int execv(const char *เส้นทาง, ถ่าน *const argv[]);
int execvp(const char *ไฟล์, char *const argv[]);
int execvpe(const char *ไฟล์, char *const argv[],
                    ถ่าน *const envp[]);

ฟังก์ชัน execl(), execle() และ execv() ใช้ตัวชี้เป็นอาร์กิวเมนต์อินพุตแรกไปยังสตริงที่มีพาธสัมบูรณ์ของไฟล์ปฏิบัติการของกระบวนการใหม่ ในขณะที่ execlp(), execvp() และ execvpe() ใช้ชื่อของไฟล์ในไดเร็กทอรีปัจจุบัน อินพุตที่สองคืออาร์กิวเมนต์ที่คุณส่งไปยังกระบวนการใหม่ สิ่งเหล่านี้ต้องเป็นสตริง const char *arg หรือรายการพอยน์เตอร์ไปยังสตริง char *const argv[]

ตอนนี้ มาดูตัวอย่างที่ใช้ฟังก์ชัน execv() เพื่อแทนที่กระบวนการและส่งผ่านอาร์กิวเมนต์อินพุตจากโปรแกรมหนึ่งไปยังอีกโปรแกรมหนึ่ง

ในการดำเนินการนี้ เราได้สร้างโค้ดง่ายๆ สองโค้ดขึ้นมา หนึ่งคือกระบวนการหลักที่เรียกใช้ฟังก์ชัน execv() เพื่อดำเนินการกระบวนการลูก เมื่อฟังก์ชัน execv() เริ่มต้นกระบวนการลูก มันจะส่งผ่านอาร์กิวเมนต์อินพุตสองตัวในรูปแบบของสตริงที่กระบวนการลูกจะดึงข้อมูลและแสดงบนเชลล์

กระบวนการย่อย

กระบวนการลูกเป็นโค้ดง่ายๆ ที่พิมพ์ข้อความ “ฉันเป็นกระบวนการลูก” ดึงข้อมูลอาร์กิวเมนต์อินพุตที่ส่งโดยกระบวนการหลัก และแสดงไว้บนเชลล์ นี่คือรหัสสำหรับกระบวนการลูก:

 
#รวม
#รวม
#รวม
 
int main(int argc, ถ่าน *argv[])
{
 printf ("ฉันเป็นกระบวนการลูก\n\n");
 printf ("อาร์กิวเมนต์ 1:%s\n", argv[1]);
 printf ("อาร์กิวเมนต์ 2:%s\n", argv[2]);
}
 

เรารวบรวมโค้ดนี้และบันทึกการผลิตไว้ใน “Documents” ภายใต้ชื่อ “child” พร้อมด้วยนามสกุล “.bin” ดังที่แสดงในตัวอย่างต่อไปนี้:

 
~$ เอกสาร gcc/child.c -o เอกสาร/child.bin
 

ด้วยวิธีนี้ เราจะบันทึกไฟล์ปฏิบัติการย่อยไว้ใน "เอกสาร" เส้นทางของการเรียกทำงานนี้คือเส้นทางอาร์กิวเมนต์อินพุตเมื่อเรียก execv() ในกระบวนการหลัก

กระบวนการหลัก

กระบวนการหลักคือกระบวนการที่เราเรียกใช้ฟังก์ชัน execv() เพื่อแทนที่ด้วยกระบวนการลูก ในโค้ดนี้ เรากำหนดอาร์เรย์ของพอยน์เตอร์ให้กับสตริงที่แสดงถึงอาร์กิวเมนต์อินพุตของกระบวนการที่ฟังก์ชัน execv() เปิดขึ้น

ในภาพประกอบต่อไปนี้ คุณสามารถดูวิธีการสร้างอาร์เรย์ของตัวชี้ไปยังสตริงได้อย่างถูกต้อง ในกรณีนี้ ประกอบด้วยพอยน์เตอร์สี่ตัวและเรียกว่า “arg_Ptr[]”

เมื่อกำหนดอาร์เรย์ของพอยน์เตอร์แล้ว แต่ละพอยน์เตอร์จะต้องถูกกำหนดด้วยสตริงที่มีอาร์กิวเมนต์อินพุตที่เราส่งไปยังกระบวนการลูก ตามกฎทั่วไปสำหรับการใช้ฟังก์ชัน execxx() อาร์กิวเมนต์แรกควรเป็นสตริงที่มีชื่อและนามสกุลของไฟล์ปฏิบัติการ และตัวชี้สุดท้ายควรเป็น NULL

ดังนั้นเราจึงกำหนดอาร์กิวเมนต์ที่เกี่ยวข้องในรูปแบบสตริงให้กับแต่ละพอยน์เตอร์:

 
arg_Ptr[0] ="child.bin";
arg_Ptr[1] =" สวัสดีจาก ";
arg_Ptr[2] =" กระบวนการ 2 ";
arg_Ptr[3] =โมฆะ;
 

ขั้นตอนต่อไปคือการเรียกใช้ฟังก์ชัน execv() โดยส่งผ่านสตริงที่มีเส้นทางสัมบูรณ์ของไฟล์ปฏิบัติการเป็นอาร์กิวเมนต์แรก และอาร์เรย์ของสตริง arg_Ptr[] เป็นอาร์กิวเมนต์ที่สอง คุณสามารถดูรหัสเต็มของกระบวนการหลักได้ดังต่อไปนี้:

 
 
#รวม 
#include 
#รวม 
#include 
#include 
 
int หลัก (){
printf ("ฉันเป็นกระบวนการหลัก");
ถ่าน *arg_Ptr[4];
arg_Ptr[0] ="child.c";
arg_Ptr[1] =" สวัสดีจาก ";
arg_Ptr[2] =" กระบวนการ 2 ";
arg_Ptr[3] =โมฆะ;
 
execv("/home/linuxhint/Documents/child.bin", arg_Ptr);
}
 

เรารวบรวมโค้ดนี้ซึ่งระบุเส้นทางของไฟล์ “.c” และชื่อของเอาต์พุต:

 
~$ gcc เอกสาร/parent.c -o รูปแบบ
 

จากนั้นเรารันเอาต์พุต:

กระบวนการหลักจะแสดงข้อความ “ฉันเป็นกระบวนการหลัก” สร้างอาร์เรย์สตริงโดยกำหนดสตริงให้กับอาร์กิวเมนต์อินพุตแต่ละรายการที่ส่งผ่านไปยังกระบวนการถัดไป และเรียกใช้ฟังก์ชัน execv()

หากฟังก์ชัน execv() ดำเนินการได้สำเร็จ ไฟล์ปฏิบัติการ "child.bin" จะแทนที่กระบวนการหลักและรับช่วง ID และหน่วยความจำที่จัดสรรไว้ ดังนั้น การดำเนินการนี้ไม่สามารถยกเลิกได้

กระบวนการลูกจะแสดงข้อความ “ฉันเป็นกระบวนการลูก” และดึงข้อมูลอาร์กิวเมนต์อินพุตแต่ละรายการที่ส่งผ่านโดยกระบวนการหลักเพื่อแสดงบนคอนโซลคำสั่ง

การเรียนรู้ fork() และ exec() ใน C:อธิบายการสร้างและเปลี่ยนกระบวนการ

ฟังก์ชัน Fork() และ Execve() รวมกันเพื่อสร้างกระบวนการใหม่ใน Linux

ดังที่เราได้เห็นมาแล้ว ฟังก์ชัน fork() จะทำซ้ำกระบวนการ ในขณะที่ execve() จะแทนที่กระบวนการ ในตัวอย่างนี้ เราจะดูว่าเราสามารถใช้ทั้งสองฟังก์ชันร่วมกันเพื่อเปิดกระบวนการใหม่จากสำเนาที่ถูกแทนที่ได้อย่างไร ในการทำเช่นนี้ เราได้รวมโค้ดจากทั้งสองฟังก์ชันที่เราเห็นก่อนหน้านี้ เพื่อให้ fork() ทำซ้ำกระบวนการหลัก และ execve() แทนที่ด้วยไฟล์ปฏิบัติการ ซึ่งในกรณีนี้คืออันเดียวกับที่เราใช้ในตัวอย่างนี้ “child.bin” ก่อนหน้านี้

ตอนนี้ เรานำไฟล์ว่างที่มีนามสกุล “.c” มาใส่โค้ดโปรแกรมที่เราเห็นในตัวอย่างฟังก์ชัน fork()

ในโปรแกรมนี้ เราแก้ไขเฉพาะโค้ดของกระบวนการลูก เพื่อให้ฟังก์ชัน execv() แทนที่ด้วยไฟล์ปฏิบัติการ "child.bin" ในขณะที่งานหลักจะเหมือนกันกับตัวอย่างฟังก์ชัน fork()

ในการดำเนินการนี้ เราจะคัดลอกเนื้อหาของฟังก์ชัน main() จากตัวอย่างฟังก์ชัน execv() และแทนที่เนื้อหาของคำสั่ง “if” ด้วยโค้ดนี้ ตอนนี้เรามาดูกันว่าโปรแกรมที่สมบูรณ์จะเป็นอย่างไร:

#รวม
#รวม

เป็นโมฆะ main(){

ถ้า (ส้อม () ==0)
   {
    printf ("ฉันเป็นกระบวนการลูก\n");
    ถ่าน *arg_Ptr[5];
    arg_Ptr[0] ="child.c";
    arg_Ptr[1] =" สวัสดีจาก ";
    arg_Ptr[2] =" กระบวนการ 2 ";
    arg_Ptr[3] =โมฆะ;
    execv(/home/linuxhint/Documents/child.bin", arg_Ptr);
   

อย่างอื่น
    {
      ในขณะที่ (1){
     printf ("

เขียนโดยกระบวนการหลัก \n");
     นอน(3);}
    }
}

ด้วยวิธีนี้ กระบวนการจะถูกทำซ้ำโดยการสร้างกระบวนการใหม่บนระบบ จากนั้นฟังก์ชัน execv() จะแทนที่ด้วยกระบวนการจากไฟล์ปฏิบัติการ “child.bin” ต่อไปเราจะเห็นภาพที่มีการคอมไพล์โค้ดนี้

การเรียนรู้ fork() และ exec() ใน C:อธิบายการสร้างและเปลี่ยนกระบวนการ

ดังที่เราเห็นในภาพ ฟังก์ชัน fork() จะสร้างกระบวนการใหม่โดยการทำซ้ำ ซึ่งฟังก์ชัน execv() จะแทนที่ด้วยไฟล์ปฏิบัติการ “child.bin”

บทสรุป

ในบทความ Linuxhint นี้ เราได้แสดงให้คุณเห็นถึงวิธีการใช้ฟังก์ชัน fork() และ execv() เพื่อเปิดกระบวนการใหม่ใน Linux เราได้แสดงคำอธิบายโดยย่อของแต่ละฟังก์ชันเหล่านี้ ไวยากรณ์ และอาร์กิวเมนต์อินพุตและเอาต์พุต เพื่อช่วยให้คุณเข้าใจวิธีการทำงานได้ดีขึ้น เราได้รวมตัวอย่างของแต่ละฟังก์ชันเหล่านี้ไว้ด้วย ซึ่งเราได้เรียนรู้วิธีการโทรและงานที่แต่ละฟังก์ชันทำ หลังจากดูทั้งสองฟังก์ชันแยกกันแล้ว เราได้อธิบายวิธีใช้ทั้งสองฟังก์ชันร่วมกันเพื่อนำวิธีการที่ภาษา C มอบให้ไปใช้เพื่อสร้างกระบวนการใหม่ใน Linux